import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, InternalServerErrorException, NotFoundException, Param, Patch, Post, UseGuards, } from '@nestjs/common'; import { eq, type Db, users as usersTable } from '@mosaicstack/db'; import type { Auth } from '@mosaicstack/auth'; import { AUTH } from '../auth/auth.tokens.js'; import { DB } from '../database/database.module.js'; import { AdminGuard } from './admin.guard.js'; import type { BanUserDto, CreateUserDto, UpdateUserRoleDto, UserDto, UserListDto, } from './admin.dto.js'; type UserRow = typeof usersTable.$inferSelect; function toUserDto(u: UserRow): UserDto { return { id: u.id, name: u.name, email: u.email, role: u.role, banned: u.banned ?? false, banReason: u.banReason ?? null, createdAt: u.createdAt.toISOString(), updatedAt: u.updatedAt.toISOString(), }; } async function requireUpdated( db: Db, id: string, update: Partial>, ): Promise { const [updated] = await db .update(usersTable) .set({ ...update, updatedAt: new Date() }) .where(eq(usersTable.id, id)) .returning(); if (!updated) throw new InternalServerErrorException('Update returned no rows'); return toUserDto(updated); } @Controller('api/admin/users') @UseGuards(AdminGuard) export class AdminController { constructor( @Inject(DB) private readonly db: Db, @Inject(AUTH) private readonly auth: Auth, ) {} @Get() async listUsers(): Promise { const rows = await this.db.select().from(usersTable).orderBy(usersTable.createdAt); const userList: UserDto[] = rows.map(toUserDto); return { users: userList, total: userList.length }; } @Get(':id') async getUser(@Param('id') id: string): Promise { const [user] = await this.db.select().from(usersTable).where(eq(usersTable.id, id)).limit(1); if (!user) throw new NotFoundException('User not found'); return toUserDto(user); } @Post() async createUser(@Body() body: CreateUserDto): Promise { // Use auth API to create user so password is properly hashed const authApi = this.auth.api as unknown as { createUser: (opts: { body: { name: string; email: string; password: string; role?: string }; }) => Promise<{ user: { id: string; name: string; email: string; createdAt: unknown; updatedAt: unknown }; }>; }; const result = await authApi.createUser({ body: { name: body.name, email: body.email, password: body.password, role: body.role ?? 'member', }, }); // Re-fetch from DB to get full row with our schema const [user] = await this.db .select() .from(usersTable) .where(eq(usersTable.id, result.user.id)) .limit(1); if (!user) throw new InternalServerErrorException('User created but not found in DB'); return toUserDto(user); } @Patch(':id/role') async setRole(@Param('id') id: string, @Body() body: UpdateUserRoleDto): Promise { await this.ensureExists(id); return requireUpdated(this.db, id, { role: body.role }); } @Post(':id/ban') @HttpCode(HttpStatus.OK) async banUser(@Param('id') id: string, @Body() body: BanUserDto): Promise { await this.ensureExists(id); return requireUpdated(this.db, id, { banned: true, banReason: body.reason ?? null }); } @Post(':id/unban') @HttpCode(HttpStatus.OK) async unbanUser(@Param('id') id: string): Promise { await this.ensureExists(id); return requireUpdated(this.db, id, { banned: false, banReason: null, banExpires: null }); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async deleteUser(@Param('id') id: string): Promise { await this.ensureExists(id); await this.db.delete(usersTable).where(eq(usersTable.id, id)); } private async ensureExists(id: string): Promise { const [existing] = await this.db .select({ id: usersTable.id }) .from(usersTable) .where(eq(usersTable.id, id)) .limit(1); if (!existing) throw new NotFoundException('User not found'); } }