feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2ff08e5127
commit
ed7db6e048
76 files changed
+4834
-1376
No files matched your search
@@ -1,23 +1,6 @@
|
||||
import os from "node:os";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
alertUser,
|
||||
disconnectUser,
|
||||
executeCommand,
|
||||
forwardUser,
|
||||
giveBadge,
|
||||
giveCredits,
|
||||
giveDiamonds,
|
||||
giveDuckets,
|
||||
hotelAlert,
|
||||
sendGift,
|
||||
setMotto,
|
||||
setRank,
|
||||
updateCatalog,
|
||||
updateNavigator,
|
||||
updateWordFilter,
|
||||
} from "@/actions/commandocentrum";
|
||||
import {
|
||||
DiagnosticRow,
|
||||
InfoItem,
|
||||
@@ -28,6 +11,7 @@ import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { CommandocentrumControls } from "./commandocentrum-controls";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -223,421 +207,7 @@ export default async function CommandoCentrum() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("emulatorControls")}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<form action={updateCatalog} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateCatalog")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateCatalogDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateCatalog")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action={updateWordFilter} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateWordFilter")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateWordFilterDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateWordFilter")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action={updateNavigator} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateNavigator")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateNavigatorDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateNavigator")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action={hotelAlert} className="admin-card mt-6">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("hotelAlert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("hotelAlertDesc")}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
name="message"
|
||||
required
|
||||
maxLength={512}
|
||||
placeholder={t("messageToBroadcast")}
|
||||
className="flex-1 min-w-[220px]"
|
||||
/>
|
||||
<button type="submit" className="btn btn-danger">
|
||||
{t("sendAlert")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* ── User RCON actions ──────────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("userActions")}</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("userActionsDesc")}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<form action={disconnectUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.disconnect")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("disconnectDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={t("usernameRequired")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-danger w-full">
|
||||
{t("actions.disconnect")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={alertUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.alert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("alertDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="message"
|
||||
type="text"
|
||||
placeholder={t("alertMessage")}
|
||||
required
|
||||
maxLength={512}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("actions.alert")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={forwardUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("forwardToRoom")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("forwardDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="roomId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("roomId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("forwardUser")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={giveCredits} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveCredits")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveCreditsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="credits"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveCredits")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={giveDuckets} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDuckets")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDucketsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveDuckets")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={giveDiamonds} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDiamonds")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDiamondsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveDiamonds")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={giveBadge} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveBadge")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveBadgeDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="badge"
|
||||
type="text"
|
||||
placeholder={t("badgeCode")}
|
||||
required
|
||||
maxLength={32}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveBadge")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={setMotto} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setMotto")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setMottoDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="motto"
|
||||
type="text"
|
||||
placeholder={t("newMotto")}
|
||||
required
|
||||
maxLength={127}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("setMotto")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={setRank} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setRank")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setRankDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="rank"
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
placeholder={t("rankPlaceholder")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("setRank")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={executeCommand} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("executeCommand")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("executeCommandDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="command"
|
||||
type="text"
|
||||
placeholder={t("commandPlaceholder")}
|
||||
required
|
||||
maxLength={100}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("execute")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={sendGift} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("sendGift")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("sendGiftDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="itemId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("furniId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="message"
|
||||
type="text"
|
||||
placeholder={t("giftMessage")}
|
||||
maxLength={255}
|
||||
defaultValue={t("defaultGiftMessage")}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("sendGift")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<CommandocentrumControls />
|
||||
|
||||
{/* ── Staff activity ─────────────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
|
||||
Reference in new issue
Block a user