/** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ import { passwordSchema, uuidSchema } from '@byline/core/validation' import { z } from 'zod' /** * Zod request/response schemas for the admin-users commands. * * Both input and output are validated — response validation keeps the * admin surface honest about what it promises downstream clients. The * DTO shaper in `dto.ts` produces values that match `adminUserResponseSchema` * exactly; if the schema or the DTO drifts, tests catch it at the * command boundary. * * `vid` is the optimistic-concurrency version — every write that touches * content content takes the client-held `vid` and the adapter gates the * write on it, throwing `ADMIN_USER_VERSION_CONFLICT` on mismatch. * * Password and uuid helpers come from `@byline/core/validation` — the * shared primitives ported from the organisation's `@infonomic/shared` * schemas so rules stay consistent across projects. */ // --------------------------------------------------------------------------- // Field-level schemas (re-used across requests) // --------------------------------------------------------------------------- const idSchema = uuidSchema const vidSchema = z .number({ message: 'vid is required' }) .int({ message: 'vid must be an integer' }) .positive({ message: 'vid must be positive' }) const emailSchema = z .email({ message: 'email must be a valid address' }) .min(3) .max(254) .transform((v) => v.toLowerCase()) const nameSchema = z.string().min(1).max(100) // BCP 47 locale codes — `en`, `pt-BR`, `zh-Hans-CN`, etc. The 16-char // ceiling matches the DB column width and is wider than any real-world // locale tag. const preferredLocaleSchema = z .string() .min(2) .max(16) .regex(/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, 'must be a BCP 47 locale tag') const orderSchema = z.enum([ 'given_name', 'family_name', 'email', 'username', 'created_at', 'updated_at', ]) // --------------------------------------------------------------------------- // Requests // --------------------------------------------------------------------------- export const listAdminUsersRequestSchema = z.object({ page: z.number().int().min(1).optional().default(1), pageSize: z.number().int().min(1).max(100).optional().default(20), query: z.string().max(128).optional(), order: orderSchema.optional().default('created_at'), desc: z.boolean().optional().default(true), }) export type ListAdminUsersRequest = z.infer export const getAdminUserRequestSchema = z.object({ id: idSchema, }) export type GetAdminUserRequest = z.infer export const createAdminUserRequestSchema = z.object({ email: emailSchema, password: passwordSchema, given_name: nameSchema.nullish(), family_name: nameSchema.nullish(), username: z.string().min(1).max(100).nullish(), is_super_admin: z.boolean().optional(), is_enabled: z.boolean().optional(), is_email_verified: z.boolean().optional(), preferred_locale: preferredLocaleSchema.nullish(), }) export type CreateAdminUserRequest = z.infer export const updateAdminUserRequestSchema = z.object({ id: idSchema, vid: vidSchema, patch: z .object({ email: emailSchema.optional(), given_name: nameSchema.nullish(), family_name: nameSchema.nullish(), username: z.string().min(1).max(100).nullish(), is_super_admin: z.boolean().optional(), is_enabled: z.boolean().optional(), is_email_verified: z.boolean().optional(), preferred_locale: preferredLocaleSchema.nullish(), }) .refine((p) => Object.keys(p).length > 0, { message: 'patch cannot be empty' }), }) export type UpdateAdminUserRequest = z.infer export const setAdminUserPasswordRequestSchema = z.object({ id: idSchema, vid: vidSchema, password: passwordSchema, }) export type SetAdminUserPasswordRequest = z.infer export const enableAdminUserRequestSchema = z.object({ id: idSchema }) export type EnableAdminUserRequest = z.infer export const disableAdminUserRequestSchema = z.object({ id: idSchema }) export type DisableAdminUserRequest = z.infer export const deleteAdminUserRequestSchema = z.object({ id: idSchema, vid: vidSchema, }) export type DeleteAdminUserRequest = z.infer // --------------------------------------------------------------------------- // Responses // --------------------------------------------------------------------------- /** * Public shape of an admin user. Deliberately excludes `password_hash` — * the DTO in `dto.ts` is responsible for producing exactly this shape * from an `AdminUserRow`, so the schema acts as a contract check. */ export const adminUserResponseSchema = z.object({ id: z.string(), vid: z.number().int(), email: z.string(), given_name: z.string().nullable(), family_name: z.string().nullable(), username: z.string().nullable(), remember_me: z.boolean(), last_login: z.date().nullable(), last_login_ip: z.string().nullable(), failed_login_attempts: z.number().int(), is_super_admin: z.boolean(), is_enabled: z.boolean(), is_email_verified: z.boolean(), preferred_locale: z.string().nullable(), created_at: z.date(), updated_at: z.date(), }) export type AdminUserResponse = z.infer export const adminUserListResponseSchema = z.object({ users: z.array(adminUserResponseSchema), meta: z.object({ total: z.number().int().min(0), total_pages: z.number().int().min(0), page: z.number().int().min(1), page_size: z.number().int().min(1), query: z.string(), order: orderSchema, desc: z.boolean(), }), }) export type AdminUserListResponse = z.infer /** Empty response for void-returning mutations (set-password, enable, disable, delete). */ export const okResponseSchema = z.object({ ok: z.literal(true) }) export type OkResponse = z.infer