Add social login (Discord/Google) + batch-6 pages

Auth: NextAuth Discord + Google providers (enabled when env id+secret set);
OAuth signIn allowed only if a hotel account matches the email; jwt binds the
session to that account (id/rank/username). Login page gets social buttons.

Batch 6 (parallel agents): /friends (messenger_friendships), /guilds/[id]/forum
(threads), /admin/navigation (navigator config), /admin/maintenance (toggle
maintenance settings), /admin/alerts (alert_logs + send hotel alert via RCON).
Header (Friends) + admin nav (Alerts/Maintenance/Navigator) extended.

Verified: tsc exit 0, vitest 48/48, next build exit 0 (57 page routes).
This commit is contained in:
Simo committed 2026-06-28 14:13:41 +02:00
1 parent 73f0f54624
commit 486ce51559
12 files changed
+911 -3

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
import { saveMaintenance } from "@/actions/admin-maintenance";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
const KEYS = [
"maintenance_enabled",
"maintenance_message",
"min_maintenance_login_rank",
] as const;
export default async function AdminMaintenance() {
// Read the three maintenance rows from website_settings. Isolated so a DB
// hiccup degrades to defaults rather than 500-ing the page.
let rows: { key: string; value: string }[] = [];
try {
rows = await prisma.websiteSetting.findMany({
where: { key: { in: [...KEYS] } },
select: { key: true, value: true },
});
} catch {
rows = [];
}
const byKey = new Map(rows.map((r) => [r.key, r.value]));
const enabledRaw = byKey.get("maintenance_enabled") ?? "0";
const enabled = enabledRaw === "1" || enabledRaw.toLowerCase() === "true";
const message = byKey.get("maintenance_message") ?? "";
const minRankRaw = byKey.get("min_maintenance_login_rank") ?? "5";
const minRank = Number.parseInt(minRankRaw, 10);
const minRankValue = Number.isFinite(minRank) ? minRank : 5;
return (
<main>
<h1>Maintenance Mode</h1>
<p className="muted">
While maintenance is enabled, only staff (and users at or above the
minimum login rank) can log into the hotel. These values are stored in
<code> website_settings</code> and read live by the login flow.
</p>
{/* ── Current state ─────────────────────────────────────── */}
<div
className="card"
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
marginBottom: "1.5rem",
}}
>
<span
aria-hidden="true"
style={{
width: 12,
height: 12,
borderRadius: 999,
flexShrink: 0,
background: enabled
? "var(--color-danger)"
: "var(--color-secondary)",
}}
/>
<div>
<strong>
Maintenance is currently {enabled ? "ENABLED" : "disabled"}.
</strong>
<p className="muted" style={{ margin: "0.2rem 0 0" }}>
{enabled
? `Only users with rank ${minRankValue} or higher can log in right now.`
: "The hotel is open to everyone."}
</p>
</div>
</div>
{/* ── Edit form ─────────────────────────────────────────── */}
<form action={saveMaintenance} className="card">
<h3 style={{ marginTop: 0 }}>Settings</h3>
<label
htmlFor="mt-enabled"
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "1rem",
}}
>
<input
id="mt-enabled"
type="checkbox"
name="enabled"
value="1"
defaultChecked={enabled}
style={{ width: "auto" }}
/>
<span>Enable maintenance mode</span>
</label>
<div style={{ marginBottom: "1rem", maxWidth: 220 }}>
<label
htmlFor="mt-min-rank"
className="muted"
style={{ display: "block", marginBottom: "0.3rem" }}
>
Minimum login rank during maintenance
</label>
<input
id="mt-min-rank"
type="number"
name="min_rank"
min={0}
step={1}
defaultValue={minRankValue}
style={{ width: "100%" }}
/>
</div>
<div style={{ marginBottom: "1rem" }}>
<label
htmlFor="mt-message"
className="muted"
style={{ display: "block", marginBottom: "0.3rem" }}
>
Maintenance message (shown on the splash; basic HTML allowed)
</label>
<textarea
id="mt-message"
name="message"
rows={8}
defaultValue={message}
placeholder="We're working hard on the hotel — back soon!"
style={{ width: "100%", resize: "vertical" }}
/>
</div>
<button type="submit" className="btn btn-primary">
Save changes
</button>
</form>
</main>
);
}