369 lines
9.7 KiB
TypeScript
369 lines
9.7 KiB
TypeScript
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 };
|
|
}
|
|
}
|