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).
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
}
|