Add security middleware, audit log, alerts, PayPal, cron, radio + apps
Security (launch blockers): - src/middleware.ts (edge): forwards x-pathname + real client IP. - access-guard.ts (Node, from root layout): routes non-staff to /maintenance when maintenance mode is on, banned users to /banned. New /banned + /maintenance pages (the consumers the admin toggle was missing). Admin layout enforces force_staff_2fa before /admin. - staff-activity.ts audit log wired into ban/lift/give-currency/set-rank actions. Infra (parallel agents): alert service (alert_logs + Discord embed + email), PayPal top-up (create/capture API routes + /shop/topup), cron worker (scripts/jobs-worker.ts via croner: emulator-ping->alert, maintenance-check, bans-cleanup), social connections page, admin radio settings/banners/ranks. Public radio subsystem: /radio (+schedule, shouts+post, contests, giveaways, apply, leaderboard) and /apply/staff + /apply/team submission forms. Radio nav link added. .env.example documents the new optional vars. (radio song-requests dropped: its table is a stub in AtomCMS — columns added by un-modeled alter-migrations.) Verified: tsc exit 0, vitest 48/48, next build exit 0 (82 page routes).
This commit is contained in:
1 parent
e19debb795
commit
22d53d0e9c
40 files changed
+3781
-6
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Paths that must never be gated (otherwise banned/maintenance loop forever).
|
||||
const EXEMPT = ["/banned", "/maintenance", "/login", "/register", "/forgot", "/reset", "/api"];
|
||||
|
||||
function isExempt(path: string): boolean {
|
||||
return EXEMPT.some((p) => path === p || path.startsWith(`${p}/`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide access enforcement (called from the root layout): routes non-staff
|
||||
* to /maintenance when maintenance mode is on, and banned users to /banned.
|
||||
* Runs in the Node runtime so it can query the DB. The redirect decision is
|
||||
* computed inside try/catch and the redirect() (which throws NEXT_REDIRECT) is
|
||||
* issued OUTSIDE it.
|
||||
*/
|
||||
export async function enforceSiteAccess(): Promise<void> {
|
||||
const h = await headers();
|
||||
const path = h.get("x-pathname") ?? "/";
|
||||
if (isExempt(path)) return;
|
||||
|
||||
let target: string | null = null;
|
||||
try {
|
||||
const session = await auth();
|
||||
const rank = session?.user?.rank ?? 0;
|
||||
|
||||
if (await siteSettings.getBool("maintenance_enabled", false)) {
|
||||
const minLogin = Number(await siteSettings.get("min_maintenance_login_rank", "7")) || 7;
|
||||
if (rank < minLogin) target = "/maintenance";
|
||||
}
|
||||
|
||||
if (!target && session?.user?.id) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ban = await prisma.ban.findFirst({
|
||||
where: { userId: Number(session.user.id), banExpire: { gt: now } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (ban) target = "/banned";
|
||||
}
|
||||
} catch {
|
||||
// On any failure, fail open (don't lock the whole site out on a DB hiccup).
|
||||
}
|
||||
|
||||
if (target) redirect(target);
|
||||
}
|
||||
Reference in new issue
Block a user