import "server-only"; import { z } from "zod"; import { PERMS } from "@/lib/permission-slugs"; import type { HousekeepingCommand } from "../../../foundation/commands/registry"; import { anyCapability } from "../../../foundation/contracts"; import { type PeopleMutationOperation, type PeopleMutationService, peopleMutationService, } from "../services/mutations"; export const USER_COMMAND_IDS = [ "people.user.update", "people.user.ban", "people.user.unban", "people.user.alert", "people.user.disconnect", "people.user.mute", "people.user.unmute", "people.user.reset-password", "people.user.send-currency", "people.user.trade-lock", "people.users.bulk-ban", "people.users.bulk-unban", "people.users.bulk-currency", "people.users.bulk-badge", ] as const; export type UserCommandId = (typeof USER_COMMAND_IDS)[number]; interface UserCommandOptions { readonly id: UserCommandId; readonly operation: PeopleMutationOperation; readonly capability: string; readonly input: z.ZodType; readonly requiresReason: boolean; readonly attempts?: number; } function userCommand( service: Pick, options: UserCommandOptions, ): HousekeepingCommand { return { id: options.id, owner: "people", risk: "sensitive", capability: anyCapability(options.capability), input: options.input, requiresReason: options.requiresReason, rateLimit: { attempts: options.attempts ?? 10, windowMs: 60_000 }, execute: (context, input) => service.execute( { capability: context.capability, correlationId: context.correlationId, }, options.operation, input, ), }; } const positiveId = z.number().int().positive(); const requiredText = (max: number) => z.string().min(1).max(max).regex(/\S/u); const userIds = z.array(positiveId).min(1).max(100); const optionalUpdateFields = { username: z.string().min(3).max(20).optional(), mail: z.email().optional(), rank: z.number().int().min(1).max(7).optional(), motto: z.string().max(127).optional(), credits: z.number().int().min(0).max(2_147_483_647).optional(), pixels: z.number().int().min(0).max(2_147_483_647).optional(), diamonds: z.number().int().min(0).max(2_147_483_647).optional(), duckets: z.number().int().min(0).max(2_147_483_647).optional(), }; const updateFields = z.union([ z.object({ ...optionalUpdateFields, username: z.string().min(3).max(20) }), z.object({ ...optionalUpdateFields, mail: z.email() }), z.object({ ...optionalUpdateFields, rank: z.number().int().min(1).max(7) }), z.object({ ...optionalUpdateFields, motto: z.string().max(127) }), z.object({ ...optionalUpdateFields, credits: z.number().int().min(0).max(2_147_483_647), }), z.object({ ...optionalUpdateFields, pixels: z.number().int().min(0).max(2_147_483_647), }), z.object({ ...optionalUpdateFields, diamonds: z.number().int().min(0).max(2_147_483_647), }), z.object({ ...optionalUpdateFields, duckets: z.number().int().min(0).max(2_147_483_647), }), ]); export function createUserCommands( service: Pick, ) { return [ userCommand(service, { id: "people.user.update", operation: "user.update", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId, fields: updateFields }), requiresReason: false, }), userCommand(service, { id: "people.user.ban", operation: "user.ban", capability: PERMS.USERS_BAN, input: z.object({ userId: positiveId, reason: requiredText(500), duration: z.number().int().min(0).max(87_600), type: z.enum(["account", "ip", "machine"]), ip: z.string().max(255).optional(), }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.unban", operation: "user.unban", capability: PERMS.USERS_BAN, input: z.object({ userId: positiveId }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.alert", operation: "user.alert", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId, message: requiredText(500) }), requiresReason: false, }), userCommand(service, { id: "people.user.disconnect", operation: "user.disconnect", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.mute", operation: "user.mute", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId, duration: z.number().int().min(0).max(87_600), }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.unmute", operation: "user.unmute", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.reset-password", operation: "user.reset-password", capability: PERMS.USERS_RESET_PASSWORD, input: z.object({ userId: positiveId }), requiresReason: true, attempts: 3, }), userCommand(service, { id: "people.user.send-currency", operation: "user.send-currency", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId, amount: z.number().int().min(1).max(1_000_000), }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.user.trade-lock", operation: "user.trade-lock", capability: PERMS.USERS_EDIT, input: z.object({ userId: positiveId, untilUnix: z.number().int().min(0).max(2_147_483_647), }), requiresReason: true, attempts: 5, }), userCommand(service, { id: "people.users.bulk-ban", operation: "users.bulk-ban", capability: PERMS.USERS_EDIT, input: z.object({ userIds, reason: requiredText(500), duration: z.number().int().min(0).max(315_360_000), }), requiresReason: true, attempts: 3, }), userCommand(service, { id: "people.users.bulk-unban", operation: "users.bulk-unban", capability: PERMS.USERS_EDIT, input: z.object({ userIds }), requiresReason: true, attempts: 3, }), userCommand(service, { id: "people.users.bulk-currency", operation: "users.bulk-currency", capability: PERMS.USERS_EDIT, input: z.object({ userIds, amount: z.number().int().min(1).max(1_000_000), type: z.enum(["credits", "pixels", "points"]), }), requiresReason: true, attempts: 3, }), userCommand(service, { id: "people.users.bulk-badge", operation: "users.bulk-badge", capability: PERMS.USERS_EDIT, input: z.object({ userIds, badgeCode: requiredText(20) }), requiresReason: true, attempts: 3, }), ] as const; } export const USER_COMMANDS = createUserCommands(peopleMutationService);