import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { v4 as uuid } from 'uuid'; import * as argon2 from 'argon2'; import { UserEntity, UserRole } from '../../database/entities/user.entity'; import { UsersService } from '../users/users.service'; import { ApiError } from '../../common/errors/api-error'; import { ERROR_CODES } from '../../common/errors/error-codes'; import { validatePassword } from '../../common/utils/password-policy'; import { ConfigService } from '@nestjs/config'; export interface ListUsersQuery { limit?: number; cursor?: string; role?: UserRole; } @Injectable() export class AdminService { constructor( private readonly usersService: UsersService, private readonly dataSource: DataSource, private readonly config: ConfigService, ) {} async listUsers(query: ListUsersQuery = {}): Promise { const limit = Math.min(Math.max(query.limit ?? 50, 1), 200); const repo = this.dataSource.getRepository(UserEntity); const qb = repo .createQueryBuilder('u') .orderBy('u.createdAt', 'ASC') .addOrderBy('u.id', 'ASC') .limit(limit + 1); if (query.role) qb.andWhere('u.role = :role', { role: query.role }); if (query.cursor) qb.andWhere('u.id > :cursor', { cursor: query.cursor }); const rows = await qb.getMany(); return rows.slice(0, limit); } async updateUserRole(targetId: string, role: UserRole): Promise { if (role !== 'admin' && role !== 'player') { throw ApiError.validation('Invalid role'); } return this.dataSource.transaction(async (manager) => { await this.usersService.enforceLastAdminInvariant(manager, targetId, role); const user = await manager.findOne(UserEntity, { where: { id: targetId } }); if (!user) throw ApiError.notFound('User not found'); user.role = role; await manager.save(user); return user; }); } async deleteUser(targetId: string): Promise { return this.dataSource.transaction(async (manager) => { await this.usersService.enforceLastAdminInvariant(manager, targetId); const res = await manager.delete(UserEntity, { id: targetId }); if (!res.affected) throw ApiError.notFound('User not found'); }); } async createUser(username: string, password: string, role: UserRole): Promise { validatePassword(password, this.config); return this.dataSource.transaction(async (manager) => { const existing = await manager.findOne(UserEntity, { where: { username } }); if (existing) throw ApiError.conflict(ERROR_CODES.CONFLICT, 'Username already taken'); const hash = await argon2.hash(password, { type: argon2.argon2id }); const user = manager.create(UserEntity, { id: uuid(), username, passwordHash: hash, role, status: 'enabled', }); await manager.save(user); return user; }); } }