From bd4ff2dbf24afe1abb9c7a2cd6256729f5146a46 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:35:47 -0600 Subject: [PATCH] refactor(billing): remove unused functions and streamline Hostinger server data handling --- .../billing/show-hostinger-servers.tsx | 72 +---- apps/dokploy/server/api/routers/hetzner.ts | 122 +------- apps/dokploy/server/api/routers/hostinger.ts | 275 +----------------- packages/server/src/index.ts | 2 + packages/server/src/services/hetzner.ts | 118 ++++++++ packages/server/src/services/hostinger.ts | 116 ++++++++ 6 files changed, 254 insertions(+), 451 deletions(-) create mode 100644 packages/server/src/services/hetzner.ts create mode 100644 packages/server/src/services/hostinger.ts diff --git a/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx b/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx index 50b923709..c29d1f1ed 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx @@ -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 (
- + @@ -113,9 +81,6 @@ export const ShowHostingerServers = () => { return monthlyPriceA - monthlyPriceB; }) ?.map((plan) => { - const specs = getVPSSpecs(plan.name); - const description = getPlanDescription(plan.name); - return ( { {plan.name} VPS
- + {/* {description} - + */} @@ -143,19 +108,19 @@ export const ShowHostingerServers = () => {
- {specs.cpu} vCPU + {plan.metadata.cpus} vCPU
- {specs.ram}GB RAM + {plan.metadata.memory}GB RAM
- {specs.storage}GB NVMe + {plan.metadata.disk_space}GB NVMe
- {specs.bandwidth}TB + {plan.metadata.bandwidth}TB
@@ -216,9 +181,6 @@ export const ShowHostingerServers = () => { -
- ID: {price.id} -
))} @@ -278,9 +240,6 @@ export const ShowHostingerServers = () => { monthly )} -
- ID: {price.id} -
); })} @@ -341,9 +300,6 @@ export const ShowHostingerServers = () => { monthly )} -
- ID: {price.id} -
); })} @@ -406,21 +362,9 @@ export const ShowHostingerServers = () => { {(!vpsPlans || vpsPlans.length === 0) && (
- Could not load VPS plans. Please verify your Hostinger API key. + Could not load VPS plans. Please retry later.
)} - -
-
-
ℹ️
-
- For resellers: 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. -
-
-
diff --git a/apps/dokploy/server/api/routers/hetzner.ts b/apps/dokploy/server/api/routers/hetzner.ts index f07c5e655..41615064f 100644 --- a/apps/dokploy/server/api/routers/hetzner.ts +++ b/apps/dokploy/server/api/routers/hetzner.ts @@ -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 { - 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 { - 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 { - 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; diff --git a/apps/dokploy/server/api/routers/hostinger.ts b/apps/dokploy/server/api/routers/hostinger.ts index a1737b18b..ca6b4fa96 100644 --- a/apps/dokploy/server/api/routers/hostinger.ts +++ b/apps/dokploy/server/api/routers/hostinger.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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, - }); - }), }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index fca371ede..4529c0796 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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"; diff --git a/packages/server/src/services/hetzner.ts b/packages/server/src/services/hetzner.ts new file mode 100644 index 000000000..f9bed78bd --- /dev/null +++ b/packages/server/src/services/hetzner.ts @@ -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 { + 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 { + 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 { + 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 || []; +} diff --git a/packages/server/src/services/hostinger.ts b/packages/server/src/services/hostinger.ts new file mode 100644 index 000000000..5343302d5 --- /dev/null +++ b/packages/server/src/services/hostinger.ts @@ -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 { + 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 { + 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 { + 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 || []; +}