133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
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<string, Knex> = new Map();
|
|
|
|
async getTenantKnex(tenantIdOrSlug: string): Promise<Knex> {
|
|
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<any> {
|
|
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;
|
|
}
|
|
}
|