import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, Param, Patch, Post, Req, UseGuards, } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { ActorService } from '../auth/actor.service.js' import { clientIp } from '../auth/client-ip.js' import { CurrentUser } from '../auth/current-user.decorator.js' import { JwtAuthGuard } from '../auth/jwt-auth.guard.js' import type { AuthentikJwtPayload } from '../auth/jwt-payload.interface.js' import { CreateUserDto } from './dto/create-user.dto.js' import { UpdateUserDto } from './dto/update-user.dto.js' import { UsersService } from './users.service.js' // Authentik group name that grants platform-wide admin in Dezky. This is the ONLY // place we look at the JWT's groups claim outside of /users/me — and even here it's // just for the bootstrap: the resulting platformAdmin boolean on the User doc is // what every other endpoint reads. const ADMIN_BOOTSTRAP_GROUP_DEFAULT = 'dezky-platform-admins' @Controller('users') @UseGuards(JwtAuthGuard) export class UsersController { private readonly adminBootstrapGroup: string constructor( private readonly users: UsersService, private readonly actor: ActorService, config: ConfigService, ) { this.adminBootstrapGroup = config.get('PLATFORM_ADMIN_BOOTSTRAP_GROUP') ?? ADMIN_BOOTSTRAP_GROUP_DEFAULT } // The signed-in user's own profile — bootstraps the user record on first call, // and syncs name/email/tenants/platformAdmin from the JWT on every subsequent call. @Get('me') async me(@CurrentUser() jwt: AuthentikJwtPayload) { return this.users.upsertFromAuthentik({ subject: jwt.sub, email: jwt.email ?? jwt.preferred_username ?? jwt.sub, name: jwt.name ?? jwt.preferred_username ?? jwt.email ?? jwt.sub, tenantSlugs: jwt.groups ?? [], platformAdmin: jwt.groups?.includes(this.adminBootstrapGroup) ?? false, }) } @Post() async create(@Body() dto: CreateUserDto, @CurrentUser() jwt: AuthentikJwtPayload) { const actor = await this.actor.resolve(jwt) if (!actor.platformAdmin) { throw new ForbiddenException('Only platform admins can create users directly') } return this.users.create(dto) } @Get() async findAll(@CurrentUser() jwt: AuthentikJwtPayload) { const actor = await this.actor.resolve(jwt) if (actor.platformAdmin) return this.users.findAll() return this.users.findAllForTenants(actor.tenantIds) } @Get(':subject') async findOne(@Param('subject') subject: string, @CurrentUser() jwt: AuthentikJwtPayload) { const actor = await this.actor.resolve(jwt) if (subject !== jwt.sub && !actor.platformAdmin) { throw new ForbiddenException('Cannot read other users') } return this.users.findOneBySubject(subject) } @Patch(':subject') async update( @Param('subject') subject: string, @Body() dto: UpdateUserDto, @CurrentUser() jwt: AuthentikJwtPayload, ) { const actor = await this.actor.resolve(jwt) if (!actor.platformAdmin) { throw new ForbiddenException('Only platform admins can update users') } return this.users.update(subject, dto) } @Delete(':subject') @HttpCode(204) async deactivate( @Param('subject') subject: string, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters[0], ) { const actor = await this.actor.resolve(jwt) if (!actor.platformAdmin) { throw new ForbiddenException('Only platform admins can deactivate users') } await this.users.deactivate(subject, { userId: String(actor._id), email: actor.email, ip: clientIp(req), }) } }