Files
Epicnabbo-Catalogus-Updated…/src/actions/admin-users.ts
T
Simo 22d53d0e9c Add security middleware, audit log, alerts, PayPal, cron, radio + apps
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).
2026-06-28 15:10:19 +02:00

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);
}