Import source: backend/src/routes/panel
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,435 @@
|
|||||||
|
import type { FastifyPluginAsync } from "fastify";
|
||||||
|
import { requireSession, accountIdFor } from "../lib/auth.js";
|
||||||
|
import { fail, ok } from "../lib/envelope.js";
|
||||||
|
import {
|
||||||
|
databaseDto,
|
||||||
|
mailboxDto,
|
||||||
|
resourceUsageDto,
|
||||||
|
serverDto,
|
||||||
|
userDto,
|
||||||
|
} from "../lib/serializers.js";
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ name: "Web server", status: "healthy", uptime: "18 days", detail: "nginx active" },
|
||||||
|
{ name: "PHP-FPM", status: "healthy", uptime: "18 days", detail: "8 pools running" },
|
||||||
|
{ name: "Database", status: "healthy", uptime: "18 days", detail: "PostgreSQL responding" },
|
||||||
|
{ name: "DNS", status: "healthy", uptime: "18 days", detail: "authoritative zones synced" },
|
||||||
|
{ name: "Email", status: "warning", uptime: "7 days", detail: "14 deferred messages" },
|
||||||
|
{ name: "Redis", status: "healthy", uptime: "18 days", detail: "queue cache ready" },
|
||||||
|
{ name: "Queue", status: "healthy", uptime: "18 days", detail: "workers idle" },
|
||||||
|
{ name: "Firewall", status: "healthy", uptime: "18 days", detail: "38 IPs blocked" },
|
||||||
|
{ name: "Backup service", status: "healthy", uptime: "18 days", detail: "last run verified" },
|
||||||
|
{
|
||||||
|
name: "Monitoring service",
|
||||||
|
status: "healthy",
|
||||||
|
uptime: "18 days",
|
||||||
|
detail: "all probes green",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
id: "file-1",
|
||||||
|
name: "public_html",
|
||||||
|
path: "/home/coaxys/public_html",
|
||||||
|
type: "directory",
|
||||||
|
size: 0,
|
||||||
|
modifiedAt: "2026-06-12T10:12:00.000Z",
|
||||||
|
owner: "coaxys",
|
||||||
|
permissions: "0755",
|
||||||
|
mime: "inode/directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-2",
|
||||||
|
name: "index.php",
|
||||||
|
path: "/home/coaxys/public_html/index.php",
|
||||||
|
type: "file",
|
||||||
|
size: 18432,
|
||||||
|
modifiedAt: "2026-06-12T08:44:00.000Z",
|
||||||
|
owner: "coaxys",
|
||||||
|
permissions: "0644",
|
||||||
|
mime: "application/x-php",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-3",
|
||||||
|
name: ".env",
|
||||||
|
path: "/home/coaxys/app/.env",
|
||||||
|
type: "file",
|
||||||
|
size: 1024,
|
||||||
|
modifiedAt: "2026-06-11T22:03:00.000Z",
|
||||||
|
owner: "coaxys",
|
||||||
|
permissions: "0600",
|
||||||
|
mime: "text/plain",
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const runtimes = [
|
||||||
|
{
|
||||||
|
id: "php",
|
||||||
|
name: "PHP",
|
||||||
|
versions: ["8.1", "8.2", "8.3", "8.4"],
|
||||||
|
extensions: ["opcache", "intl", "imagick", "redis", "pdo_mysql"],
|
||||||
|
processManagers: ["PHP-FPM"],
|
||||||
|
environmentSchema: ["APP_ENV", "APP_DEBUG", "PHP_MEMORY_LIMIT"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nodejs",
|
||||||
|
name: "Node.js",
|
||||||
|
versions: ["20", "22", "24"],
|
||||||
|
extensions: ["npm", "pnpm", "yarn"],
|
||||||
|
processManagers: ["pm2", "systemd"],
|
||||||
|
environmentSchema: ["NODE_ENV", "PORT", "NPM_CONFIG_PRODUCTION"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "python",
|
||||||
|
name: "Python",
|
||||||
|
versions: ["3.11", "3.12", "3.13"],
|
||||||
|
extensions: ["pip", "venv", "uvicorn", "gunicorn"],
|
||||||
|
processManagers: ["systemd", "supervisor"],
|
||||||
|
environmentSchema: ["PYTHONPATH", "VIRTUAL_ENV", "PORT"],
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function accountDto(account: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
primaryDomain: string;
|
||||||
|
homeDirectory: string;
|
||||||
|
sharedIp: string;
|
||||||
|
dedicatedIp: string | null;
|
||||||
|
theme: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
username: account.username,
|
||||||
|
primaryDomain: account.primaryDomain,
|
||||||
|
homeDirectory: account.homeDirectory,
|
||||||
|
sharedIp: account.sharedIp,
|
||||||
|
dedicatedIp: account.dedicatedIp ?? undefined,
|
||||||
|
theme: account.theme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function iso(value: Date | null) {
|
||||||
|
return value?.toISOString() ?? new Date(0).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const panelRoutes: FastifyPluginAsync = async (app) => {
|
||||||
|
app.get("/api/dashboard/summary", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const accountId = accountIdFor(session);
|
||||||
|
const [account, accounts, serverList, resources, notifications, metrics] =
|
||||||
|
await Promise.all([
|
||||||
|
app.prisma.hostingAccount.findUniqueOrThrow({ where: { id: accountId } }),
|
||||||
|
app.prisma.hostingAccount.findMany({
|
||||||
|
where: { users: { some: { userId: session.userId } } },
|
||||||
|
orderBy: { username: "asc" },
|
||||||
|
}),
|
||||||
|
app.prisma.server.findMany({ orderBy: { name: "asc" } }),
|
||||||
|
app.prisma.resourceUsage.findMany({ where: { accountId }, orderBy: { key: "asc" } }),
|
||||||
|
app.prisma.notification.findMany({
|
||||||
|
where: { accountId },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 8,
|
||||||
|
}),
|
||||||
|
app.prisma.metricPoint.findMany({
|
||||||
|
where: { accountId },
|
||||||
|
orderBy: { timestamp: "asc" },
|
||||||
|
take: 48,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ok(request, {
|
||||||
|
user: userDto(session.user),
|
||||||
|
account: accountDto(account),
|
||||||
|
server: serverDto(serverList[0]!),
|
||||||
|
accounts: accounts.map(accountDto),
|
||||||
|
servers: serverList.map(serverDto),
|
||||||
|
resources: resources.map(resourceUsageDto),
|
||||||
|
services,
|
||||||
|
notifications: notifications.map((notification) => ({
|
||||||
|
id: notification.id,
|
||||||
|
title: notification.title,
|
||||||
|
category: notification.category,
|
||||||
|
read: notification.read,
|
||||||
|
createdAt: notification.createdAt.toISOString(),
|
||||||
|
body: notification.body,
|
||||||
|
})),
|
||||||
|
metrics: metrics.map((point) => ({
|
||||||
|
timestamp: point.timestamp.toISOString(),
|
||||||
|
cpu: point.cpu,
|
||||||
|
memory: point.memory,
|
||||||
|
disk: point.disk,
|
||||||
|
bandwidth: point.bandwidth,
|
||||||
|
requests: point.requests,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/files", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
return ok(request, files);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/files/operations", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
const body = request.body as { operation?: string } | undefined;
|
||||||
|
return ok(request, {
|
||||||
|
id: `job-${Date.now()}`,
|
||||||
|
label: body?.operation ?? "File operation",
|
||||||
|
status: "running",
|
||||||
|
progress: 64,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
message: "Operation is queued in the control plane and scoped to the hosting account.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/databases", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const databases = await app.prisma.database.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
include: { users: true },
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
return ok(request, databases.map(databaseDto));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/databases", async (request, reply) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const accountId = accountIdFor(session);
|
||||||
|
const body = request.body as { name?: string } | undefined;
|
||||||
|
const name = body?.name?.trim();
|
||||||
|
if (!name || name.length < 2) {
|
||||||
|
return fail(request, reply, 400, {
|
||||||
|
code: "VALIDATION_ERROR",
|
||||||
|
message: "Database name is required.",
|
||||||
|
fields: { name: "Use at least two characters." },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingDatabase = await app.prisma.database.findFirst({
|
||||||
|
where: { accountId, name },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (existingDatabase) {
|
||||||
|
return fail(request, reply, 409, {
|
||||||
|
code: "VALIDATION_ERROR",
|
||||||
|
message: "A database with this name already exists.",
|
||||||
|
fields: { name: "Choose a unique database name for this account." },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = await app.prisma.database.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
engine: "MySQL",
|
||||||
|
size: 0n,
|
||||||
|
tables: 0,
|
||||||
|
connections: 0,
|
||||||
|
lastBackup: new Date(),
|
||||||
|
accountId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply
|
||||||
|
.status(201)
|
||||||
|
.send(ok(request, { id: database.id, name: database.name, status: "created" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/email/accounts", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const mailboxes = await app.prisma.mailbox.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
include: { domain: true },
|
||||||
|
orderBy: { address: "asc" },
|
||||||
|
});
|
||||||
|
return ok(request, mailboxes.map(mailboxDto));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/email/accounts", async (request, reply) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const accountId = accountIdFor(session);
|
||||||
|
const body = request.body as { address?: string } | undefined;
|
||||||
|
const address = body?.address?.trim().toLowerCase();
|
||||||
|
const domainName = address?.split("@")[1];
|
||||||
|
const domain = domainName
|
||||||
|
? await app.prisma.domain.findFirst({
|
||||||
|
where: { accountId, name: domainName },
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!address || !address.includes("@") || !domain) {
|
||||||
|
return fail(request, reply, 400, {
|
||||||
|
code: "VALIDATION_ERROR",
|
||||||
|
message: "Mailbox address is invalid or its domain is not managed by this account.",
|
||||||
|
fields: { address: "Use an address on a managed domain." },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMailbox = await app.prisma.mailbox.findFirst({
|
||||||
|
where: { accountId, address },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (existingMailbox) {
|
||||||
|
return fail(request, reply, 409, {
|
||||||
|
code: "VALIDATION_ERROR",
|
||||||
|
message: "A mailbox with this address already exists.",
|
||||||
|
fields: { address: "Choose a unique mailbox address." },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailbox = await app.prisma.mailbox.create({
|
||||||
|
data: {
|
||||||
|
address,
|
||||||
|
quota: 5_000_000_000n,
|
||||||
|
used: 0n,
|
||||||
|
forwarding: false,
|
||||||
|
spamScore: 0,
|
||||||
|
authStatus: "healthy",
|
||||||
|
domainId: domain.id,
|
||||||
|
accountId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply
|
||||||
|
.status(201)
|
||||||
|
.send(ok(request, { id: mailbox.id, address: mailbox.address, status: "created" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/metrics/history", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const points = await app.prisma.metricPoint.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: { timestamp: "asc" },
|
||||||
|
take: 96,
|
||||||
|
});
|
||||||
|
return ok(
|
||||||
|
request,
|
||||||
|
points.map((point) => ({
|
||||||
|
timestamp: point.timestamp.toISOString(),
|
||||||
|
cpu: point.cpu,
|
||||||
|
memory: point.memory,
|
||||||
|
disk: point.disk,
|
||||||
|
bandwidth: point.bandwidth,
|
||||||
|
requests: point.requests,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/security/findings", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const findings = await app.prisma.securityFinding.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: [{ severity: "asc" }, { title: "asc" }],
|
||||||
|
});
|
||||||
|
return ok(request, findings);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/backups", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const backups = await app.prisma.backup.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
return ok(
|
||||||
|
request,
|
||||||
|
backups.map((backup) => ({
|
||||||
|
id: backup.id,
|
||||||
|
name: backup.name,
|
||||||
|
scope: backup.scope,
|
||||||
|
destination: backup.destination,
|
||||||
|
size: Number(backup.size),
|
||||||
|
status: backup.status,
|
||||||
|
encrypted: backup.encrypted,
|
||||||
|
createdAt: backup.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/backups", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
const body = request.body as { scope?: string } | undefined;
|
||||||
|
return ok(request, {
|
||||||
|
id: `job-${Date.now()}`,
|
||||||
|
label: `Create ${body?.scope ?? "full"} backup`,
|
||||||
|
status: "queued",
|
||||||
|
progress: 0,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/automation", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const automations = await app.prisma.automation.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
return ok(
|
||||||
|
request,
|
||||||
|
automations.map((automation) => ({
|
||||||
|
id: automation.id,
|
||||||
|
name: automation.name,
|
||||||
|
trigger: automation.trigger,
|
||||||
|
status: automation.status,
|
||||||
|
lastRun: iso(automation.lastRun),
|
||||||
|
successRate: automation.successRate,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/notifications", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const notifications = await app.prisma.notification.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
return ok(
|
||||||
|
request,
|
||||||
|
notifications.map((notification) => ({
|
||||||
|
id: notification.id,
|
||||||
|
title: notification.title,
|
||||||
|
category: notification.category,
|
||||||
|
read: notification.read,
|
||||||
|
createdAt: notification.createdAt.toISOString(),
|
||||||
|
body: notification.body,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/audit-logs", async (request) => {
|
||||||
|
const session = await requireSession(app, request);
|
||||||
|
const entries = await app.prisma.auditEntry.findMany({
|
||||||
|
where: { accountId: accountIdFor(session) },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
return ok(
|
||||||
|
request,
|
||||||
|
entries.map((entry) => ({
|
||||||
|
id: entry.id,
|
||||||
|
actor: entry.actor,
|
||||||
|
action: entry.action,
|
||||||
|
target: entry.target,
|
||||||
|
ipAddress: entry.ipAddress,
|
||||||
|
createdAt: entry.createdAt.toISOString(),
|
||||||
|
requestId: entry.requestId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/software/runtimes", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
return ok(request, runtimes);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/servers", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
const servers = await app.prisma.server.findMany({ orderBy: { name: "asc" } });
|
||||||
|
return ok(request, servers.map(serverDto));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/servers/services", async (request) => {
|
||||||
|
await requireSession(app, request);
|
||||||
|
return ok(request, services);
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user