From 486ce515592b83646f37451b2e5aec0644663dd8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 28 Jun 2026 14:13:41 +0200 Subject: [PATCH] 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). --- src/actions/admin-alerts.ts | 27 +++++ src/actions/admin-maintenance.ts | 57 +++++++++ src/app/admin/alerts/page.tsx | 131 ++++++++++++++++++++ src/app/admin/layout.tsx | 3 + src/app/admin/maintenance/page.tsx | 144 ++++++++++++++++++++++ src/app/admin/navigation/page.tsx | 185 +++++++++++++++++++++++++++++ src/app/friends/page.tsx | 127 ++++++++++++++++++++ src/app/guilds/[id]/forum/page.tsx | 162 +++++++++++++++++++++++++ src/app/login/page.tsx | 22 ++++ src/components/site-header.tsx | 3 + src/env.ts | 7 ++ src/lib/auth.ts | 46 ++++++- 12 files changed, 911 insertions(+), 3 deletions(-) create mode 100644 src/actions/admin-alerts.ts create mode 100644 src/actions/admin-maintenance.ts create mode 100644 src/app/admin/alerts/page.tsx create mode 100644 src/app/admin/maintenance/page.tsx create mode 100644 src/app/admin/navigation/page.tsx create mode 100644 src/app/friends/page.tsx create mode 100644 src/app/guilds/[id]/forum/page.tsx diff --git a/src/actions/admin-alerts.ts b/src/actions/admin-alerts.ts new file mode 100644 index 00000000..e4cb2162 --- /dev/null +++ b/src/actions/admin-alerts.ts @@ -0,0 +1,27 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { requireStaff } from "@/lib/admin/guard"; +import { rcon } from "@/lib/services/rcon"; + +/** + * Broadcast a hotel-wide alert to every online user via RCON. + * + * Faithful to AtomCMS: the emulator's `hotelalert` command takes a single + * `message` payload. Staff-gated; the message is trimmed/bounded before send. + */ +export async function sendHotelAlert(formData: FormData): Promise { + await requireStaff(); + + const message = String(formData.get("message") ?? "").trim().slice(0, 1000); + if (!message) return; + + try { + await rcon.send("hotelalert", { message }); + } catch { + // Best-effort delivery (dead socket / emulator offline) — never 500 the + // admin page. The emulator writes its own alert_logs row on receipt. + } + + revalidatePath("/admin/alerts"); +} diff --git a/src/actions/admin-maintenance.ts b/src/actions/admin-maintenance.ts new file mode 100644 index 00000000..259e3df4 --- /dev/null +++ b/src/actions/admin-maintenance.ts @@ -0,0 +1,57 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { requireStaff } from "@/lib/admin/guard"; +import { prisma } from "@/lib/prisma"; +import { siteSettings } from "@/lib/services/site-settings"; + +// Maintenance mode lives in three CMS-owned website_settings rows (mirrors +// AtomCMS's MaintenanceToggle Livewire component): +// maintenance_enabled '1' | '0' +// maintenance_message free text (HTML allowed, shown on the splash) +// min_maintenance_login_rank int as string (min rank that may still log in) +// The Laravel login flow reads these via setting() to gate non-staff logins +// while maintenance is on, so the website_settings keys are the source of truth. + +const KEY_ENABLED = "maintenance_enabled"; +const KEY_MESSAGE = "maintenance_message"; +const KEY_MIN_RANK = "min_maintenance_login_rank"; + +const COMMENTS: Record = { + [KEY_ENABLED]: "Determines whether maintenance is enabled or not", + [KEY_MESSAGE]: + "The maintenance message displayed to users while maintenance is activated", + [KEY_MIN_RANK]: + "The minimum rank required to login to the hotel during maintenance", +}; + +async function upsertSetting(key: string, value: string): Promise { + await prisma.websiteSetting.upsert({ + where: { key }, + update: { value }, + create: { key, value, comment: COMMENTS[key] ?? null }, + }); +} + +export async function saveMaintenance(formData: FormData): Promise { + await requireStaff(); + + // Checkbox: present only when ticked. Normalise to the '1'/'0' string the + // emulator/Laravel side expects. + const enabled = formData.get("enabled") != null ? "1" : "0"; + + const message = String(formData.get("message") ?? ""); + + // Coerce the rank to a non-negative integer; fall back to AtomCMS's default + // of 5 when the field is blank or garbage. + const rawRank = String(formData.get("min_rank") ?? "").trim(); + const parsedRank = Number.parseInt(rawRank, 10); + const minRank = Number.isFinite(parsedRank) && parsedRank >= 0 ? parsedRank : 5; + + await upsertSetting(KEY_ENABLED, enabled); + await upsertSetting(KEY_MESSAGE, message); + await upsertSetting(KEY_MIN_RANK, String(minRank)); + + siteSettings.reload(); + revalidatePath("/admin/maintenance"); +} diff --git a/src/app/admin/alerts/page.tsx b/src/app/admin/alerts/page.tsx new file mode 100644 index 00000000..12e08a88 --- /dev/null +++ b/src/app/admin/alerts/page.tsx @@ -0,0 +1,131 @@ +import { sendHotelAlert } from "@/actions/admin-alerts"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +// Read-only feed of alert_logs (prisma.alertLogs delegate): the emulator and +// CMS write rows here when hotel/user alerts fire. Auto-gated by the admin +// layout (requireStaff); only the mutation re-asserts staff in the action. + +function fromDate(d: Date | null | undefined): string { + return d ? d.toISOString().slice(0, 16).replace("T", " ") : "—"; +} + +// Map a free-text severity to a theme-token colour. Unknown severities fall +// back to muted grey rather than inventing a new palette. +function severityColor(severity: string): string { + switch (severity.toLowerCase()) { + case "critical": + case "error": + case "danger": + return "var(--color-danger)"; + case "warning": + case "warn": + return "var(--color-primary)"; + case "success": + return "var(--color-secondary)"; + case "info": + case "notice": + return "var(--color-accent)"; + default: + return "var(--color-text-muted)"; + } +} + +function Badge({ label, color }: { label: string; color: string }) { + return ( + + {label} + + ); +} + +export default async function AdminAlerts() { + let alerts: Awaited> = []; + try { + alerts = await prisma.alertLogs.findMany({ + orderBy: { id: "desc" }, + take: 100, + }); + } catch { + alerts = []; + } + + return ( +
+

Alerts

+

+ Read-only · 100 most recent alert_logs (newest first). +

+ +
+

Send hotel alert

+

+ Broadcasts a message to every online user via RCON (hotelalert). +

+
+