AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateFirstAdminDtoSchema = z.object({
|
||||
username: z.string().min(3).max(64),
|
||||
password: z.string().min(1).max(256),
|
||||
});
|
||||
export type CreateFirstAdminDto = z.infer<typeof CreateFirstAdminDtoSchema>;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Post, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe';
|
||||
import { Public } from '../../common/decorators/public.decorator';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { CreateFirstAdminDtoSchema } from './dto/create-first-admin.dto';
|
||||
import { setRefreshCookie } from '../auth/cookie';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('api/v1/auth')
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('register-first-admin')
|
||||
@ApiOperation({ summary: 'Create the very first admin (only allowed when no admins exist)' })
|
||||
async registerFirstAdmin(
|
||||
@Body(new ZodValidationPipe(CreateFirstAdminDtoSchema)) body: { username: string; password: string },
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
const ip = (req.ip || req.socket.remoteAddress || 'unknown') as string;
|
||||
const session = await this.auth.registerFirstAdmin(body.username, body.password, ip);
|
||||
setRefreshCookie(res, session.refreshToken, this.config);
|
||||
return {
|
||||
accessToken: session.accessToken,
|
||||
expiresIn: session.expiresIn,
|
||||
user: session.user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserEntity } from '../../database/entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserEntity]), AuthModule],
|
||||
providers: [UsersService],
|
||||
controllers: [UsersController],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user