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