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,51 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const FAR_FUTURE = Math.floor(Date.now() / 1000) + 50 * 365 * 24 * 3600;
|
||||
|
||||
export default async function BannedPage() {
|
||||
const session = await auth();
|
||||
let reason = "";
|
||||
let expire = 0;
|
||||
if (session?.user?.id) {
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ban = await prisma.ban.findFirst({
|
||||
where: { userId: Number(session.user.id), banExpire: { gt: now } },
|
||||
orderBy: { banExpire: "desc" },
|
||||
select: { banReason: true, banExpire: true },
|
||||
});
|
||||
if (ban) {
|
||||
reason = ban.banReason;
|
||||
expire = ban.banExpire;
|
||||
}
|
||||
} catch {
|
||||
// show generic message
|
||||
}
|
||||
}
|
||||
|
||||
const expiryText =
|
||||
expire === 0
|
||||
? ""
|
||||
: expire > FAR_FUTURE
|
||||
? "This ban is permanent."
|
||||
: `Expires ${new Date(expire * 1000).toISOString().slice(0, 16).replace("T", " ")}.`;
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: 540, margin: "2rem auto" }}>
|
||||
<div className="card" style={{ padding: "1.75rem", textAlign: "center" }}>
|
||||
<h1 style={{ marginTop: 0, color: "var(--color-danger)" }}>You are banned</h1>
|
||||
<p>Your account has been suspended from the hotel.</p>
|
||||
{reason ? (
|
||||
<p>
|
||||
<strong>Reason:</strong> {reason}
|
||||
</p>
|
||||
) : null}
|
||||
{expiryText ? <p className="muted">{expiryText}</p> : null}
|
||||
<p className="muted">If you believe this is a mistake, contact the staff team.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user