Add 2FA, email + password reset, and batch-7 pages
Auth (hand-built on the auth core): - 2FA: User model gains two_factor_secret/recovery_codes/confirmed_at (+ idempotent MariaDB migration). authorize() requires a valid TOTP code when 2FA is confirmed (secret decrypted via Laravel APP_KEY, fail-closed). Two-step login (precheckLogin reveals the code field). /settings/2fa enable/confirm/disable flow. - Password reset: nodemailer email service; PasswordReset model + migration; /forgot (request, generic response) + /reset (token sha256 + 1h TTL, sets argon2id hash). Login links to forgot. Batch 7 (parallel agents): /admin/commandocentrum (RCON controls + emulator_errors), social write actions (friend request + guild forum new thread), /help/[category], /badges (public). env: APP_KEY, APP_URL, SMTP_*. Nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (64 page routes).
This commit is contained in:
1 parent
486ce51559
commit
e668fa85ec
24 files changed
+1223
-34
No files matched your search
@@ -0,0 +1,53 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requireStaff } from "@/lib/admin/guard";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
|
||||
const PATH = "/admin/commandocentrum";
|
||||
|
||||
/** Rebuild the in-memory catalog on the emulator (rcon: updatecatalog). */
|
||||
export async function updateCatalog(): Promise<void> {
|
||||
await requireStaff();
|
||||
try {
|
||||
await rcon.updateCatalog();
|
||||
} catch {
|
||||
// RCON is best-effort; a dead socket must not 500 the admin page.
|
||||
}
|
||||
revalidatePath(PATH);
|
||||
}
|
||||
|
||||
/** Reload the chat word filter on the emulator (rcon: updatewordfilter). */
|
||||
export async function updateWordFilter(): Promise<void> {
|
||||
await requireStaff();
|
||||
try {
|
||||
await rcon.updateWordFilter();
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
revalidatePath(PATH);
|
||||
}
|
||||
|
||||
/** Reload navigator data on the emulator (rcon: updatenavigator, no payload). */
|
||||
export async function updateNavigator(): Promise<void> {
|
||||
await requireStaff();
|
||||
try {
|
||||
await rcon.send("updatenavigator", null);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
revalidatePath(PATH);
|
||||
}
|
||||
|
||||
/** Broadcast a hotel-wide alert to every connected user (rcon: hotelalert). */
|
||||
export async function hotelAlert(formData: FormData): Promise<void> {
|
||||
await requireStaff();
|
||||
const message = String(formData.get("message") ?? "").trim().slice(0, 512);
|
||||
if (!message) return;
|
||||
try {
|
||||
await rcon.send("hotelalert", { message });
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
revalidatePath(PATH);
|
||||
}
|
||||
Reference in new issue
Block a user