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

49 lines
1.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { UserEntity, UserRole } from '../../database/entities/user.entity';
import { ApiError } from '../../common/errors/api-error';
import { ERROR_CODES } from '../../common/errors/error-codes';
type UserRepoLike = Repository<UserEntity> | EntityManager;
@Injectable()
export class UsersService {
constructor(@InjectRepository(UserEntity) private readonly repo: Repository<UserEntity>) {}
countAdmins(manager?: EntityManager): Promise<number> {
return this.getRepo(manager).count({ where: { role: 'admin' } });
}
findById(id: string, manager?: EntityManager): Promise<UserEntity | null> {
return this.getRepo(manager).findOne({ where: { id } });
}
listAll(): Promise<UserEntity[]> {
return this.repo.find({ order: { createdAt: 'ASC' } });
}
/**
* Verify, inside an open transaction, that demoting/deleting the given user
* would not leave the system with zero admins. Throws ApiError(LAST_ADMIN)
* if so. Must be invoked from a `dataSource.transaction(...)` callback.
*/
async enforceLastAdminInvariant(manager: EntityManager, targetId: string, nextRole?: UserRole): Promise<void> {
const repo = manager.getRepository(UserEntity);
const target = await repo.findOne({ where: { id: targetId } });
if (!target) throw ApiError.notFound('User not found');
const wouldDemoteOrDelete =
target.role === 'admin' && (nextRole === undefined || nextRole !== 'admin');
if (!wouldDemoteOrDelete) return;
const adminCount = await repo.count({ where: { role: 'admin' } });
if (adminCount <= 1) {
throw ApiError.conflict(ERROR_CODES.LAST_ADMIN, 'Cannot delete or demote the last admin');
}
}
private getRepo(manager?: EntityManager): Repository<UserEntity> {
return manager ? manager.getRepository(UserEntity) : this.repo;
}
}