import { Injectable, Logger } from '@nestjs/common'; import { Knex, knex } from 'knex'; import { getCentralPrisma } from '../prisma/central-prisma.service'; import * as crypto from 'crypto'; @Injectable() export class TenantDatabaseService { private readonly logger = new Logger(TenantDatabaseService.name); private tenantConnections: Map = new Map(); async getTenantKnex(tenantIdOrSlug: string): Promise { if (this.tenantConnections.has(tenantIdOrSlug)) { return this.tenantConnections.get(tenantIdOrSlug); } const centralPrisma = getCentralPrisma(); // Try to find tenant by ID first, then by slug let tenant = await centralPrisma.tenant.findUnique({ where: { id: tenantIdOrSlug }, }); if (!tenant) { tenant = await centralPrisma.tenant.findUnique({ where: { slug: tenantIdOrSlug }, }); } if (!tenant) { throw new Error(`Tenant ${tenantIdOrSlug} not found`); } if (tenant.status !== 'active') { throw new Error(`Tenant ${tenantIdOrSlug} is not active`); } // Decrypt password const decryptedPassword = this.decryptPassword(tenant.dbPassword); const tenantKnex = knex({ client: 'mysql2', connection: { host: tenant.dbHost, port: tenant.dbPort, user: tenant.dbUsername, password: decryptedPassword, database: tenant.dbName, }, pool: { min: 2, max: 10, }, }); // Test connection try { await tenantKnex.raw('SELECT 1'); this.logger.log(`Connected to tenant database: ${tenant.dbName}`); } catch (error) { this.logger.error( `Failed to connect to tenant database: ${tenant.dbName}`, error, ); throw error; } this.tenantConnections.set(tenantIdOrSlug, tenantKnex); return tenantKnex; } async getTenantByDomain(domain: string): Promise { const centralPrisma = getCentralPrisma(); const domainRecord = await centralPrisma.domain.findUnique({ where: { domain }, include: { tenant: true }, }); if (!domainRecord) { throw new Error(`Domain ${domain} not found`); } if (domainRecord.tenant.status !== 'active') { throw new Error(`Tenant for domain ${domain} is not active`); } return domainRecord.tenant; } async disconnectTenant(tenantId: string) { const connection = this.tenantConnections.get(tenantId); if (connection) { await connection.destroy(); this.tenantConnections.delete(tenantId); this.logger.log(`Disconnected tenant: ${tenantId}`); } } removeTenantConnection(tenantId: string) { this.tenantConnections.delete(tenantId); this.logger.log(`Removed tenant connection from cache: ${tenantId}`); } async disconnectAll() { for (const [tenantId, connection] of this.tenantConnections.entries()) { await connection.destroy(); } this.tenantConnections.clear(); this.logger.log('Disconnected all tenant connections'); } encryptPassword(password: string): string { const algorithm = 'aes-256-cbc'; const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(password, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } private decryptPassword(encryptedPassword: string): string { const algorithm = 'aes-256-cbc'; const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); const parts = encryptedPassword.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipheriv(algorithm, key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } }