Web-tier features completing the AtomCMS→Next.js conversion (slice 2): UI/UX: - Dark mode: html.dark CSS-var overrides + ThemeSwitcher (localStorage, no-flash boot script) wired into the nav. - i18n (next-intl, cookie-based / no URL routing): en + it catalogs, request.ts, provider in root layout, LanguageSwitcher; shell (nav, header, footer) fully translated. URLs + access-guard unchanged. - globals.css: --muted/--border aliases used across admin pages. User features: - /messages: offline messages + friend-request accept (server action re-reads session, two directional rows, idempotent). - Email verification: signed-token /verify route + sendVerification wired into register (best-effort, never blocks signup). - Article reactions: toggle UI on news/[slug] + server action. - Content moderation service (website_wordfilter + optional OpenAI moderations, fail-open) wired into article comments + guestbook. Admin CRUD parity (Filament replacement): - /admin/shop (+ new/[id]) packages CRUD + read-only orders. - /admin/transactions read-only PayPal log. - /admin/permissions, /admin/tags, /admin/ads (+ new/[id]), /admin/help-questions (+ new/[id]), /admin/radio/history, /admin/users/[id]/edit. All gated by requireStaff + logStaffActivity. Verified: tsc 0, vitest 48/48, next build 0 (all routes incl. new admin CRUD + /messages + /verify).
95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { redirect } from "next/navigation";
|
|
import { requireStaff } from "@/lib/admin/guard";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
|
|
|
// users_currency.type values for the non-credits currencies (mirror send-currency.ts).
|
|
// Credits live on users.credits; pixels/points live on the users row too;
|
|
// duckets/diamonds live in users_currency keyed by (user_id, type).
|
|
const DUCKETS_TYPE = 0;
|
|
const DIAMONDS_TYPE = 5;
|
|
|
|
function toInt(value: FormDataEntryValue | null, min = 0): number | null {
|
|
if (value == null) return null;
|
|
const raw = String(value).trim();
|
|
if (raw === "") return null;
|
|
const n = Number(raw);
|
|
if (!Number.isFinite(n)) return null;
|
|
const i = Math.trunc(n);
|
|
return i < min ? min : i;
|
|
}
|
|
|
|
/**
|
|
* Edit the SAFE website-managed fields of a users row (and the duckets/diamonds
|
|
* balances in users_currency). Never touches the password. Re-reads the staff
|
|
* user from the session and logs the action. emulator-owned users.id is Int.
|
|
*/
|
|
export async function updateUser(formData: FormData): Promise<void> {
|
|
// Never trust the client: re-check staff inside the action.
|
|
const staff = await requireStaff();
|
|
|
|
const userId = Number(formData.get("id"));
|
|
if (!Number.isInteger(userId) || userId <= 0) return;
|
|
|
|
const existing = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { id: true },
|
|
});
|
|
if (!existing) return;
|
|
|
|
// users row — only existing, safe columns.
|
|
const mailRaw = String(formData.get("mail") ?? "").trim();
|
|
const motto = String(formData.get("motto") ?? "").slice(0, 127);
|
|
const look = String(formData.get("look") ?? "").slice(0, 256);
|
|
const rank = toInt(formData.get("rank"), 1);
|
|
const credits = toInt(formData.get("credits"), 0);
|
|
const pixels = toInt(formData.get("pixels"), 0);
|
|
const points = toInt(formData.get("points"), 0);
|
|
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
mail: mailRaw === "" ? null : mailRaw.slice(0, 500),
|
|
motto,
|
|
look,
|
|
...(rank != null ? { rank } : {}),
|
|
...(credits != null ? { credits } : {}),
|
|
...(pixels != null ? { pixels } : {}),
|
|
...(points != null ? { points } : {}),
|
|
},
|
|
});
|
|
|
|
// users_currency — set exact balances for duckets / diamonds.
|
|
const duckets = toInt(formData.get("duckets"), 0);
|
|
const diamonds = toInt(formData.get("diamonds"), 0);
|
|
if (duckets != null) {
|
|
await prisma.usersCurrency.upsert({
|
|
where: { userId_type: { userId, type: DUCKETS_TYPE } },
|
|
update: { amount: duckets },
|
|
create: { userId, type: DUCKETS_TYPE, amount: duckets },
|
|
});
|
|
}
|
|
if (diamonds != null) {
|
|
await prisma.usersCurrency.upsert({
|
|
where: { userId_type: { userId, type: DIAMONDS_TYPE } },
|
|
update: { amount: diamonds },
|
|
create: { userId, type: DIAMONDS_TYPE, amount: diamonds },
|
|
});
|
|
}
|
|
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "user_edit",
|
|
description: `Edited account fields of user #${userId}`,
|
|
targetType: "user",
|
|
targetId: userId,
|
|
});
|
|
|
|
revalidatePath(`/admin/users/${userId}`);
|
|
revalidatePath(`/admin/users/${userId}/edit`);
|
|
redirect(`/admin/users/${userId}`);
|
|
}
|