mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 13:45:23 +02:00
refactor(billing): remove unused functions and streamline Hostinger server data handling
This commit is contained in:
@@ -19,38 +19,6 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
// Map specs based on plan name
|
||||
function getVPSSpecs(planName: string) {
|
||||
const kvmMatch = planName.match(/KVM\s*(\d+)/i);
|
||||
const kvmNumber = kvmMatch ? Number.parseInt(kvmMatch[1] || "1", 10) : 1;
|
||||
|
||||
const specs = {
|
||||
1: { cpu: 1, ram: 4, storage: 50, bandwidth: 4 },
|
||||
2: { cpu: 2, ram: 8, storage: 100, bandwidth: 8 },
|
||||
4: { cpu: 4, ram: 16, storage: 200, bandwidth: 16 },
|
||||
8: { cpu: 8, ram: 32, storage: 400, bandwidth: 32 },
|
||||
};
|
||||
|
||||
return specs[kvmNumber as keyof typeof specs] || specs[1];
|
||||
}
|
||||
|
||||
// Get description based on plan name
|
||||
function getPlanDescription(planName: string) {
|
||||
const kvmMatch = planName.match(/KVM\s*(\d+)/i);
|
||||
const kvmNumber = kvmMatch ? Number.parseInt(kvmMatch[1] || "1", 10) : 1;
|
||||
|
||||
const descriptions = {
|
||||
1: "Perfect for small projects and personal websites",
|
||||
2: "Most popular plan for growing businesses",
|
||||
4: "High performance for demanding applications",
|
||||
8: "Maximum power for resource-intensive workloads",
|
||||
};
|
||||
|
||||
return (
|
||||
descriptions[kvmNumber as keyof typeof descriptions] || descriptions[1]
|
||||
);
|
||||
}
|
||||
|
||||
// Format billing period
|
||||
function formatBillingPeriod(period: number, unit: string): string {
|
||||
if (unit === "month") {
|
||||
@@ -80,7 +48,7 @@ export const ShowHostingerServers = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
@@ -113,9 +81,6 @@ export const ShowHostingerServers = () => {
|
||||
return monthlyPriceA - monthlyPriceB;
|
||||
})
|
||||
?.map((plan) => {
|
||||
const specs = getVPSSpecs(plan.name);
|
||||
const description = getPlanDescription(plan.name);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.id}
|
||||
@@ -132,9 +97,9 @@ export const ShowHostingerServers = () => {
|
||||
<CardTitle className="text-lg">{plan.name}</CardTitle>
|
||||
<Badge variant="secondary">VPS</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-sm">
|
||||
{/* <CardDescription className="text-sm">
|
||||
{description}
|
||||
</CardDescription>
|
||||
</CardDescription> */}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
@@ -143,19 +108,19 @@ export const ShowHostingerServers = () => {
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<Cpu className="h-4 w-4 text-blue-500" />
|
||||
<span>{specs.cpu} vCPU</span>
|
||||
<span>{plan.metadata.cpus} vCPU</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<MemoryStick className="h-4 w-4 text-green-500" />
|
||||
<span>{specs.ram}GB RAM</span>
|
||||
<span>{plan.metadata.memory}GB RAM</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<HardDrive className="h-4 w-4 text-orange-500" />
|
||||
<span>{specs.storage}GB NVMe</span>
|
||||
<span>{plan.metadata.disk_space}GB NVMe</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Globe className="h-4 w-4 text-indigo-500" />
|
||||
<span>{specs.bandwidth}TB</span>
|
||||
<span>{plan.metadata.bandwidth}TB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -216,9 +181,6 @@ export const ShowHostingerServers = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
ID: {price.id}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
@@ -278,9 +240,6 @@ export const ShowHostingerServers = () => {
|
||||
monthly
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
ID: {price.id}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -341,9 +300,6 @@ export const ShowHostingerServers = () => {
|
||||
monthly
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
ID: {price.id}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -406,21 +362,9 @@ export const ShowHostingerServers = () => {
|
||||
|
||||
{(!vpsPlans || vpsPlans.length === 0) && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Could not load VPS plans. Please verify your Hostinger API key.
|
||||
Could not load VPS plans. Please retry later.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="text-blue-600 mt-0.5">ℹ️</div>
|
||||
<div className="text-sm text-blue-800">
|
||||
<strong>For resellers:</strong> These are real prices from
|
||||
Hostinger API. Promotional pricing applies to the first billing
|
||||
period only. You can use the shown IDs to create servers via
|
||||
API.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,122 +1,10 @@
|
||||
import {
|
||||
fetchHetznerLocations,
|
||||
fetchHetznerServerTypes,
|
||||
fetchHetznerServers,
|
||||
} from "@dokploy/server/index";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
const HETZNER_API_URL = "https://api.hetzner.cloud/v1";
|
||||
|
||||
interface HetznerLocation {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
country: string;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
network_zone: string;
|
||||
}
|
||||
|
||||
interface HetznerServerType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
cores: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
prices: {
|
||||
location: string;
|
||||
price_hourly: {
|
||||
net: string;
|
||||
gross: string;
|
||||
};
|
||||
price_monthly: {
|
||||
net: string;
|
||||
gross: string;
|
||||
};
|
||||
}[];
|
||||
storage_type: string;
|
||||
cpu_type: string;
|
||||
architecture: string;
|
||||
}
|
||||
|
||||
interface HetznerServer {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
created: string;
|
||||
server_type: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
cores: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
};
|
||||
public_net: {
|
||||
ipv4: {
|
||||
ip: string;
|
||||
};
|
||||
ipv6: {
|
||||
ip: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchHetznerLocations(
|
||||
apiKey: string,
|
||||
): Promise<HetznerLocation[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/locations`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hetzner locations: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { locations?: HetznerLocation[] };
|
||||
return data.locations || [];
|
||||
}
|
||||
|
||||
async function fetchHetznerServerTypes(
|
||||
apiKey: string,
|
||||
): Promise<HetznerServerType[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/server_types`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hetzner server types: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
server_types?: HetznerServerType[];
|
||||
};
|
||||
return data.server_types || [];
|
||||
}
|
||||
|
||||
async function fetchHetznerServers(apiKey: string): Promise<HetznerServer[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/servers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Hetzner servers: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { servers?: HetznerServer[] };
|
||||
return data.servers || [];
|
||||
}
|
||||
|
||||
export const hetznerRouter = createTRPCRouter({
|
||||
locations: protectedProcedure.query(async () => {
|
||||
const apiKey = process.env.HETZNER_API_KEY;
|
||||
|
||||
@@ -1,228 +1,9 @@
|
||||
import {
|
||||
fetchHostingerCatalog,
|
||||
fetchHostingerDataCenters,
|
||||
fetchHostingerServers,
|
||||
} from "@dokploy/server/index";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { z } from "zod";
|
||||
|
||||
const HOSTINGER_API_URL = "https://api.hostinger.com";
|
||||
|
||||
interface HostingerCatalogItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
prices: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
currency: string;
|
||||
price: number; // en centavos
|
||||
first_period_price: number; // precio promocional en centavos
|
||||
period: number;
|
||||
period_unit: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface HostingerServer {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
ip_address: string;
|
||||
plan: {
|
||||
name: string;
|
||||
cpu: number;
|
||||
ram: number;
|
||||
storage: number;
|
||||
};
|
||||
location: string;
|
||||
}
|
||||
|
||||
interface HostingerTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
interface HostingerDataCenter {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
interface HostingerVMCreateResponse {
|
||||
order: {
|
||||
id: number;
|
||||
subscription_id: string;
|
||||
status: string;
|
||||
currency: string;
|
||||
subtotal: number;
|
||||
total: number;
|
||||
};
|
||||
virtual_machine: {
|
||||
id: number;
|
||||
subscription_id: string;
|
||||
plan: string;
|
||||
hostname: string;
|
||||
state: string;
|
||||
cpus: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
bandwidth: number;
|
||||
ipv4: Array<{
|
||||
id: number;
|
||||
address: string;
|
||||
ptr: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// Obtener catalog items (productos VPS con precios reales)
|
||||
async function fetchHostingerCatalog(
|
||||
apiKey: string,
|
||||
): Promise<HostingerCatalogItem[]> {
|
||||
const response = await fetch(
|
||||
`${HOSTINGER_API_URL}/api/billing/v1/catalog?category=VPS`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hostinger catalog: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as HostingerCatalogItem[];
|
||||
return data || [];
|
||||
}
|
||||
|
||||
// Obtener VPS existentes
|
||||
async function fetchHostingerServers(
|
||||
apiKey: string,
|
||||
): Promise<HostingerServer[]> {
|
||||
const response = await fetch(
|
||||
`${HOSTINGER_API_URL}/api/vps/v1/virtual-machines`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
// Si no hay servidores o falla, retornamos array vacío
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data?: HostingerServer[] };
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
// Obtener templates (sistemas operativos) disponibles
|
||||
async function fetchHostingerTemplates(
|
||||
apiKey: string,
|
||||
): Promise<HostingerTemplate[]> {
|
||||
const response = await fetch(`${HOSTINGER_API_URL}/api/vps/v1/templates`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hostinger templates: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data?: HostingerTemplate[] };
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
// Obtener data centers disponibles
|
||||
async function fetchHostingerDataCenters(
|
||||
apiKey: string,
|
||||
): Promise<HostingerDataCenter[]> {
|
||||
const response = await fetch(`${HOSTINGER_API_URL}/api/vps/v1/data-centers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hostinger data centers: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data?: HostingerDataCenter[] };
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
// Crear nuevo VPS
|
||||
async function createHostingerVPS(
|
||||
apiKey: string,
|
||||
params: {
|
||||
item_id: string;
|
||||
template_id: number;
|
||||
data_center_id: number;
|
||||
hostname?: string;
|
||||
password?: string;
|
||||
enable_backups?: boolean;
|
||||
public_key?: {
|
||||
name: string;
|
||||
key: string;
|
||||
};
|
||||
},
|
||||
): Promise<HostingerVMCreateResponse> {
|
||||
const response = await fetch(
|
||||
`${HOSTINGER_API_URL}/api/vps/v1/virtual-machines`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
item_id: params.item_id,
|
||||
setup: {
|
||||
template_id: params.template_id,
|
||||
data_center_id: params.data_center_id,
|
||||
hostname: params.hostname || `vps-${Date.now()}.hstgr.cloud`,
|
||||
password: params.password || generateRandomPassword(),
|
||||
enable_backups: params.enable_backups ?? true,
|
||||
install_monarx: false,
|
||||
...(params.public_key && { public_key: params.public_key }),
|
||||
},
|
||||
coupons: [],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(
|
||||
`Failed to create Hostinger VPS: ${response.statusText} - ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
return (await response.json()) as HostingerVMCreateResponse;
|
||||
}
|
||||
|
||||
// Generar contraseña aleatoria
|
||||
function generateRandomPassword(): string {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
||||
let password = "";
|
||||
for (let i = 0; i < 16; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
export const hostingerRouter = createTRPCRouter({
|
||||
vpsPlans: protectedProcedure.query(async () => {
|
||||
@@ -243,14 +24,6 @@ export const hostingerRouter = createTRPCRouter({
|
||||
return await fetchHostingerServers(apiKey);
|
||||
}),
|
||||
|
||||
templates: protectedProcedure.query(async () => {
|
||||
const apiKey = process.env.HOSTINGER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Hostinger API key not configured");
|
||||
}
|
||||
return await fetchHostingerTemplates(apiKey);
|
||||
}),
|
||||
|
||||
dataCenters: protectedProcedure.query(async () => {
|
||||
const apiKey = process.env.HOSTINGER_API_KEY;
|
||||
if (!apiKey) {
|
||||
@@ -258,42 +31,4 @@ export const hostingerRouter = createTRPCRouter({
|
||||
}
|
||||
return await fetchHostingerDataCenters(apiKey);
|
||||
}),
|
||||
|
||||
createVPS: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
item_id: z.string(),
|
||||
template_id: z.number(),
|
||||
data_center_id: z.number(),
|
||||
hostname: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
enable_backups: z.boolean().optional(),
|
||||
ssh_key_name: z.string().optional(),
|
||||
ssh_key_content: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const apiKey = process.env.HOSTINGER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Hostinger API key not configured");
|
||||
}
|
||||
|
||||
const publicKey =
|
||||
input.ssh_key_name && input.ssh_key_content
|
||||
? {
|
||||
name: input.ssh_key_name,
|
||||
key: input.ssh_key_content,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return await createHostingerVPS(apiKey, {
|
||||
item_id: input.item_id,
|
||||
template_id: input.template_id,
|
||||
data_center_id: input.data_center_id,
|
||||
hostname: input.hostname,
|
||||
password: input.password,
|
||||
enable_backups: input.enable_backups,
|
||||
public_key: publicKey,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -55,6 +55,8 @@ export * from "./utils/backups/utils";
|
||||
export * from "./utils/backups/web-server";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./templates/processors";
|
||||
export * from "./services/hostinger";
|
||||
export * from "./services/hetzner";
|
||||
|
||||
export * from "./utils/notifications/build-error";
|
||||
export * from "./utils/notifications/build-success";
|
||||
|
||||
118
packages/server/src/services/hetzner.ts
Normal file
118
packages/server/src/services/hetzner.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
const HETZNER_API_URL = "https://api.hetzner.cloud/v1";
|
||||
|
||||
interface HetznerLocation {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
country: string;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
network_zone: string;
|
||||
}
|
||||
|
||||
interface HetznerServerType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
cores: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
prices: {
|
||||
location: string;
|
||||
price_hourly: {
|
||||
net: string;
|
||||
gross: string;
|
||||
};
|
||||
price_monthly: {
|
||||
net: string;
|
||||
gross: string;
|
||||
};
|
||||
}[];
|
||||
storage_type: string;
|
||||
cpu_type: string;
|
||||
architecture: string;
|
||||
}
|
||||
|
||||
interface HetznerServer {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
created: string;
|
||||
server_type: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
cores: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
};
|
||||
public_net: {
|
||||
ipv4: {
|
||||
ip: string;
|
||||
};
|
||||
ipv6: {
|
||||
ip: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchHetznerLocations(
|
||||
apiKey: string,
|
||||
): Promise<HetznerLocation[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/locations`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hetzner locations: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { locations?: HetznerLocation[] };
|
||||
return data.locations || [];
|
||||
}
|
||||
|
||||
export async function fetchHetznerServerTypes(
|
||||
apiKey: string,
|
||||
): Promise<HetznerServerType[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/server_types`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hetzner server types: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
server_types?: HetznerServerType[];
|
||||
};
|
||||
return data.server_types || [];
|
||||
}
|
||||
|
||||
export async function fetchHetznerServers(
|
||||
apiKey: string,
|
||||
): Promise<HetznerServer[]> {
|
||||
const response = await fetch(`${HETZNER_API_URL}/servers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Hetzner servers: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { servers?: HetznerServer[] };
|
||||
return data.servers || [];
|
||||
}
|
||||
116
packages/server/src/services/hostinger.ts
Normal file
116
packages/server/src/services/hostinger.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
const HOSTINGER_API_URL = "https://developers.hostinger.com";
|
||||
|
||||
interface HostingerCatalogItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
prices: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
currency: string;
|
||||
price: number; // en centavos
|
||||
first_period_price: number; // precio promocional en centavos
|
||||
period: number;
|
||||
period_unit: string;
|
||||
}>;
|
||||
metadata: {
|
||||
cpus: string;
|
||||
memory: string;
|
||||
bandwidth: string;
|
||||
disk_space: string;
|
||||
network: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface HostingerServer {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
ip_address: string;
|
||||
plan: {
|
||||
name: string;
|
||||
cpu: number;
|
||||
ram: number;
|
||||
storage: number;
|
||||
};
|
||||
location: string;
|
||||
}
|
||||
|
||||
interface HostingerDataCenter {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
// Obtener catalog items (productos VPS con precios reales)
|
||||
export async function fetchHostingerCatalog(
|
||||
apiKey: string,
|
||||
): Promise<HostingerCatalogItem[]> {
|
||||
const response = await fetch(
|
||||
`${HOSTINGER_API_URL}/api/billing/v1/catalog?category=VPS`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hostinger catalog: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as HostingerCatalogItem[];
|
||||
console.log(data);
|
||||
return data || [];
|
||||
}
|
||||
|
||||
// Obtener VPS existentes
|
||||
export async function fetchHostingerServers(
|
||||
apiKey: string,
|
||||
): Promise<HostingerServer[]> {
|
||||
const response = await fetch(
|
||||
`${HOSTINGER_API_URL}/api/vps/v1/virtual-machines`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
// Si no hay servidores o falla, retornamos array vacío
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data?: HostingerServer[] };
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
// Obtener data centers disponibles
|
||||
export async function fetchHostingerDataCenters(
|
||||
apiKey: string,
|
||||
): Promise<HostingerDataCenter[]> {
|
||||
const response = await fetch(`${HOSTINGER_API_URL}/api/vps/v1/data-centers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Hostinger data centers: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data?: HostingerDataCenter[] };
|
||||
return data.data || [];
|
||||
}
|
||||
Reference in New Issue
Block a user