import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, UnauthorizedException, Req, } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CentralTenant, CentralDomain, CentralUser } from '../models/central.model'; import { getCentralKnex, initCentralModels } from './central-database.service'; import { TenantProvisioningService } from './tenant-provisioning.service'; import { TenantDatabaseService } from './tenant-database.service'; import * as bcrypt from 'bcrypt'; /** * Controller for managing central database entities (tenants, domains, users) * Only accessible when logged in as central admin */ @Controller('central') @UseGuards(JwtAuthGuard) export class CentralAdminController { constructor( private readonly provisioningService: TenantProvisioningService, private readonly tenantDbService: TenantDatabaseService, ) { // Initialize central models on controller creation initCentralModels(); } private checkCentralAdmin(req: any) { const subdomain = req.raw?.subdomain; const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(','); if (!subdomain || !centralSubdomains.includes(subdomain)) { throw new UnauthorizedException('This endpoint is only accessible to central administrators'); } } // ==================== TENANTS ==================== @Get('tenants') async getTenants(@Req() req: any) { this.checkCentralAdmin(req); return CentralTenant.query().withGraphFetched('domains'); } @Get('tenants/:id') async getTenant(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); return CentralTenant.query() .findById(id) .withGraphFetched('domains'); } @Post('tenants') async createTenant( @Req() req: any, @Body() data: { name: string; slug?: string; primaryDomain: string; dbHost?: string; dbPort?: number; }, ) { this.checkCentralAdmin(req); // Use the provisioning service to create tenant with database and migrations const result = await this.provisioningService.provisionTenant({ name: data.name, slug: data.slug || data.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''), primaryDomain: data.primaryDomain, dbHost: data.dbHost, dbPort: data.dbPort, }); // Return the created tenant return CentralTenant.query() .findById(result.tenantId) .withGraphFetched('domains'); } @Put('tenants/:id') async updateTenant( @Req() req: any, @Param('id') id: string, @Body() data: { name?: string; slug?: string; dbHost?: string; dbPort?: number; dbName?: string; dbUsername?: string; status?: string; }, ) { this.checkCentralAdmin(req); return CentralTenant.query() .patchAndFetchById(id, data); } @Delete('tenants/:id') async deleteTenant(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); await CentralTenant.query().deleteById(id); return { success: true }; } // Get users for a specific tenant @Get('tenants/:id/users') async getTenantUsers(@Req() req: any, @Param('id') tenantId: string) { this.checkCentralAdmin(req); try { // Get tenant to verify it exists const tenant = await CentralTenant.query().findById(tenantId); if (!tenant) { throw new UnauthorizedException('Tenant not found'); } // Connect to tenant database using tenant ID directly const tenantKnex = await this.tenantDbService.getTenantKnexById(tenantId); // Fetch users from tenant database const users = await tenantKnex('users').select('*'); // Remove password from response return users.map(({ password, ...user }) => user); } catch (error) { console.error('Error fetching tenant users:', error); throw error; } } // Create a user in a specific tenant @Post('tenants/:id/users') async createTenantUser( @Req() req: any, @Param('id') tenantId: string, @Body() data: { email: string; password: string; firstName?: string; lastName?: string; }, ) { this.checkCentralAdmin(req); try { // Get tenant to verify it exists const tenant = await CentralTenant.query().findById(tenantId); if (!tenant) { throw new UnauthorizedException('Tenant not found'); } // Connect to tenant database using tenant ID directly const tenantKnex = await this.tenantDbService.getTenantKnexById(tenantId); // Hash password const hashedPassword = await bcrypt.hash(data.password, 10); // Generate UUID for the new user const userId = require('crypto').randomUUID(); // Create user in tenant database await tenantKnex('users').insert({ id: userId, email: data.email, password: hashedPassword, firstName: data.firstName || null, lastName: data.lastName || null, created_at: new Date(), updated_at: new Date(), }); // Fetch and return the created user const user = await tenantKnex('users').where('id', userId).first(); if (!user) { throw new Error('Failed to create user'); } const { password, ...userWithoutPassword } = user; return userWithoutPassword; } catch (error) { console.error('Error creating tenant user:', error); throw error; } } // ==================== DOMAINS ==================== @Get('domains') async getDomains( @Req() req: any, @Query('parentId') parentId?: string, @Query('tenantId') tenantId?: string, ) { this.checkCentralAdmin(req); let query = CentralDomain.query().withGraphFetched('tenant'); // Filter by parent/tenant ID if provided (for related lists) if (parentId || tenantId) { query = query.where('tenantId', parentId || tenantId); } return query; } @Get('domains/:id') async getDomain(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); return CentralDomain.query() .findById(id) .withGraphFetched('tenant'); } @Post('domains') async createDomain( @Req() req: any, @Body() data: { domain: string; tenantId: string; isPrimary?: boolean; }, ) { this.checkCentralAdmin(req); return CentralDomain.query().insert({ domain: data.domain, tenantId: data.tenantId, isPrimary: data.isPrimary || false, }); } @Put('domains/:id') async updateDomain( @Req() req: any, @Param('id') id: string, @Body() data: { domain?: string; tenantId?: string; isPrimary?: boolean; }, ) { this.checkCentralAdmin(req); return CentralDomain.query() .patchAndFetchById(id, data); } @Delete('domains/:id') async deleteDomain(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); // Get domain info before deleting to invalidate cache const domain = await CentralDomain.query().findById(id); // Delete the domain await CentralDomain.query().deleteById(id); // Invalidate tenant connection cache for this domain if (domain) { this.tenantDbService.removeTenantConnection(domain.domain); } return { success: true }; } // ==================== USERS (Central Admin Users) ==================== @Get('users') async getUsers(@Req() req: any) { this.checkCentralAdmin(req); const users = await CentralUser.query(); // Remove password from response return users.map(({ password, ...user }) => user); } @Get('users/:id') async getUser(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); const user = await CentralUser.query().findById(id); if (!user) { throw new UnauthorizedException('User not found'); } const { password, ...userWithoutPassword } = user; return userWithoutPassword; } @Post('users') async createUser( @Req() req: any, @Body() data: { email: string; password: string; firstName?: string; lastName?: string; role?: string; isActive?: boolean; }, ) { this.checkCentralAdmin(req); const hashedPassword = await bcrypt.hash(data.password, 10); const user = await CentralUser.query().insert({ email: data.email, password: hashedPassword, firstName: data.firstName || null, lastName: data.lastName || null, role: data.role || 'admin', isActive: data.isActive !== undefined ? data.isActive : true, }); const { password, ...userWithoutPassword } = user; return userWithoutPassword; } @Put('users/:id') async updateUser( @Req() req: any, @Param('id') id: string, @Body() data: { email?: string; password?: string; firstName?: string; lastName?: string; role?: string; isActive?: boolean; }, ) { this.checkCentralAdmin(req); const updateData: any = { ...data }; // Hash password if provided if (data.password) { updateData.password = await bcrypt.hash(data.password, 10); } else { // Remove password from update if not provided delete updateData.password; } const user = await CentralUser.query() .patchAndFetchById(id, updateData); const { password, ...userWithoutPassword } = user; return userWithoutPassword; } @Delete('users/:id') async deleteUser(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); await CentralUser.query().deleteById(id); return { success: true }; } }