2026-03-20 22:38:17 -05:00
|
|
|
import { authenticateRequest } from "../../auth";
|
2026-03-26 15:28:49 -05:00
|
|
|
import { getConfig } from "../../config";
|
2026-03-20 22:38:17 -05:00
|
|
|
import {
|
|
|
|
|
getPendingClients,
|
|
|
|
|
getEnrollmentStats,
|
|
|
|
|
setClientEnrollmentStatus,
|
|
|
|
|
getClientEnrollmentStatus,
|
|
|
|
|
getClientIp,
|
|
|
|
|
banIp,
|
|
|
|
|
unbanIp,
|
|
|
|
|
isIpBanned,
|
|
|
|
|
listBannedIps,
|
2026-04-13 22:45:12 -05:00
|
|
|
getClientBuildOwnership,
|
2026-03-20 22:38:17 -05:00
|
|
|
} from "../../db";
|
|
|
|
|
import { logAudit, AuditAction } from "../../auditLog";
|
|
|
|
|
import * as clientManager from "../../clientManager";
|
|
|
|
|
import { setOnlineState } from "../../db";
|
2026-04-13 22:45:12 -05:00
|
|
|
import { requirePermission } from "../../rbac";
|
2026-04-14 10:35:33 -05:00
|
|
|
import { getUserById, getUserClientAccessScope, setUserClientAccessScope, setUserClientAccessRule } from "../../users";
|
2026-04-14 10:23:59 -05:00
|
|
|
|
2026-04-14 10:35:33 -05:00
|
|
|
|
|
|
|
|
function grantBuildOwnerAccess(clientId: string): void {
|
|
|
|
|
const ownership = getClientBuildOwnership(clientId);
|
|
|
|
|
if (!ownership || ownership.builtByUserId == null) return;
|
|
|
|
|
|
|
|
|
|
const owner = getUserById(ownership.builtByUserId);
|
|
|
|
|
if (!owner || owner.role === "admin") return;
|
|
|
|
|
|
|
|
|
|
const currentScope = getUserClientAccessScope(ownership.builtByUserId);
|
2026-04-14 10:23:59 -05:00
|
|
|
if (currentScope === "none") {
|
2026-04-14 10:35:33 -05:00
|
|
|
setUserClientAccessScope(ownership.builtByUserId, "allowlist");
|
2026-04-14 10:23:59 -05:00
|
|
|
}
|
|
|
|
|
if (currentScope === "none" || currentScope === "allowlist") {
|
2026-04-14 10:35:33 -05:00
|
|
|
setUserClientAccessRule(ownership.builtByUserId, clientId, "allow");
|
2026-04-14 10:23:59 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-13 14:11:49 -05:00
|
|
|
|
2026-04-13 22:45:12 -05:00
|
|
|
function getClientScopeFilters(userId: number, role: string): {
|
|
|
|
|
allowedClientIds?: string[];
|
|
|
|
|
deniedClientIds?: string[];
|
|
|
|
|
builtByUserId?: number;
|
|
|
|
|
requireBuildOwner?: boolean;
|
|
|
|
|
} {
|
2026-04-13 14:11:49 -05:00
|
|
|
if (role === "admin") return {};
|
2026-04-13 22:45:12 -05:00
|
|
|
return {
|
|
|
|
|
builtByUserId: userId,
|
|
|
|
|
requireBuildOwner: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canManageEnrollmentClient(
|
|
|
|
|
userId: number,
|
|
|
|
|
role: string,
|
|
|
|
|
clientId: string,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (role === "admin") return true;
|
|
|
|
|
if (role !== "operator") return false;
|
|
|
|
|
|
|
|
|
|
const ownership = getClientBuildOwnership(clientId);
|
|
|
|
|
if (!ownership || ownership.builtByUserId == null) return false;
|
|
|
|
|
return ownership.builtByUserId === userId;
|
2026-04-13 14:11:49 -05:00
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
2026-03-29 14:03:05 -05:00
|
|
|
let _postApproveHook: ((clientId: string) => void) | undefined;
|
|
|
|
|
|
|
|
|
|
export function setPostApproveHook(hook: (clientId: string) => void): void {
|
|
|
|
|
_postApproveHook = hook;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
export async function handleEnrollmentRoutes(
|
|
|
|
|
req: Request,
|
|
|
|
|
url: URL,
|
|
|
|
|
): Promise<Response | null> {
|
|
|
|
|
// GET /api/enrollment/pending
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/enrollment/pending") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
2026-04-13 14:11:49 -05:00
|
|
|
const scopeFilters = getClientScopeFilters(user.userId, user.role);
|
|
|
|
|
const clients = getPendingClients(scopeFilters);
|
2026-03-20 22:38:17 -05:00
|
|
|
return Response.json({ items: clients });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/enrollment/stats
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/enrollment/stats") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
2026-04-13 14:11:49 -05:00
|
|
|
const scopeFilters = getClientScopeFilters(user.userId, user.role);
|
|
|
|
|
const stats = getEnrollmentStats(scopeFilters);
|
2026-03-20 22:38:17 -05:00
|
|
|
return Response.json(stats);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/enrollment/settings
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/enrollment/settings") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
|
|
|
|
|
const config = getConfig();
|
|
|
|
|
return Response.json({
|
|
|
|
|
requireApproval: config.enrollment?.requireApproval ?? true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/:id/approve
|
|
|
|
|
const approveMatch = url.pathname.match(/^\/api\/enrollment\/([^/]+)\/approve$/);
|
|
|
|
|
if (req.method === "POST" && approveMatch) {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
const clientId = decodeURIComponent(approveMatch[1]);
|
|
|
|
|
const current = getClientEnrollmentStatus(clientId);
|
|
|
|
|
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
|
|
|
|
|
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, clientId)) {
|
2026-04-13 14:11:49 -05:00
|
|
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
setClientEnrollmentStatus(clientId, "approved", user.username);
|
|
|
|
|
|
2026-04-14 10:35:33 -05:00
|
|
|
grantBuildOwnerAccess(clientId);
|
2026-04-14 10:23:59 -05:00
|
|
|
|
2026-03-29 14:03:05 -05:00
|
|
|
_postApproveHook?.(clientId);
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_APPROVE,
|
|
|
|
|
targetClientId: clientId,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ enrollment: "approved" }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, status: "approved" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/:id/deny
|
|
|
|
|
const denyMatch = url.pathname.match(/^\/api\/enrollment\/([^/]+)\/deny$/);
|
|
|
|
|
if (req.method === "POST" && denyMatch) {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
const clientId = decodeURIComponent(denyMatch[1]);
|
|
|
|
|
const current = getClientEnrollmentStatus(clientId);
|
|
|
|
|
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
|
|
|
|
|
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, clientId)) {
|
2026-04-13 14:11:49 -05:00
|
|
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
setClientEnrollmentStatus(clientId, "denied");
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_DENY,
|
|
|
|
|
targetClientId: clientId,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ enrollment: "denied" }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, status: "denied" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/:id/reset
|
|
|
|
|
const resetMatch = url.pathname.match(/^\/api\/enrollment\/([^/]+)\/reset$/);
|
|
|
|
|
if (req.method === "POST" && resetMatch) {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
const clientId = decodeURIComponent(resetMatch[1]);
|
|
|
|
|
const current = getClientEnrollmentStatus(clientId);
|
|
|
|
|
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
|
|
|
|
|
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, clientId)) {
|
2026-04-13 14:11:49 -05:00
|
|
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
setClientEnrollmentStatus(clientId, "pending");
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, status: "pending" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/bulk
|
|
|
|
|
if (req.method === "POST" && url.pathname === "/api/enrollment/bulk") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
let body: any;
|
|
|
|
|
try {
|
|
|
|
|
body = await req.json();
|
|
|
|
|
} catch {
|
|
|
|
|
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ids = Array.isArray(body?.ids) ? body.ids.filter((id: unknown) => typeof id === "string") : [];
|
|
|
|
|
const action = body?.action;
|
|
|
|
|
|
|
|
|
|
if (!["approve", "deny", "reset", "ban-ip"].includes(action)) {
|
|
|
|
|
return Response.json({ error: "action must be 'approve', 'deny', 'reset', or 'ban-ip'" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
return Response.json({ error: "ids array is required" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action === "ban-ip") {
|
|
|
|
|
let banned = 0;
|
|
|
|
|
for (const id of ids) {
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, id)) continue;
|
2026-03-20 22:38:17 -05:00
|
|
|
const clientIp = getClientIp(id);
|
|
|
|
|
if (!clientIp) continue;
|
|
|
|
|
banIp(clientIp, `Bulk banned from purgatory by ${user.username}`);
|
|
|
|
|
setClientEnrollmentStatus(id, "denied");
|
|
|
|
|
const target = clientManager.getClient(id);
|
|
|
|
|
if (target) {
|
|
|
|
|
try { target.ws.close(4003, "banned"); } catch {}
|
|
|
|
|
setOnlineState(id, false);
|
|
|
|
|
}
|
|
|
|
|
banned++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_BULK,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ enrollment: { bulk: "ban-ip", count: banned } }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, action, updated: banned });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const status = action === "approve" ? "approved" : action === "deny" ? "denied" : "pending";
|
|
|
|
|
let updated = 0;
|
|
|
|
|
for (const id of ids) {
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, id)) continue;
|
2026-03-20 22:38:17 -05:00
|
|
|
const ok = setClientEnrollmentStatus(
|
|
|
|
|
id,
|
|
|
|
|
status as "approved" | "denied" | "pending",
|
|
|
|
|
action === "approve" ? user.username : undefined,
|
|
|
|
|
);
|
2026-03-29 14:03:05 -05:00
|
|
|
if (ok) {
|
|
|
|
|
updated++;
|
|
|
|
|
if (action === "approve") {
|
2026-04-14 10:35:33 -05:00
|
|
|
grantBuildOwnerAccess(id);
|
2026-03-29 14:03:05 -05:00
|
|
|
_postApproveHook?.(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_BULK,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ enrollment: { bulk: action, count: updated } }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, action, updated });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/:id/ban-ip
|
|
|
|
|
// Ban ip API makes me wanna fucking kms.
|
|
|
|
|
const banMatch = url.pathname.match(/^\/api\/enrollment\/([^/]+)\/ban-ip$/);
|
|
|
|
|
if (req.method === "POST" && banMatch) {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-04-13 22:45:12 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "clients:enroll");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
const clientId = decodeURIComponent(banMatch[1]);
|
|
|
|
|
const targetIp = getClientIp(clientId);
|
|
|
|
|
if (!targetIp) return Response.json({ error: "Client IP not found" }, { status: 404 });
|
|
|
|
|
|
2026-04-13 22:45:12 -05:00
|
|
|
if (!canManageEnrollmentClient(user.userId, user.role, clientId)) {
|
2026-04-13 14:11:49 -05:00
|
|
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:38:17 -05:00
|
|
|
banIp(targetIp, `Banned from purgatory by ${user.username} for client ${clientId}`);
|
|
|
|
|
setClientEnrollmentStatus(clientId, "denied");
|
|
|
|
|
|
|
|
|
|
const target = clientManager.getClient(clientId);
|
|
|
|
|
if (target) {
|
|
|
|
|
try { target.ws.close(4003, "banned"); } catch {}
|
|
|
|
|
setOnlineState(clientId, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_DENY,
|
|
|
|
|
targetClientId: clientId,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ bannedIp: targetIp }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, ip: targetIp });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/enrollment/banned-ips
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/enrollment/banned-ips") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-05-19 00:46:30 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "network:manage-bans");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
2026-04-13 22:45:12 -05:00
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
return Response.json({ items: listBannedIps() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DELETE /api/enrollment/banned-ips?ip=...
|
|
|
|
|
if (req.method === "DELETE" && url.pathname === "/api/enrollment/banned-ips") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-05-19 00:46:30 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "network:manage-bans");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
2026-04-13 22:45:12 -05:00
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
const ipToUnban = (url.searchParams.get("ip") || "").trim();
|
|
|
|
|
if (!ipToUnban) return Response.json({ error: "Missing ip parameter" }, { status: 400 });
|
|
|
|
|
if (!/^[0-9a-fA-F:.]{3,64}$/.test(ipToUnban)) return Response.json({ error: "Invalid IP format" }, { status: 400 });
|
|
|
|
|
if (!isIpBanned(ipToUnban)) return Response.json({ error: "IP is not banned" }, { status: 404 });
|
|
|
|
|
|
|
|
|
|
unbanIp(ipToUnban);
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_BULK,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ unbannedIp: ipToUnban }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/enrollment/ban-ip (manual IP ban)
|
|
|
|
|
if (req.method === "POST" && url.pathname === "/api/enrollment/ban-ip") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
2026-05-19 00:46:30 -05:00
|
|
|
try {
|
|
|
|
|
requirePermission(user, "network:manage-bans");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
2026-04-13 22:45:12 -05:00
|
|
|
}
|
2026-03-20 22:38:17 -05:00
|
|
|
|
|
|
|
|
let body: any;
|
|
|
|
|
try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); }
|
|
|
|
|
|
|
|
|
|
const ip = typeof body?.ip === "string" ? body.ip.trim() : "";
|
|
|
|
|
if (!ip) return Response.json({ error: "ip is required" }, { status: 400 });
|
|
|
|
|
if (!/^[0-9a-fA-F:.]{3,64}$/.test(ip)) return Response.json({ error: "Invalid IP format" }, { status: 400 });
|
|
|
|
|
|
|
|
|
|
const reason = typeof body?.reason === "string" ? body.reason.slice(0, 200) : `Banned from purgatory by ${user.username}`;
|
|
|
|
|
banIp(ip, reason);
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
username: user.username,
|
|
|
|
|
ip: "server",
|
|
|
|
|
action: AuditAction.ENROLLMENT_DENY,
|
|
|
|
|
success: true,
|
|
|
|
|
details: JSON.stringify({ bannedIp: ip, reason }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true, ip });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|