WIP - manage tenant users from central

This commit is contained in:
Francisco Gaona
2025-12-24 12:17:22 +01:00
parent b9fa3bd008
commit 52c0849de2
7 changed files with 773 additions and 60 deletions

View File

@@ -112,6 +112,91 @@ export class CentralAdminController {
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')

View File

@@ -8,83 +8,116 @@ export class TenantDatabaseService {
private readonly logger = new Logger(TenantDatabaseService.name);
private tenantConnections: Map<string, Knex> = new Map();
async getTenantKnex(tenantIdOrSlug: string): Promise<Knex> {
/**
* Get tenant database connection by domain (for subdomain-based authentication)
* This is used when users log in via tenant subdomains
*/
async getTenantKnexByDomain(domain: string): Promise<Knex> {
const cacheKey = `domain:${domain}`;
// Check if we have a cached connection
if (this.tenantConnections.has(tenantIdOrSlug)) {
// For domain-based lookups, validate the domain still exists before returning cached connection
if (this.tenantConnections.has(cacheKey)) {
// Validate the domain still exists before returning cached connection
const centralPrisma = getCentralPrisma();
// Check if this looks like a domain (not a UUID)
const isDomain = !tenantIdOrSlug.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
if (isDomain) {
try {
const domainRecord = await centralPrisma.domain.findUnique({
where: { domain: tenantIdOrSlug },
});
// If domain no longer exists, remove cached connection and continue to error
if (!domainRecord) {
this.logger.warn(`Domain ${tenantIdOrSlug} no longer exists, removing cached connection`);
await this.disconnectTenant(tenantIdOrSlug);
throw new Error(`Domain ${tenantIdOrSlug} not found`);
}
} catch (error) {
// If domain doesn't exist, remove from cache and re-throw
if (error.message.includes('not found')) {
throw error;
}
// For other errors, log but continue with cached connection
this.logger.warn(`Error validating domain ${tenantIdOrSlug}:`, error.message);
try {
const domainRecord = await centralPrisma.domain.findUnique({
where: { domain },
});
// If domain no longer exists, remove cached connection
if (!domainRecord) {
this.logger.warn(`Domain ${domain} no longer exists, removing cached connection`);
await this.disconnectTenant(cacheKey);
throw new Error(`Domain ${domain} not found`);
}
} catch (error) {
// If domain doesn't exist, remove from cache and re-throw
if (error.message.includes('not found')) {
throw error;
}
// For other errors, log but continue with cached connection
this.logger.warn(`Error validating domain ${domain}:`, error.message);
}
return this.tenantConnections.get(tenantIdOrSlug);
return this.tenantConnections.get(cacheKey);
}
const centralPrisma = getCentralPrisma();
let tenant = null;
// First, try to find by domain (most common case - subdomain lookup)
try {
const domainRecord = await centralPrisma.domain.findUnique({
where: { domain: tenantIdOrSlug },
include: { tenant: true },
});
console.log('here:' + JSON.stringify(domainRecord));
// Find tenant by domain
const domainRecord = await centralPrisma.domain.findUnique({
where: { domain },
include: { tenant: true },
});
if (domainRecord) {
tenant = domainRecord.tenant;
this.logger.log(`Found tenant by domain: ${tenantIdOrSlug} -> ${tenant.name}`);
}
} catch (error) {
this.logger.debug(`No domain found for: ${tenantIdOrSlug}, trying ID/slug lookup`);
}
// Fallback: Try to find tenant by ID
if (!tenant) {
tenant = await centralPrisma.tenant.findUnique({
where: { id: tenantIdOrSlug },
});
}
// Fallback: Try to find by slug
if (!tenant) {
tenant = await centralPrisma.tenant.findUnique({
where: { slug: tenantIdOrSlug },
});
if (!domainRecord) {
throw new Error(`Domain ${domain} not found`);
}
const tenant = domainRecord.tenant;
this.logger.log(`Found tenant by domain: ${domain} -> ${tenant.name}`);
if (tenant.status !== 'active') {
throw new Error(`Tenant ${tenant.name} is not active`);
}
// Create connection and cache it
const tenantKnex = await this.createTenantConnection(tenant);
this.tenantConnections.set(cacheKey, tenantKnex);
return tenantKnex;
}
/**
* Get tenant database connection by tenant ID (for central admin operations)
* This is used when central admin needs to access tenant databases
*/
async getTenantKnexById(tenantId: string): Promise<Knex> {
const cacheKey = `id:${tenantId}`;
// Check if we have a cached connection (no validation needed for ID-based lookups)
if (this.tenantConnections.has(cacheKey)) {
return this.tenantConnections.get(cacheKey);
}
const centralPrisma = getCentralPrisma();
// Find tenant by ID
const tenant = await centralPrisma.tenant.findUnique({
where: { id: tenantId },
});
if (!tenant) {
throw new Error(`Tenant ${tenantIdOrSlug} not found`);
throw new Error(`Tenant ${tenantId} not found`);
}
if (tenant.status !== 'active') {
throw new Error(`Tenant ${tenantIdOrSlug} is not active`);
throw new Error(`Tenant ${tenant.name} is not active`);
}
this.logger.log(`Connecting to tenant database by ID: ${tenant.name}`);
// Create connection and cache it
const tenantKnex = await this.createTenantConnection(tenant);
this.tenantConnections.set(cacheKey, tenantKnex);
return tenantKnex;
}
/**
* Legacy method - delegates to domain-based lookup
* @deprecated Use getTenantKnexByDomain or getTenantKnexById instead
*/
async getTenantKnex(tenantIdOrSlug: string): Promise<Knex> {
// Assume it's a domain if it contains a dot
return this.getTenantKnexByDomain(tenantIdOrSlug);
}
/**
* Create a new Knex connection to a tenant database
*/
private async createTenantConnection(tenant: any): Promise<Knex> {
// Decrypt password
const decryptedPassword = this.decryptPassword(tenant.dbPassword);
@@ -115,7 +148,6 @@ export class TenantDatabaseService {
throw error;
}
this.tenantConnections.set(tenantIdOrSlug, tenantKnex);
return tenantKnex;
}