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).
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { requireStaff } from "@/lib/admin/guard";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { rcon } from "@/lib/services/rcon";
|
|
import { type CurrencyName, sendCurrency } from "@/lib/services/send-currency";
|
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
|
|
|
const CURRENCIES: ReadonlySet<string> = new Set(["credits", "duckets", "diamonds", "points"]);
|
|
|
|
export async function giveCurrency(formData: FormData): Promise<void> {
|
|
const staff = await requireStaff();
|
|
const userId = Number(formData.get("userId"));
|
|
const type = String(formData.get("type"));
|
|
const amount = Number(formData.get("amount"));
|
|
if (userId > 0 && amount > 0 && CURRENCIES.has(type)) {
|
|
await sendCurrency({ rcon, db: prisma }, userId, type as CurrencyName, amount);
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "give_currency",
|
|
description: `Gave ${amount} ${type} to user #${userId}`,
|
|
targetType: "user",
|
|
targetId: userId,
|
|
});
|
|
}
|
|
revalidatePath(`/admin/users/${userId}`);
|
|
}
|
|
|
|
export async function setMotto(formData: FormData): Promise<void> {
|
|
await requireStaff();
|
|
const userId = Number(formData.get("userId"));
|
|
const motto = String(formData.get("motto") ?? "").slice(0, 127);
|
|
if (userId > 0) {
|
|
await prisma.user.update({ where: { id: userId }, data: { motto } });
|
|
await rcon.setMotto(userId, motto);
|
|
}
|
|
revalidatePath(`/admin/users/${userId}`);
|
|
}
|
|
|
|
export async function setRank(formData: FormData): Promise<void> {
|
|
const staff = await requireStaff();
|
|
const userId = Number(formData.get("userId"));
|
|
const rank = Number(formData.get("rank"));
|
|
if (userId > 0 && rank > 0) {
|
|
await prisma.user.update({ where: { id: userId }, data: { rank } });
|
|
await rcon.setRank(userId, rank);
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "rank_change",
|
|
description: `Set rank of user #${userId} to ${rank}`,
|
|
targetType: "user",
|
|
targetId: userId,
|
|
});
|
|
}
|
|
revalidatePath(`/admin/users/${userId}`);
|
|
}
|
|
|
|
export async function alertUser(formData: FormData): Promise<void> {
|
|
await requireStaff();
|
|
const userId = Number(formData.get("userId"));
|
|
const message = String(formData.get("message") ?? "").trim();
|
|
if (userId > 0 && message) await rcon.alertUser(userId, message);
|
|
}
|
|
|
|
export async function disconnectUser(formData: FormData): Promise<void> {
|
|
await requireStaff();
|
|
const userId = Number(formData.get("userId"));
|
|
const username = String(formData.get("username") ?? "");
|
|
if (userId > 0) await rcon.disconnectUser(userId, username);
|
|
}
|