import { Controller, Get, Post, Put, Body, Param, UseGuards, Req, ForbiddenException, BadRequestException } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { UsersService } from '../users/users.service'; import { OrganizationsService } from '../organizations/organizations.service'; import * as bcrypt from 'bcryptjs'; @ApiTags('admin') @Controller('admin') @ApiBearerAuth() @UseGuards(JwtAuthGuard) export class AdminController { constructor( private usersService: UsersService, private orgService: OrganizationsService, ) {} private async requireSuperadmin(req: any) { const user = await this.usersService.findById(req.user.userId || req.user.sub); if (!user?.isSuperadmin) { throw new ForbiddenException('SuperUser Admin access required'); } } @Get('users') async listUsers(@Req() req: any) { await this.requireSuperadmin(req); const users = await this.usersService.findAllUsers(); return users.map(u => ({ id: u.id, email: u.email, firstName: u.firstName, lastName: u.lastName, isSuperadmin: u.isSuperadmin, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt, organizations: u.userOrganizations?.map(uo => ({ id: uo.organizationId, name: uo.organization?.name, role: uo.role, })) || [], })); } @Get('organizations') async listOrganizations(@Req() req: any) { await this.requireSuperadmin(req); return this.usersService.findAllOrganizations(); } @Post('users/:id/superadmin') async toggleSuperadmin(@Req() req: any, @Param('id') id: string, @Body() body: { isSuperadmin: boolean }) { await this.requireSuperadmin(req); await this.usersService.setSuperadmin(id, body.isSuperadmin); return { success: true }; } @Post('tenants') async createTenant(@Req() req: any, @Body() body: { orgName: string; email?: string; phone?: string; addressLine1?: string; city?: string; state?: string; zipCode?: string; contractNumber?: string; planLevel?: string; fiscalYearStartMonth?: number; adminEmail: string; adminPassword: string; adminFirstName: string; adminLastName: string; }) { await this.requireSuperadmin(req); if (!body.orgName || !body.adminEmail || !body.adminPassword) { throw new BadRequestException('Organization name, admin email and password are required'); } // Check if admin email already exists const existingUser = await this.usersService.findByEmail(body.adminEmail); let userId: string; if (existingUser) { userId = existingUser.id; } else { // Create the first user for this tenant const passwordHash = await bcrypt.hash(body.adminPassword, 12); const newUser = await this.usersService.create({ email: body.adminEmail, passwordHash, firstName: body.adminFirstName, lastName: body.adminLastName, }); userId = newUser.id; } // Create the organization + tenant schema + membership const org = await this.orgService.create({ name: body.orgName, email: body.email, phone: body.phone, addressLine1: body.addressLine1, city: body.city, state: body.state, zipCode: body.zipCode, contractNumber: body.contractNumber, planLevel: body.planLevel || 'standard', fiscalYearStartMonth: body.fiscalYearStartMonth || 1, }, userId); return { success: true, organization: org }; } @Put('organizations/:id/status') async updateOrgStatus( @Req() req: any, @Param('id') id: string, @Body() body: { status: string }, ) { await this.requireSuperadmin(req); const validStatuses = ['active', 'suspended', 'trial', 'archived']; if (!validStatuses.includes(body.status)) { throw new BadRequestException(`Invalid status. Must be one of: ${validStatuses.join(', ')}`); } const org = await this.orgService.updateStatus(id, body.status); return { success: true, organization: org }; } }