refactor(billing): remove unused functions and streamline Hostinger server data handling

This commit is contained in:
Mauricio Siu
2025-07-23 00:35:47 -06:00
parent 579a2262bf
commit bd4ff2dbf2
6 changed files with 254 additions and 451 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,
});
}),
});

View File

@@ -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";

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

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