diff --git a/src/api/services/panel-service.ts b/src/api/services/panel-service.ts new file mode 100644 index 0000000..a1dc382 --- /dev/null +++ b/src/api/services/panel-service.ts @@ -0,0 +1,244 @@ +import { addMinutes } from "date-fns"; +import type { ApiResponse, JobProgress, QueryParams } from "@/api/contracts/common"; +import type { + DatabaseAdminTool, + DnsRecord, + SystemDiscovery, + Website, +} from "@/api/contracts/models"; +import { mockResponse } from "@/api/client"; +import { hasConfiguredHttpApi, httpPanelService } from "@/api/services/http-panel-service"; +import { + accounts, + auditEntries, + automations, + backups, + currentUser, + databases, + dnsRecords, + domains, + files, + mailboxes, + metrics, + notifications, + resourceUsage, + runtimes, + securityFindings, + servers, + services, + websites, +} from "@/api/mocks/seed"; + +function searchBy(items: T[], params: QueryParams | undefined, keys: Array) { + if (!params?.search) return items; + const term = params.search.toLowerCase(); + return items.filter((item) => + keys.some((key) => + String(item[key] ?? "") + .toLowerCase() + .includes(term), + ), + ); +} + +export const mockPanelService = { + auth: { + me: () => mockResponse(currentUser), + login: (email: string, password?: string, code?: string) => { + void password; + void code; + return mockResponse({ + user: currentUser, + challenge: email.includes("2fa") ? "totp" : null, + sessionExpiresAt: addMinutes(new Date(), 45).toISOString(), + }); + }, + logout: () => mockResponse({ ok: true }), + }, + dashboard: { + summary: () => + mockResponse({ + user: currentUser, + account: accounts[0], + server: servers[0], + accounts, + servers, + resources: resourceUsage, + services, + notifications, + metrics, + }), + }, + websites: { + list: (params?: QueryParams) => + mockResponse(searchBy(websites, params, ["name", "domain", "runtime"])), + create: (payload: Partial) => + mockResponse({ + id: `site-${Date.now()}`, + name: payload.name ?? "New website", + domain: payload.domain ?? "new.example.com", + runtime: payload.runtime ?? "PHP", + status: "healthy", + ssl: "healthy", + environment: "production", + cpuLimit: "1 vCPU", + memoryLimit: "1 GB", + lastDeployment: new Date().toISOString(), + } satisfies Website), + }, + domains: { + list: (params?: QueryParams) => mockResponse(searchBy(domains, params, ["name", "type"])), + records: () => mockResponse(dnsRecords), + saveRecord: (record: Partial) => + mockResponse({ + id: record.id ?? `dns-${Date.now()}`, + name: record.name ?? "new.example.com.", + ttl: record.ttl ?? 14400, + class: "IN", + type: record.type ?? "A", + record: record.record ?? "198.51.100.40", + priority: record.priority, + } satisfies DnsRecord), + }, + files: { + list: (params?: QueryParams) => + mockResponse(searchBy(files, params, ["name", "path", "mime"])), + operation: (operation: string): Promise> => + mockResponse({ + id: `job-${Date.now()}`, + label: operation, + status: "running", + progress: 64, + startedAt: new Date().toISOString(), + message: "Mock operation is staged and will not touch the host filesystem.", + }), + }, + databases: { + list: (params?: QueryParams) => + mockResponse(searchBy(databases, params, ["name", "engine"])), + create: (name: string) => mockResponse({ id: `db-${Date.now()}`, name, status: "created" }), + adminTools: () => + mockResponse([ + { + id: "phpmyadmin", + name: "phpMyAdmin", + engine: "MySQL/MariaDB", + status: "available", + launchUrl: "/phpmyadmin/", + sso: false, + note: "Launches the local phpMyAdmin deployment in a separate browser tab.", + }, + { + id: "adminer", + name: "Adminer", + engine: "MySQL, MariaDB, PostgreSQL", + status: "planned", + launchUrl: null, + sso: false, + note: "Ready in the API contract, not exposed on this host.", + }, + { + id: "pgadmin", + name: "pgAdmin", + engine: "PostgreSQL", + status: "planned", + launchUrl: null, + sso: false, + note: "Ready in the API contract, not exposed on this host.", + }, + ]), + }, + email: { + list: (params?: QueryParams) => + mockResponse(searchBy(mailboxes, params, ["address", "domain"])), + create: (address: string) => + mockResponse({ id: `mail-${Date.now()}`, address, status: "created" }), + }, + metrics: { + history: () => mockResponse(metrics), + }, + security: { + findings: () => mockResponse(securityFindings), + }, + backups: { + list: () => mockResponse(backups), + create: (scope: string) => + mockResponse({ + id: `job-${Date.now()}`, + label: `Create ${scope} backup`, + status: "queued", + progress: 0, + startedAt: new Date().toISOString(), + }), + }, + automation: { + list: () => mockResponse(automations), + }, + notifications: { + list: () => mockResponse(notifications), + }, + audit: { + list: () => mockResponse(auditEntries), + }, + software: { + runtimes: () => mockResponse(runtimes), + }, + servers: { + list: () => mockResponse(servers), + services: () => mockResponse(services), + }, + system: { + discovery: () => + mockResponse({ + detectedAt: new Date().toISOString(), + nginx: { + available: true, + sites: [ + { + id: "mock-panel-site", + serverNames: ["cp.asabbagh.com"], + listens: ["443 ssl", "80"], + root: null, + proxyPasses: ["http://127.0.0.1:3110", "http://127.0.0.1:3020"], + tls: true, + source: "mock", + }, + ], + proxiedSites: [ + { + id: "mock-panel-site", + serverNames: ["cp.asabbagh.com"], + listens: ["443 ssl", "80"], + root: null, + proxyPasses: ["http://127.0.0.1:3110", "http://127.0.0.1:3020"], + tls: true, + source: "mock", + }, + ], + }, + docker: { + available: true, + containers: [ + { + id: "mock-phpmyadmin", + name: "coaxyspanel-phpmyadmin", + image: "phpmyadmin:latest", + status: "Up", + ports: "127.0.0.1:8089->80/tcp", + }, + ], + }, + currentPanel: { + hostname: "cp.asabbagh.com", + frontend: "http://127.0.0.1:3110", + backend: "http://127.0.0.1:3020", + }, + }), + }, +}; + +export type PanelService = typeof mockPanelService; + +export const panelService: PanelService = hasConfiguredHttpApi + ? (httpPanelService as PanelService) + : mockPanelService;