Files
HIPCTF2/backend/src/modules/admin/admin.service.ts
T

79 lines
2.9 KiB
TypeScript

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<UserEntity[]> {
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<UserEntity> {
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<void> {
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<UserEntity> {
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;
});
}
}