Files
photo-live/services/api/src/admin/admin-users.dto.ts
T
浅念 f4a1ba7a14 feat(api): admin account governance endpoints
P4a Task 6: GET /admin/users 双模式收口(无 accountPrefix 进入管理列表
模式:全量用户、query/status 过滤、createdAt+ID 倒序键集游标、仅 admin;
前缀模式 P1b 语义保留)+ PATCH / reset-password / activate / deactivate
治理端点(sourceStatus 只读拒绝、自我保护、会话撤销、审计、生成初始
密码)。附带修复 full-chain e2e 的 job-done 断言竞态(轮询条件补齐
photo_jobs 终态)。api e2e 214 → 227 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 19:31:00 +08:00

66 lines
2.3 KiB
TypeScript

// services/api/src/admin/admin-users.dto.ts —— 账号治理端点入参 DTO(P4a
// Task 6)。契约 AdminUserUpdateRequest / AdminPasswordResetRequest。
// 校验语义要点:
// · 可选字段一律 @ValidateIf((_, value) => value !== undefined) 而非
// @IsOptional:后者连 null 一并跳过,显式 null 会以 null 值渗入服务层
// (Prisma 拒绝、错误形态不契约化);ValidateIf 只放行 undefined(真缺省),
// null 落到类型校验上 → 400 VALIDATION_FAILED,与「提供了非法值」同出口。
// · sourceStatus 只读(技术方案 3.1A 红线:上游状态仅由用户同步任务维护,
// 本地不越权改写):DTO 显式声明该字段 + 只读约束,出现(含 null)即
// 400。全局 ValidationPipe 是 whitelist(剥离而非拒绝)——若不声明,
// sourceStatus 会被静默剥离成「忽略」,与只读拒绝语义不符。
import {
IsBoolean,
IsIn,
IsString,
MaxLength,
MinLength,
Validate,
ValidateIf,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
/** 只读字段约束:仅 undefined(真缺省)放行,任何显式值(含 null)拒绝。 */
@ValidatorConstraint({ name: 'adminUserReadOnlyField', async: false })
class ReadOnlyFieldConstraint implements ValidatorConstraintInterface {
validate(value: unknown): boolean {
return value === undefined;
}
defaultMessage(args: ValidationArguments): string {
return `${args.property} 为只读字段,不能经本端点修改`;
}
}
export class AdminUserUpdateDto {
@ValidateIf((_, value) => value !== undefined)
@IsIn(['staff', 'admin'])
role?: 'staff' | 'admin';
@ValidateIf((_, value) => value !== undefined)
@IsBoolean()
localLocked?: boolean;
@ValidateIf((_, value) => value !== undefined)
@IsString()
@MinLength(1)
@MaxLength(128)
displayName?: string;
/** sourceStatus 不在可更新集(契约 AdminUserUpdateRequest 无此字段)。 */
@ValidateIf(() => true)
@Validate(ReadOnlyFieldConstraint)
declare sourceStatus?: unknown;
}
export class AdminPasswordResetDto {
/** 契约 bounds:8-128;缺省由服务端生成随机初始密码。 */
@ValidateIf((_, value) => value !== undefined)
@IsString()
@MinLength(8)
@MaxLength(128)
newPassword?: string;
}