Added auth functionality, initial work with views and field types
This commit is contained in:
132
backend/src/tenant/tenant-database.service.ts
Normal file
132
backend/src/tenant/tenant-database.service.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
36
backend/src/tenant/tenant-provisioning.controller.ts
Normal file
36
backend/src/tenant/tenant-provisioning.controller.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { TenantProvisioningService } from './tenant-provisioning.service';
|
||||
|
||||
@Controller('setup/tenants')
|
||||
export class TenantProvisioningController {
|
||||
constructor(
|
||||
private readonly provisioningService: TenantProvisioningService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async createTenant(
|
||||
@Body()
|
||||
data: {
|
||||
name: string;
|
||||
slug: string;
|
||||
primaryDomain: string;
|
||||
dbHost?: string;
|
||||
dbPort?: number;
|
||||
},
|
||||
) {
|
||||
return this.provisioningService.provisionTenant(data);
|
||||
}
|
||||
|
||||
@Delete(':tenantId')
|
||||
async deleteTenant(@Param('tenantId') tenantId: string) {
|
||||
await this.provisioningService.deprovisionTenant(tenantId);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
344
backend/src/tenant/tenant-provisioning.service.ts
Normal file
344
backend/src/tenant/tenant-provisioning.service.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { TenantDatabaseService } from './tenant-database.service';
|
||||
import * as knex from 'knex';
|
||||
import * as crypto from 'crypto';
|
||||
import { getCentralPrisma } from '../prisma/central-prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class TenantProvisioningService {
|
||||
private readonly logger = new Logger(TenantProvisioningService.name);
|
||||
|
||||
constructor(private readonly tenantDbService: TenantDatabaseService) {}
|
||||
|
||||
/**
|
||||
* Provision a new tenant with database and default data
|
||||
*/
|
||||
async provisionTenant(data: {
|
||||
name: string;
|
||||
slug: string;
|
||||
primaryDomain: string;
|
||||
dbHost?: string;
|
||||
dbPort?: number;
|
||||
}) {
|
||||
const dbHost = data.dbHost || process.env.DB_HOST || 'platform-db';
|
||||
const dbPort = data.dbPort || parseInt(process.env.DB_PORT || '3306');
|
||||
const dbName = `tenant_${data.slug}`;
|
||||
const dbUsername = `tenant_${data.slug}_user`;
|
||||
const dbPassword = this.generateSecurePassword();
|
||||
|
||||
this.logger.log(`Provisioning tenant: ${data.name} (${data.slug})`);
|
||||
|
||||
try {
|
||||
// Step 1: Create MySQL database and user
|
||||
await this.createTenantDatabase(
|
||||
dbHost,
|
||||
dbPort,
|
||||
dbName,
|
||||
dbUsername,
|
||||
dbPassword,
|
||||
);
|
||||
|
||||
// Step 2: Run migrations on new tenant database
|
||||
await this.runTenantMigrations(
|
||||
dbHost,
|
||||
dbPort,
|
||||
dbName,
|
||||
dbUsername,
|
||||
dbPassword,
|
||||
);
|
||||
|
||||
// Step 3: Store tenant info in central database
|
||||
const centralPrisma = getCentralPrisma();
|
||||
const tenant = await centralPrisma.tenant.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
dbHost,
|
||||
dbPort,
|
||||
dbName,
|
||||
dbUsername,
|
||||
dbPassword: this.tenantDbService.encryptPassword(dbPassword),
|
||||
status: 'active',
|
||||
domains: {
|
||||
create: {
|
||||
domain: data.primaryDomain,
|
||||
isPrimary: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Tenant provisioned successfully: ${tenant.id}`);
|
||||
|
||||
// Step 4: Seed default data (admin user, default roles, etc.)
|
||||
await this.seedDefaultData(tenant.id);
|
||||
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
dbName,
|
||||
dbUsername,
|
||||
dbPassword, // Return for initial setup, should be stored securely
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to provision tenant: ${data.slug}`, error);
|
||||
// Attempt cleanup
|
||||
await this.rollbackProvisioning(dbHost, dbPort, dbName, dbUsername).catch(
|
||||
(cleanupError) => {
|
||||
this.logger.error(
|
||||
'Failed to cleanup after provisioning error',
|
||||
cleanupError,
|
||||
);
|
||||
},
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MySQL database and user
|
||||
*/
|
||||
private async createTenantDatabase(
|
||||
host: string,
|
||||
port: number,
|
||||
dbName: string,
|
||||
username: string,
|
||||
password: string,
|
||||
) {
|
||||
// Connect as root to create database and user
|
||||
const rootKnex = knex.default({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
user: process.env.DB_ROOT_USER || 'root',
|
||||
password: process.env.DB_ROOT_PASSWORD || 'root',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Create database
|
||||
await rootKnex.raw(
|
||||
`CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
);
|
||||
this.logger.log(`Database created: ${dbName}`);
|
||||
|
||||
// Create user and grant privileges
|
||||
await rootKnex.raw(
|
||||
`CREATE USER IF NOT EXISTS '${username}'@'%' IDENTIFIED BY '${password}'`,
|
||||
);
|
||||
await rootKnex.raw(
|
||||
`GRANT ALL PRIVILEGES ON \`${dbName}\`.* TO '${username}'@'%'`,
|
||||
);
|
||||
await rootKnex.raw('FLUSH PRIVILEGES');
|
||||
this.logger.log(`User created: ${username}`);
|
||||
} finally {
|
||||
await rootKnex.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Knex migrations on tenant database
|
||||
*/
|
||||
private async runTenantMigrations(
|
||||
host: string,
|
||||
port: number,
|
||||
dbName: string,
|
||||
username: string,
|
||||
password: string,
|
||||
) {
|
||||
const tenantKnex = knex.default({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
database: dbName,
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
migrations: {
|
||||
directory: './migrations/tenant',
|
||||
tableName: 'knex_migrations',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await tenantKnex.migrate.latest();
|
||||
this.logger.log(`Migrations completed for database: ${dbName}`);
|
||||
} finally {
|
||||
await tenantKnex.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed default data for new tenant
|
||||
*/
|
||||
private async seedDefaultData(tenantId: string) {
|
||||
const tenantKnex = await this.tenantDbService.getTenantKnex(tenantId);
|
||||
|
||||
try {
|
||||
// Create default roles
|
||||
const adminRoleId = crypto.randomUUID();
|
||||
await tenantKnex('roles').insert({
|
||||
id: adminRoleId,
|
||||
name: 'Admin',
|
||||
guardName: 'api',
|
||||
description: 'Full system administrator access',
|
||||
created_at: tenantKnex.fn.now(),
|
||||
updated_at: tenantKnex.fn.now(),
|
||||
});
|
||||
|
||||
const userRoleId = crypto.randomUUID();
|
||||
await tenantKnex('roles').insert({
|
||||
id: userRoleId,
|
||||
name: 'User',
|
||||
guardName: 'api',
|
||||
description: 'Standard user access',
|
||||
created_at: tenantKnex.fn.now(),
|
||||
updated_at: tenantKnex.fn.now(),
|
||||
});
|
||||
|
||||
// Create default permissions
|
||||
const permissions = [
|
||||
{ name: 'manage_users', description: 'Manage users' },
|
||||
{ name: 'manage_roles', description: 'Manage roles and permissions' },
|
||||
{ name: 'manage_apps', description: 'Manage applications' },
|
||||
{ name: 'manage_objects', description: 'Manage object definitions' },
|
||||
{ name: 'view_data', description: 'View data' },
|
||||
{ name: 'create_data', description: 'Create data' },
|
||||
{ name: 'edit_data', description: 'Edit data' },
|
||||
{ name: 'delete_data', description: 'Delete data' },
|
||||
];
|
||||
|
||||
for (const perm of permissions) {
|
||||
await tenantKnex('permissions').insert({
|
||||
id: crypto.randomUUID(),
|
||||
name: perm.name,
|
||||
guardName: 'api',
|
||||
description: perm.description,
|
||||
created_at: tenantKnex.fn.now(),
|
||||
updated_at: tenantKnex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Grant all permissions to Admin role
|
||||
const allPermissions = await tenantKnex('permissions').select('id');
|
||||
for (const perm of allPermissions) {
|
||||
await tenantKnex('role_permissions').insert({
|
||||
id: crypto.randomUUID(),
|
||||
roleId: adminRoleId,
|
||||
permissionId: perm.id,
|
||||
created_at: tenantKnex.fn.now(),
|
||||
updated_at: tenantKnex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Grant view/create/edit permissions to User role
|
||||
const userPermissions = await tenantKnex('permissions')
|
||||
.whereIn('name', ['view_data', 'create_data', 'edit_data'])
|
||||
.select('id');
|
||||
for (const perm of userPermissions) {
|
||||
await tenantKnex('role_permissions').insert({
|
||||
id: crypto.randomUUID(),
|
||||
roleId: userRoleId,
|
||||
permissionId: perm.id,
|
||||
created_at: tenantKnex.fn.now(),
|
||||
updated_at: tenantKnex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(`Default data seeded for tenant: ${tenantId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to seed default data for tenant: ${tenantId}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback provisioning in case of error
|
||||
*/
|
||||
private async rollbackProvisioning(
|
||||
host: string,
|
||||
port: number,
|
||||
dbName: string,
|
||||
username: string,
|
||||
) {
|
||||
const rootKnex = knex.default({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
user: process.env.DB_ROOT_USER || 'root',
|
||||
password: process.env.DB_ROOT_PASSWORD || 'root',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await rootKnex.raw(`DROP DATABASE IF EXISTS \`${dbName}\``);
|
||||
await rootKnex.raw(`DROP USER IF EXISTS '${username}'@'%'`);
|
||||
this.logger.log(`Rolled back provisioning for database: ${dbName}`);
|
||||
} finally {
|
||||
await rootKnex.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure random password
|
||||
*/
|
||||
private generateSecurePassword(): string {
|
||||
return crypto.randomBytes(32).toString('base64').slice(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprovision a tenant (delete database and central record)
|
||||
*/
|
||||
async deprovisionTenant(tenantId: string) {
|
||||
const centralPrisma = getCentralPrisma();
|
||||
const tenant = await centralPrisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
throw new Error(`Tenant not found: ${tenantId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete tenant database
|
||||
const rootKnex = knex.default({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host: tenant.dbHost,
|
||||
port: tenant.dbPort,
|
||||
user: process.env.DB_ROOT_USER || 'root',
|
||||
password: process.env.DB_ROOT_PASSWORD || 'root',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await rootKnex.raw(`DROP DATABASE IF EXISTS \`${tenant.dbName}\``);
|
||||
await rootKnex.raw(`DROP USER IF EXISTS '${tenant.dbUsername}'@'%'`);
|
||||
this.logger.log(`Database deleted: ${tenant.dbName}`);
|
||||
} finally {
|
||||
await rootKnex.destroy();
|
||||
}
|
||||
|
||||
// Delete tenant from central database
|
||||
await centralPrisma.tenant.delete({
|
||||
where: { id: tenantId },
|
||||
});
|
||||
|
||||
// Remove from connection cache
|
||||
this.tenantDbService.removeTenantConnection(tenantId);
|
||||
|
||||
this.logger.log(`Tenant deprovisioned: ${tenantId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to deprovision tenant: ${tenantId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,88 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { TenantDatabaseService } from './tenant-database.service';
|
||||
|
||||
@Injectable()
|
||||
export class TenantMiddleware implements NestMiddleware {
|
||||
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
|
||||
if (tenantId) {
|
||||
// Attach tenantId to request object
|
||||
(req as any).tenantId = tenantId;
|
||||
private readonly logger = new Logger(TenantMiddleware.name);
|
||||
|
||||
constructor(private readonly tenantDbService: TenantDatabaseService) {}
|
||||
|
||||
async use(
|
||||
req: FastifyRequest['raw'],
|
||||
res: FastifyReply['raw'],
|
||||
next: () => void,
|
||||
) {
|
||||
try {
|
||||
// Extract subdomain from hostname
|
||||
const host = req.headers.host || '';
|
||||
const hostname = host.split(':')[0]; // Remove port if present
|
||||
const parts = hostname.split('.');
|
||||
|
||||
this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}`);
|
||||
|
||||
// For local development, accept x-tenant-id header
|
||||
let tenantId = req.headers['x-tenant-id'] as string;
|
||||
let subdomain: string | null = null;
|
||||
|
||||
this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}, x-tenant-id: ${tenantId}`);
|
||||
|
||||
// If x-tenant-id is explicitly provided, use it directly
|
||||
if (tenantId) {
|
||||
this.logger.log(`Using explicit x-tenant-id: ${tenantId}`);
|
||||
(req as any).tenantId = tenantId;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract subdomain (e.g., "tenant1" from "tenant1.routebox.co")
|
||||
// For production domains with 3+ parts, extract first part as subdomain
|
||||
if (parts.length >= 3) {
|
||||
subdomain = parts[0];
|
||||
// Ignore www subdomain
|
||||
if (subdomain === 'www') {
|
||||
subdomain = null;
|
||||
}
|
||||
}
|
||||
// For development (e.g., tenant1.localhost), also check 2 parts
|
||||
else if (parts.length === 2 && parts[1] === 'localhost') {
|
||||
subdomain = parts[0];
|
||||
}
|
||||
|
||||
this.logger.log(`Extracted subdomain: ${subdomain}`);
|
||||
|
||||
// Get tenant by subdomain if available
|
||||
if (subdomain) {
|
||||
try {
|
||||
const tenant = await this.tenantDbService.getTenantByDomain(subdomain);
|
||||
if (tenant) {
|
||||
tenantId = tenant.id;
|
||||
this.logger.log(
|
||||
`Tenant identified: ${tenant.name} (${tenant.id}) from subdomain: ${subdomain}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`No tenant found for subdomain: ${subdomain}`, error.message);
|
||||
// Fall back to using subdomain as tenantId directly if domain lookup fails
|
||||
tenantId = subdomain;
|
||||
this.logger.log(`Using subdomain as tenantId fallback: ${tenantId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (tenantId) {
|
||||
// Attach tenant info to request object
|
||||
(req as any).tenantId = tenantId;
|
||||
if (subdomain) {
|
||||
(req as any).subdomain = subdomain;
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`No tenant identified from host: ${hostname}`);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
this.logger.error('Error in tenant middleware', error);
|
||||
next();
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
||||
import { TenantMiddleware } from './tenant.middleware';
|
||||
import { TenantDatabaseService } from './tenant-database.service';
|
||||
import { TenantProvisioningService } from './tenant-provisioning.service';
|
||||
import { TenantProvisioningController } from './tenant-provisioning.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({})
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [TenantProvisioningController],
|
||||
providers: [
|
||||
TenantDatabaseService,
|
||||
TenantProvisioningService,
|
||||
TenantMiddleware,
|
||||
],
|
||||
exports: [TenantDatabaseService, TenantProvisioningService],
|
||||
})
|
||||
export class TenantModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(TenantMiddleware).forRoutes('*');
|
||||
|
||||
Reference in New Issue
Block a user