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 || [];
+}