Built via two more parallel agent workflows (read schema -> return files), integrated + verified. Admin: /admin/emulator (emulator_settings + emulator_texts key/value editor), /admin/badges (give-badge via RCON), /admin/rare-values (CRUD), /admin/housekeeping (CRUD), /admin/email-templates (CRUD), /admin/photos (moderation). Public: /rares (+[category]), /leaderboard (credits/diamonds/duckets), /guilds (+[id] members), /redeem (voucher -> sendCurrency). Expanded: /u/[username] now shows badges + photos + guestbook (post form); /news/[slug] now shows reactions + comments (comment form). Reactions tallied in JS (Prisma groupBy typing avoided). Self-hosted Nunito via next/font/google wired to --font-nunito (the atom theme font, no runtime external fetch). Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0.
42 lines
1.3 KiB
TypeScript
42 lines
1.3 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";
|
|
|
|
export async function giveBadge(formData: FormData): Promise<void> {
|
|
await requireStaff();
|
|
|
|
const userId = Number(formData.get("userId"));
|
|
const code = String(formData.get("code") ?? "").trim().slice(0, 32);
|
|
if (!(userId > 0) || code.length === 0) return;
|
|
|
|
// Fire the emulator command so the badge appears live for online users.
|
|
await rcon.giveBadge(userId, code);
|
|
|
|
// Persist the badge directly so it survives a relog / offline grant.
|
|
// users_badges has no unique (user_id, badge_code) constraint, so guard
|
|
// against duplicates and compute the next free slot ourselves.
|
|
try {
|
|
const existing = await prisma.usersBadges.findFirst({
|
|
where: { userId, badgeCode: code },
|
|
select: { id: true },
|
|
});
|
|
if (!existing) {
|
|
const max = await prisma.usersBadges.aggregate({
|
|
where: { userId },
|
|
_max: { slotId: true },
|
|
});
|
|
const slotId = (max._max.slotId ?? 0) + 1;
|
|
await prisma.usersBadges.create({
|
|
data: { userId, slotId, badgeCode: code },
|
|
});
|
|
}
|
|
} catch {
|
|
// Best-effort: the RCON grant already succeeded for online users.
|
|
}
|
|
|
|
revalidatePath("/admin/badges");
|
|
}
|