"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config"; import { type SystemMutationContext, type SystemMutationOperation, systemMutationService, } from "@/features/housekeeping/domains/system/services/mutations"; import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context"; import { createCorrelationId } from "@/features/housekeeping/foundation/correlation"; import { requirePermissionRateLimited } from "@/lib/admin/guard"; import { actionOk, adminAction } from "@/lib/foundation/action"; import type { AdminActionContext } from "@/lib/foundation/types"; import { HABBO_GAMEDATA_HOTEL_SETTING_KEY, normalizeHabboGamedataHotel, } from "@/lib/habbo-gamedata-hotel"; import { PERMS } from "@/lib/permissions"; const managedKeySet = new Set(MANAGED_SETTING_KEYS); function normalizeSettingValue(key: string, value: string): string { if (key === HABBO_GAMEDATA_HOTEL_SETTING_KEY) { return normalizeHabboGamedataHotel(value); } return value; } function actionMutationContext( ctx: Pick, ): SystemMutationContext { return { capability: createHousekeepingCapabilityContext( { id: Number(ctx.session.user.id), rank: Number(ctx.session.user.rank), username: ctx.session.user.username, }, ctx.permissions, ), correlationId: String(ctx.requestId), }; } function checkedMutationContext(staff: { id: number; rank: number; username: string; }): SystemMutationContext { const matches = (slug: string) => slug === PERMS.SETTINGS_EDIT; return { capability: createHousekeepingCapabilityContext(staff, { isSuperAdmin: false, has: matches, hasAny: (...slugs) => slugs.some(matches), hasAll: (...slugs) => slugs.every(matches), }), correlationId: createCorrelationId(), }; } async function runMutation( context: SystemMutationContext, operation: SystemMutationOperation, input: unknown, ): Promise { const result = await systemMutationService.execute(context, operation, input); if (!result.ok) throw new Error(result.error.messageKey); return result.data; } const saveManagedSchema = z.object({ settings: z.record(z.string(), z.string()), }); export const saveManagedSettings = adminAction( { permission: PERMS.SETTINGS_EDIT, schema: saveManagedSchema, rateLimitKey: "admin-settings-save", rateLimitMax: 30, }, async (ctx) => { const entries = Object.entries(ctx.data.settings) .filter(([key]) => managedKeySet.has(key)) .map(([key, value]) => [key, normalizeSettingValue(key, value)] as const); const mutation = (await runMutation( actionMutationContext(ctx), "configuration.settings.save", { settings: Object.fromEntries(entries) }, )) as { saved: number }; revalidatePath("/admin/settings"); revalidatePath("/admin/catalog"); return actionOk({ saved: mutation.saved }); }, ); export async function updateSetting(formData: FormData): Promise { const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT); const key = String(formData.get("key") ?? "") .normalize("NFC") .trim(); const value = normalizeSettingValue( key, String(formData.get("value") ?? "").normalize("NFC"), ); if (!key) return; await runMutation( checkedMutationContext(staff), "configuration.setting.update", { key, value }, ); revalidatePath("/admin/settings"); } export async function createSetting(formData: FormData): Promise { const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT); const key = String(formData.get("key") ?? "") .normalize("NFC") .trim() .slice(0, 255); const value = normalizeSettingValue( key, String(formData.get("value") ?? "").normalize("NFC"), ); const comment = String(formData.get("comment") ?? "") .normalize("NFC") .trim() .slice(0, 255); if (!key) return; await runMutation( checkedMutationContext(staff), "configuration.setting.create", { key, value, comment }, ); revalidatePath("/admin/settings"); } export async function deleteSetting(formData: FormData): Promise { const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT); const key = String(formData.get("key") ?? "") .normalize("NFC") .trim(); if (!key) return; await runMutation( checkedMutationContext(staff), "configuration.setting.delete", { key }, ); revalidatePath("/admin/settings"); }