import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Req, UseGuards, } from '@nestjs/common' 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 { OperatorGuard } from '../auth/operator.guard.js' import type { AuthentikJwtPayload } from '../auth/jwt-payload.interface.js' import type { AuditActor } from '../audit/audit.service.js' import { InvitePartnerUserDto } from '../users/dto/invite-partner-user.dto.js' import { UsersService } from '../users/users.service.js' import { CreatePartnerDto } from './dto/create-partner.dto.js' import { UpdatePartnerDto } from './dto/update-partner.dto.js' import { PartnersService } from './partners.service.js' function auditActor( user: { _id: unknown; email: string }, req: Parameters[0], ): AuditActor { return { userId: String(user._id), email: user.email, ip: clientIp(req), } } // Partners are operator-managed only. Every endpoint requires an // operator-scoped token (aud === 'dezky-operator') plus platformAdmin on the // resolved user. A self-serve partner portal (partner.dezky.local) is a // future surface and will hit different endpoints scoped to "this partner's // own customers" rather than the full set. @Controller('partners') @UseGuards(JwtAuthGuard, OperatorGuard) export class PartnersController { constructor( private readonly partners: PartnersService, private readonly users: UsersService, private readonly actorService: ActorService, ) {} @Post() async create( @Body() dto: CreatePartnerDto, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters[0], ) { const user = await this.actorService.resolve(jwt) return this.partners.create(dto, auditActor(user, req)) } @Get() async findAll() { const rows = await this.partners.findAllWithStats() return rows.map((r) => ({ ...r.partner.toObject(), customers: r.customers })) } @Get(':slug') async findOne(@Param('slug') slug: string) { const row = await this.partners.findOneWithStats(slug) return { ...row.partner.toObject(), customers: row.customers } } @Get(':slug/tenants') listTenants(@Param('slug') slug: string) { return this.partners.listTenants(slug) } @Patch(':slug') async update( @Param('slug') slug: string, @Body() dto: UpdatePartnerDto, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters[0], ) { const user = await this.actorService.resolve(jwt) return this.partners.update(slug, dto, auditActor(user, req)) } @Delete(':slug') @HttpCode(204) async terminate( @Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters[0], ) { const user = await this.actorService.resolve(jwt) await this.partners.terminate(slug, auditActor(user, req)) } // Partner-staff team listing. Returns the User docs whose partnerId matches // this partner. The /partners/:slug page's Team section calls this on load. @Get(':slug/users') async listUsers(@Param('slug') slug: string) { const partner = await this.partners.findOneBySlug(slug) return this.users.listPartnerUsers(partner._id) } // Invite a new partner-staff user. Resolves slug → partner, delegates to // UsersService.invitePartnerUser which handles Authentik user creation, // group assignment, local User pre-create, and audit recording. @Post(':slug/users') async inviteUser( @Param('slug') slug: string, @Body() dto: InvitePartnerUserDto, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters[0], ) { const actor = await this.actorService.resolve(jwt) const partner = await this.partners.findOneBySlug(slug) return this.users.invitePartnerUser( dto, { _id: partner._id, slug: partner.slug }, auditActor(actor, req), ) } }