🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
Multi-Tenant Architektur Beispiele
Umfassende Multi-Tenant Architektur Muster mit Datenisolation, Tenant-Management, Ressourcenquoten und White-Label Lösungen
💻 Datenisolationsstrategien
🔴 complex
⭐⭐⭐⭐
Implementierung verschiedener Datenisolationsmuster für Multi-Tenant Anwendungen
⏱️ 45 min
🏷️ data-isolation, database-strategies, multi-tenant-architecture
Prerequisites:
Database design knowledge, Multi-tenancy concepts
// Multi-Tenant Data Isolation Patterns
// Tenant Context
interface TenantContext {
tenantId: string
tenantName: string
domain: string
plan: 'basic' | 'premium' | 'enterprise'
settings: TenantSettings
createdAt: Date
}
interface TenantSettings {
timezone: string
currency: string
locale: string
features: Record<string, boolean>
limits: ResourceLimits
}
interface ResourceLimits {
maxUsers: number
maxStorage: number // in GB
maxApiCalls: number
maxProjects: number
}
// Strategy 1: Database per Tenant (Complete Isolation)
class DatabasePerTenantStrategy {
private connections: Map<string, any> = new Map()
constructor(private connectionStringTemplate: string) {
// Template: "postgresql://user:password@localhost/tenant_{tenant_id}_db"
}
async getConnection(tenantId: string): Promise<any> {
let connection = this.connections.get(tenantId)
if (!connection) {
const connectionString = this.connectionStringTemplate.replace('{tenant_id}', tenantId)
connection = await this.createConnection(connectionString)
this.connections.set(tenantId, connection)
}
return connection
}
private async createConnection(connectionString: string): Promise<any> {
// Simulated database connection
console.log(`Creating connection to: ${connectionString}`)
return {
query: async (sql: string, params?: any[]) => {
console.log(`[${this.extractTenantId(connectionString)}] Executing: ${sql}`, params || [])
return { rows: [], rowCount: 0 }
},
close: async () => {
console.log(`Closing connection: ${connectionString}`)
}
}
}
private extractTenantId(connectionString: string): string {
const match = connectionString.match(/tenant_(.+?)_db/)
return match ? match[1] : 'unknown'
}
async createTenantDatabase(tenantId: string, schema: string): Promise<void> {
console.log(`Creating database schema for tenant ${tenantId}`)
// In real implementation, would execute SQL CREATE DATABASE and schema setup
}
async dropTenantDatabase(tenantId: string): Promise<void> {
console.log(`Dropping database for tenant ${tenantId}`)
const connection = this.connections.get(tenantId)
if (connection) {
await connection.close()
this.connections.delete(tenantId)
}
}
}
// Strategy 2: Schema per Tenant (Shared Database)
class SchemaPerTenantStrategy {
private connection: any
private tenantSchemas: Set<string> = new Set()
constructor(private connectionString: string) {
this.initializeConnection()
}
private async initializeConnection(): Promise<void> {
this.connection = {
query: async (sql: string, params?: any[]) => {
console.log(`Executing: ${sql}`, params || [])
return { rows: [], rowCount: 0 }
}
}
}
async getConnection(tenantId: string): Promise<any> {
const schemaName = `tenant_${tenantId}`
if (!this.tenantSchemas.has(tenantId)) {
await this.createTenantSchema(tenantId)
}
return {
query: async (sql: string, params?: any[]) => {
const schemaSql = sql.replace(/FROM\s+(\w+)/gi, `FROM ${schemaName}.$1`)
const schemaSql2 = schemaSql.replace(/INSERT\s+INTO\s+(\w+)/gi, `INSERT INTO ${schemaName}.$1`)
const schemaSql3 = schemaSql2.replace(/UPDATE\s+(\w+)/gi, `UPDATE ${schemaName}.$1`)
const finalSql = schemaSql3.replace(/DELETE\s+FROM\s+(\w+)/gi, `DELETE FROM ${schemaName}.$1`)
console.log(`[${schemaName}] ${finalSql}`, params || [])
return { rows: [], rowCount: 0 }
}
}
}
private async createTenantSchema(tenantId: string): Promise<void> {
const schemaName = `tenant_${tenantId}`
console.log(`Creating schema ${schemaName}`)
// Simulate schema creation
await this.connection.query(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`)
// Create tables in schema
const tables = [
`CREATE TABLE IF NOT EXISTS ${schemaName}.users (id SERIAL PRIMARY KEY, name VARCHAR(255))`,
`CREATE TABLE IF NOT EXISTS ${schemaName}.products (id SERIAL PRIMARY KEY, name VARCHAR(255), price DECIMAL)`
]
for (const tableSql of tables) {
await this.connection.query(tableSql)
}
this.tenantSchemas.add(tenantId)
}
async dropTenantSchema(tenantId: string): Promise<void> {
const schemaName = `tenant_${tenantId}`
console.log(`Dropping schema ${schemaName}`)
await this.connection.query(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`)
this.tenantSchemas.delete(tenantId)
}
}
// Strategy 3: Shared Table with Tenant Column (Row-Level Security)
class SharedTableStrategy {
private connection: any
private tenantContext: TenantContext | null = null
constructor(connectionString: string) {
this.initializeConnection(connectionString)
}
private async initializeConnection(connectionString: string): Promise<void> {
this.connection = {
query: async (sql: string, params?: any[]) => {
console.log(`[Shared] ${sql}`, params || [])
return { rows: [], rowCount: 0 }
}
}
// Enable row-level security
await this.setupRowLevelSecurity()
}
setTenantContext(tenant: TenantContext): void {
this.tenantContext = tenant
}
async query(sql: string, params?: any[]): Promise<any> {
if (!this.tenantContext) {
throw new Error('Tenant context not set')
}
// Add tenant filter to all queries
let tenantAwareSql = sql
// Add WHERE clause for tenant_id if not present
if (!sql.toLowerCase().includes('tenant_id')) {
if (sql.toLowerCase().includes('where')) {
tenantAwareSql = sql.replace(/where/gi, `WHERE tenant_id = '${this.tenantContext.tenantId}' AND`)
} else if (sql.toLowerCase().includes('from') && !sql.toLowerCase().includes('update') && !sql.toLowerCase().includes('delete')) {
tenantAwareSql = sql.replace(/from\s+(\w+)/gi, `FROM $1 WHERE tenant_id = '${this.tenantContext.tenantId}'`)
}
}
// Automatically add tenant_id to INSERT statements
if (sql.toLowerCase().startsWith('insert')) {
const hasTenantId = sql.toLowerCase().includes('tenant_id')
if (!hasTenantId) {
const valuesMatch = sql.match(/VALUES\s*\((.*?)\)/i)
if (valuesMatch) {
const existingValues = valuesMatch[1]
const newValues = existingValues ? `${existingValues}, '${this.tenantContext.tenantId}'` : `'${this.tenantContext.tenantId}'`
tenantAwareSql = sql.replace(/values\s*\((.*?)\)/i, `VALUES (${newValues})`)
// Add tenant_id to column list
tenantAwareSql = tenantAwareSql.replace(/insert\s+into\s+(\w+)\s*\((.*?)\)/i, (match, table, columns) => {
if (columns) {
return `INSERT INTO ${table} (${columns}, tenant_id)`
}
return match
})
}
}
}
return await this.connection.query(tenantAwareSql, params)
}
private async setupRowLevelSecurity(): Promise<void> {
console.log('Setting up row-level security policies')
// Simulate RLS setup
const policies = [
'CREATE POLICY tenant_isolation ON users FOR ALL TO application_user USING (tenant_id = current_setting('app.current_tenant_id')::uuid)',
'CREATE POLICY tenant_isolation ON products FOR ALL TO application_user USING (tenant_id = current_setting('app.current_tenant_id')::uuid)'
]
for (const policy of policies) {
await this.connection.query(policy)
}
}
async createTenant(tenant: TenantContext): Promise<void> {
// Create tenant-specific views or triggers if needed
console.log(`Setting up tenant ${tenant.tenantId} in shared database`)
}
async removeTenant(tenantId: string): Promise<void> {
// Soft delete or archive tenant data
await this.connection.query(
`UPDATE users SET deleted_at = NOW() WHERE tenant_id = $1`,
[tenantId]
)
}
}
// Strategy 4: Hybrid Approach (Based on tenant plan)
class HybridDataIsolationStrategy {
private databaseStrategy: DatabasePerTenantStrategy
private schemaStrategy: SchemaPerTenantStrategy
private sharedStrategy: SharedTableStrategy
constructor() {
this.databaseStrategy = new DatabasePerTenantStrategy('postgresql://user:password@localhost/tenant_{tenant_id}_db')
this.schemaStrategy = new SchemaPerTenantStrategy('postgresql://user:password@localhost/multi_tenant_db')
this.sharedStrategy = new SharedTableStrategy('postgresql://user:password@localhost/multi_tenant_db')
}
async getConnection(tenant: TenantContext): Promise<any> {
switch (tenant.plan) {
case 'enterprise':
return await this.databaseStrategy.getConnection(tenant.tenantId)
case 'premium':
return await this.schemaStrategy.getConnection(tenant.tenantId)
case 'basic':
this.sharedStrategy.setTenantContext(tenant)
return this.sharedStrategy
default:
throw new Error(`Unknown tenant plan: ${tenant.plan}`)
}
}
async createTenant(tenant: TenantContext): Promise<void> {
switch (tenant.plan) {
case 'enterprise':
await this.databaseStrategy.createTenantDatabase(tenant.tenantId, 'enterprise_schema')
break
case 'premium':
await this.schemaStrategy.createTenantSchema(tenant.tenantId)
break
case 'basic':
await this.sharedStrategy.createTenant(tenant)
break
}
}
async removeTenant(tenantId: string, plan: string): Promise<void> {
switch (plan) {
case 'enterprise':
await this.databaseStrategy.dropTenantDatabase(tenantId)
break
case 'premium':
await this.schemaStrategy.dropTenantSchema(tenantId)
break
case 'basic':
await this.sharedStrategy.removeTenant(tenantId)
break
}
}
async getTenantDataUsage(tenant: TenantContext): Promise<{
storageUsed: number
recordCount: number
databaseSize: number
}> {
const connection = await this.getConnection(tenant)
// Simulate data usage query
const usage = {
storageUsed: Math.floor(Math.random() * 100), // GB
recordCount: Math.floor(Math.random() * 1000000),
databaseSize: Math.floor(Math.random() * 1000) // MB
}
console.log(`Tenant ${tenant.tenantId} usage:`, usage)
return usage
}
}
// Data Migration between Strategies
class DataMigrationService {
async migrateTenant(
fromStrategy: string,
toStrategy: string,
tenant: TenantContext
): Promise<{
success: boolean
recordsMigrated: number
duration: number
errors: string[]
}> {
const startTime = Date.now()
const errors: string[] = []
let recordsMigrated = 0
console.log(`Starting migration for tenant ${tenant.tenantId} from ${fromStrategy} to ${toStrategy}`)
try {
// Simulate migration process
const tables = ['users', 'products', 'orders']
for (const table of tables) {
console.log(`Migrating table: ${table}`)
// Simulate data extraction and loading
const recordCount = Math.floor(Math.random() * 10000)
recordsMigrated += recordCount
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 100))
}
console.log(`Migration completed successfully. Records migrated: ${recordsMigrated}`)
} catch (error) {
errors.push(error instanceof Error ? error.message : 'Unknown error')
console.error(`Migration failed:`, errors)
}
const duration = Date.now() - startTime
return {
success: errors.length === 0,
recordsMigrated,
duration,
errors
}
}
async validateMigration(
sourceTenant: TenantContext,
targetTenant: TenantContext
): Promise<{
isValid: boolean
discrepancies: Array<{
table: string
sourceCount: number
targetCount: number
difference: number
}>
}> {
console.log(`Validating migration from ${sourceTenant.tenantId} to ${targetTenant.tenantId}`)
// Simulate validation
const tables = ['users', 'products', 'orders']
const discrepancies = []
for (const table of tables) {
const sourceCount = Math.floor(Math.random() * 10000)
const targetCount = sourceCount + Math.floor(Math.random() * 10) - 5 // Small random difference
const difference = Math.abs(sourceCount - targetCount)
if (difference > 0) {
discrepancies.push({
table,
sourceCount,
targetCount,
difference
})
}
}
return {
isValid: discrepancies.length === 0,
discrepancies
}
}
}
// Performance Monitoring for Multi-Tenant Systems
class MultiTenantPerformanceMonitor {
private queryMetrics: Map<string, Array<{ duration: number; timestamp: Date }>> = new Map()
private connectionMetrics: Map<string, { active: number; total: number; errors: number }> = new Map()
recordQuery(tenantId: string, duration: number): void {
if (!this.queryMetrics.has(tenantId)) {
this.queryMetrics.set(tenantId, [])
}
const metrics = this.queryMetrics.get(tenantId)!
metrics.push({ duration, timestamp: new Date() })
// Keep only last 1000 queries per tenant
if (metrics.length > 1000) {
metrics.shift()
}
}
recordConnection(tenantId: string, isActive: boolean, isError: boolean = false): void {
if (!this.connectionMetrics.has(tenantId)) {
this.connectionMetrics.set(tenantId, { active: 0, total: 0, errors: 0 })
}
const metrics = this.connectionMetrics.get(tenantId)!
if (isActive) {
metrics.active++
} else {
metrics.active = Math.max(0, metrics.active - 1)
}
metrics.total++
if (isError) {
metrics.errors++
}
}
getTenantMetrics(tenantId: string): {
avgQueryTime: number
totalQueries: number
activeConnections: number
errorRate: number
} {
const queryMetrics = this.queryMetrics.get(tenantId) || []
const connectionMetrics = this.connectionMetrics.get(tenantId) || { active: 0, total: 0, errors: 0 }
const avgQueryTime = queryMetrics.length > 0
? queryMetrics.reduce((sum, m) => sum + m.duration, 0) / queryMetrics.length
: 0
const errorRate = connectionMetrics.total > 0
? connectionMetrics.errors / connectionMetrics.total
: 0
return {
avgQueryTime,
totalQueries: queryMetrics.length,
activeConnections: connectionMetrics.active,
errorRate
}
}
identifySlowTenants(thresholdMs: number = 1000): Array<{
tenantId: string
avgQueryTime: number
totalQueries: number
}> {
const slowTenants: Array<{
tenantId: string
avgQueryTime: number
totalQueries: number
}> = []
for (const [tenantId, queryMetrics] of this.queryMetrics.entries()) {
if (queryMetrics.length === 0) continue
const avgQueryTime = queryMetrics.reduce((sum, m) => sum + m.duration, 0) / queryMetrics.length
if (avgQueryTime > thresholdMs) {
slowTenants.push({
tenantId,
avgQueryTime,
totalQueries: queryMetrics.length
})
}
}
return slowTenants.sort((a, b) => b.avgQueryTime - a.avgQueryTime)
}
}
// Usage Examples
async function dataIsolationExample() {
console.log('=== Multi-Tenant Data Isolation Strategies ===\n')
// Create sample tenant contexts
const enterpriseTenant: TenantContext = {
tenantId: 'enterprise-123',
tenantName: 'Acme Corporation',
domain: 'acme.corp',
plan: 'enterprise',
settings: {
timezone: 'UTC',
currency: 'USD',
locale: 'en-US',
features: { 'advanced-analytics': true, 'custom-integrations': true },
limits: { maxUsers: 1000, maxStorage: 1000, maxApiCalls: 10000000, maxProjects: 100 }
},
createdAt: new Date()
}
const premiumTenant: TenantContext = {
tenantId: 'premium-456',
tenantName: 'Startup Inc',
domain: 'startup.io',
plan: 'premium',
settings: {
timezone: 'America/New_York',
currency: 'USD',
locale: 'en-US',
features: { 'advanced-analytics': true, 'custom-integrations': false },
limits: { maxUsers: 100, maxStorage: 100, maxApiCalls: 1000000, maxProjects: 20 }
},
createdAt: new Date()
}
const basicTenant: TenantContext = {
tenantId: 'basic-789',
tenantName: 'Small Business LLC',
domain: 'smallbiz.com',
plan: 'basic',
settings: {
timezone: 'America/Los_Angeles',
currency: 'USD',
locale: 'en-US',
features: { 'advanced-analytics': false, 'custom-integrations': false },
limits: { maxUsers: 10, maxStorage: 10, maxApiCalls: 100000, maxProjects: 5 }
},
createdAt: new Date()
}
// Test different strategies
console.log('1. Database per Tenant Strategy')
const dbStrategy = new DatabasePerTenantStrategy('postgresql://user:password@localhost/tenant_{tenant_id}_db')
await dbStrategy.getConnection(enterpriseTenant.tenantId)
console.log('\n2. Schema per Tenant Strategy')
const schemaStrategy = new SchemaPerTenantStrategy('postgresql://user:password@localhost/multi_tenant_db')
await schemaStrategy.getConnection(premiumTenant.tenantId)
console.log('\n3. Shared Table Strategy')
const sharedStrategy = new SharedTableStrategy('postgresql://user:password@localhost/multi_tenant_db')
sharedStrategy.setTenantContext(basicTenant)
await sharedStrategy.query('SELECT * FROM users')
console.log('\n4. Hybrid Strategy')
const hybridStrategy = new HybridDataIsolationStrategy()
// Create tenants with different plans
await hybridStrategy.createTenant(enterpriseTenant)
await hybridStrategy.createTenant(premiumTenant)
await hybridStrategy.createTenant(basicTenant)
// Get connections for different tenants
await hybridStrategy.getConnection(enterpriseTenant)
await hybridStrategy.getConnection(premiumTenant)
await hybridStrategy.getConnection(basicTenant)
// Monitor data usage
console.log('\n5. Data Usage Monitoring')
for (const tenant of [enterpriseTenant, premiumTenant, basicTenant]) {
const usage = await hybridStrategy.getTenantDataUsage(tenant)
console.log(`Tenant ${tenant.tenantId} (${tenant.plan}):`, usage)
}
// Performance monitoring
console.log('\n6. Performance Monitoring')
const monitor = new MultiTenantPerformanceMonitor()
// Simulate some queries
for (let i = 0; i < 10; i++) {
monitor.recordQuery(enterpriseTenant.tenantId, Math.random() * 200 + 50)
monitor.recordQuery(premiumTenant.tenantId, Math.random() * 300 + 100)
monitor.recordQuery(basicTenant.tenantId, Math.random() * 500 + 200)
}
// Get metrics
for (const tenantId of [enterpriseTenant.tenantId, premiumTenant.tenantId, basicTenant.tenantId]) {
const metrics = monitor.getTenantMetrics(tenantId)
console.log(`Tenant ${tenantId} metrics:`, metrics)
}
// Identify slow tenants
const slowTenants = monitor.identifySlowTenants(400)
if (slowTenants.length > 0) {
console.log('\nSlow tenants identified:', slowTenants)
}
}
export {
TenantContext,
TenantSettings,
ResourceLimits,
DatabasePerTenantStrategy,
SchemaPerTenantStrategy,
SharedTableStrategy,
HybridDataIsolationStrategy,
DataMigrationService,
MultiTenantPerformanceMonitor,
dataIsolationExample
}
💻 Tenant-Management
🔴 complex
⭐⭐⭐⭐
Umfassendes Tenant-Lebenszyklus-Management mit Onboarding, Konfiguration und Monitoring
⏱️ 60 min
🏷️ tenant-management, lifecycle, upgrades-downgrades
Prerequisites:
Understanding of SaaS architecture, User management concepts
// Tenant Management System
// Tenant Types and Plans
enum TenantPlan {
BASIC = 'basic',
PREMIUM = 'premium',
ENTERPRISE = 'enterprise'
}
enum TenantStatus {
PENDING = 'pending',
ACTIVE = 'active',
SUSPENDED = 'suspended',
TERMINATED = 'terminated'
}
interface Tenant {
id: string
name: string
domain: string
plan: TenantPlan
status: TenantStatus
settings: TenantSettings
billing: BillingInfo
createdAt: Date
updatedAt: Date
activatedAt?: Date
suspendedAt?: Date
terminatedAt?: Date
}
interface TenantSettings {
timezone: string
currency: string
locale: string
theme: {
primaryColor: string
logo: string
customCSS?: string
}
features: Record<string, boolean>
limits: ResourceLimits
notifications: NotificationSettings
}
interface BillingInfo {
billingEmail: string
paymentMethod: string
nextBillingDate: Date
subscriptionAmount: number
currency: string
isActive: boolean
}
interface ResourceLimits {
maxUsers: number
maxStorage: number // GB
maxApiCalls: number
maxProjects: number
maxIntegrations: number
bandwidthLimit: number // GB/month
}
interface NotificationSettings {
email: boolean
sms: boolean
inApp: boolean
alerts: {
diskUsage: boolean
apiLimit: boolean
userLimit: boolean
billing: boolean
}
}
// Tenant Repository
interface ITenantRepository {
create(tenant: Omit<Tenant, 'id' | 'createdAt' | 'updatedAt'>): Promise<Tenant>
findById(id: string): Promise<Tenant | null>
findByDomain(domain: string): Promise<Tenant | null>
update(id: string, updates: Partial<Tenant>): Promise<Tenant>
delete(id: string): Promise<void>
findAll(filters?: TenantFilters): Promise<Tenant[]>
findActiveTenants(): Promise<Tenant[]>
}
interface TenantFilters {
plan?: TenantPlan
status?: TenantStatus
createdAfter?: Date
createdBefore?: Date
limit?: number
offset?: number
}
class InMemoryTenantRepository implements ITenantRepository {
private tenants: Map<string, Tenant> = new Map()
async create(tenantData: Omit<Tenant, 'id' | 'createdAt' | 'updatedAt'>): Promise<Tenant> {
const tenant: Tenant = {
...tenantData,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date()
}
this.tenants.set(tenant.id, tenant)
return tenant
}
async findById(id: string): Promise<Tenant | null> {
return this.tenants.get(id) || null
}
async findByDomain(domain: string): Promise<Tenant | null> {
for (const tenant of this.tenants.values()) {
if (tenant.domain === domain) {
return tenant
}
}
return null
}
async update(id: string, updates: Partial<Tenant>): Promise<Tenant> {
const tenant = this.tenants.get(id)
if (!tenant) {
throw new Error(`Tenant not found: ${id}`)
}
const updatedTenant = {
...tenant,
...updates,
updatedAt: new Date()
}
this.tenants.set(id, updatedTenant)
return updatedTenant
}
async delete(id: string): Promise<void> {
if (!this.tenants.has(id)) {
throw new Error(`Tenant not found: ${id}`)
}
this.tenants.delete(id)
}
async findAll(filters?: TenantFilters): Promise<Tenant[]> {
let tenants = Array.from(this.tenants.values())
if (filters) {
if (filters.plan) {
tenants = tenants.filter(t => t.plan === filters.plan)
}
if (filters.status) {
tenants = tenants.filter(t => t.status === filters.status)
}
if (filters.createdAfter) {
tenants = tenants.filter(t => t.createdAt >= filters.createdAfter!)
}
if (filters.createdBefore) {
tenants = tenants.filter(t => t.createdAt <= filters.createdBefore!)
}
}
return tenants.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}
async findActiveTenants(): Promise<Tenant[]> {
return Array.from(this.tenants.values()).filter(t => t.status === TenantStatus.ACTIVE)
}
}
// Tenant Onboarding Service
class TenantOnboardingService {
constructor(
private tenantRepository: ITenantRepository,
private notificationService: NotificationService,
private provisioningService: TenantProvisioningService
) {}
async onboardTenant(request: TenantOnboardingRequest): Promise<{
tenant: Tenant
onboardingSteps: OnboardingStep[]
}> {
console.log(`Starting onboarding for: ${request.name}`)
const onboardingSteps: OnboardingStep[] = []
// Step 1: Validate request
onboardingSteps.push(await this.validateOnboardingRequest(request))
// Step 2: Create tenant
const tenant = await this.createTenantFromRequest(request)
onboardingSteps.push({
step: 'CREATE_TENANT',
status: 'COMPLETED',
message: 'Tenant record created',
timestamp: new Date()
})
// Step 3: Provision resources
onboardingSteps.push(await this.provisioningService.provisionTenantResources(tenant))
// Step 4: Configure initial settings
onboardingSteps.push(await this.configureTenantSettings(tenant))
// Step 5: Send welcome email
onboardingSteps.push(await this.sendWelcomeNotification(tenant))
// Step 6: Activate tenant
const activatedTenant = await this.activateTenant(tenant.id)
onboardingSteps.push({
step: 'ACTIVATE_TENANT',
status: 'COMPLETED',
message: 'Tenant activated successfully',
timestamp: new Date()
})
return {
tenant: activatedTenant,
onboardingSteps
}
}
private async validateOnboardingRequest(request: TenantOnboardingRequest): Promise<OnboardingStep> {
// Validate domain uniqueness
const existingTenant = await this.tenantRepository.findByDomain(request.domain)
if (existingTenant) {
throw new Error(`Domain ${request.domain} is already taken`)
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(request.adminEmail)) {
throw new Error('Invalid admin email format')
}
// Validate plan
if (!Object.values(TenantPlan).includes(request.plan as TenantPlan)) {
throw new Error(`Invalid plan: ${request.plan}`)
}
return {
step: 'VALIDATE_REQUEST',
status: 'COMPLETED',
message: 'Request validation passed',
timestamp: new Date()
}
}
private async createTenantFromRequest(request: TenantOnboardingRequest): Promise<Tenant> {
const defaultSettings = this.getDefaultTenantSettings(request.plan as TenantPlan)
const defaultLimits = this.getDefaultResourceLimits(request.plan as TenantPlan)
const tenant = await this.tenantRepository.create({
name: request.name,
domain: request.domain,
plan: request.plan as TenantPlan,
status: TenantStatus.PENDING,
settings: {
...defaultSettings,
limits: defaultLimits
},
billing: {
billingEmail: request.adminEmail,
paymentMethod: 'CREDIT_CARD',
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
subscriptionAmount: this.getPlanPricing(request.plan as TenantPlan),
currency: 'USD',
isActive: false
}
})
return tenant
}
private async configureTenantSettings(tenant: Tenant): Promise<OnboardingStep> {
// Configure default features based on plan
const features = this.getDefaultFeatures(tenant.plan)
await this.tenantRepository.update(tenant.id, {
settings: {
...tenant.settings,
features
}
})
return {
step: 'CONFIGURE_SETTINGS',
status: 'COMPLETED',
message: 'Default settings configured',
timestamp: new Date()
}
}
private async sendWelcomeNotification(tenant: Tenant): Promise<OnboardingStep> {
try {
await this.notificationService.sendWelcomeEmail(tenant)
return {
step: 'SEND_WELCOME_EMAIL',
status: 'COMPLETED',
message: 'Welcome email sent',
timestamp: new Date()
}
} catch (error) {
return {
step: 'SEND_WELCOME_EMAIL',
status: 'FAILED',
message: error instanceof Error ? error.message : 'Failed to send welcome email',
timestamp: new Date()
}
}
}
private async activateTenant(tenantId: string): Promise<Tenant> {
return await this.tenantRepository.update(tenantId, {
status: TenantStatus.ACTIVE,
activatedAt: new Date(),
billing: {
// This should merge with existing billing info
billingEmail: '', // Will be set in update
paymentMethod: '',
nextBillingDate: new Date(),
subscriptionAmount: 0,
currency: 'USD',
isActive: true
}
})
}
private getDefaultTenantSettings(plan: TenantPlan): Omit<TenantSettings, 'limits'> {
const baseSettings = {
timezone: 'UTC',
currency: 'USD',
locale: 'en-US',
theme: {
primaryColor: '#007bff',
logo: '/default-logo.png'
},
notifications: {
email: true,
sms: false,
inApp: true,
alerts: {
diskUsage: true,
apiLimit: true,
userLimit: true,
billing: true
}
}
}
return baseSettings
}
private getDefaultResourceLimits(plan: TenantPlan): ResourceLimits {
switch (plan) {
case TenantPlan.BASIC:
return {
maxUsers: 5,
maxStorage: 10,
maxApiCalls: 10000,
maxProjects: 3,
maxIntegrations: 2,
bandwidthLimit: 50
}
case TenantPlan.PREMIUM:
return {
maxUsers: 50,
maxStorage: 100,
maxApiCalls: 1000000,
maxProjects: 20,
maxIntegrations: 10,
bandwidthLimit: 500
}
case TenantPlan.ENTERPRISE:
return {
maxUsers: 1000,
maxStorage: 1000,
maxApiCalls: 10000000,
maxProjects: 100,
maxIntegrations: 50,
bandwidthLimit: 5000
}
default:
throw new Error(`Unknown plan: ${plan}`)
}
}
private getDefaultFeatures(plan: TenantPlan): Record<string, boolean> {
const baseFeatures = {
'dashboard': true,
'user-management': true,
'basic-analytics': true
}
switch (plan) {
case TenantPlan.BASIC:
return baseFeatures
case TenantPlan.PREMIUM:
return {
...baseFeatures,
'advanced-analytics': true,
'api-access': true,
'team-collaboration': true,
'export-data': true
}
case TenantPlan.ENTERPRISE:
return {
...baseFeatures,
'advanced-analytics': true,
'api-access': true,
'team-collaboration': true,
'export-data': true,
'custom-integrations': true,
'single-sign-on': true,
'white-label': true,
'dedicated-support': true,
'audit-logs': true,
'custom-domains': true
}
default:
return baseFeatures
}
}
private getPlanPricing(plan: TenantPlan): number {
switch (plan) {
case TenantPlan.BASIC: return 29
case TenantPlan.PREMIUM: return 99
case TenantPlan.ENTERPRISE: return 499
default: return 0
}
}
}
// Tenant Lifecycle Management
class TenantLifecycleManager {
constructor(
private tenantRepository: ITenantRepository,
private monitoringService: TenantMonitoringService,
private notificationService: NotificationService
) {}
async upgradeTenantPlan(tenantId: string, newPlan: TenantPlan): Promise<Tenant> {
const tenant = await this.tenantRepository.findById(tenantId)
if (!tenant) {
throw new Error(`Tenant not found: ${tenantId}`)
}
if (tenant.plan === newPlan) {
throw new Error(`Tenant already on plan: ${newPlan}`)
}
console.log(`Upgrading tenant ${tenantId} from ${tenant.plan} to ${newPlan}`)
// Update resource limits
const newLimits = this.getResourceLimitsForPlan(newPlan)
const newFeatures = this.getFeaturesForPlan(newPlan)
const updatedTenant = await this.tenantRepository.update(tenantId, {
plan: newPlan,
settings: {
...tenant.settings,
limits: newLimits,
features: newFeatures
},
billing: {
...tenant.billing,
subscriptionAmount: this.getPlanPrice(newPlan)
}
})
// Send notification
await this.notificationService.sendPlanUpgradeNotification(updatedTenant, tenant.plan)
return updatedTenant
}
async suspendTenant(tenantId: string, reason: string): Promise<Tenant> {
const tenant = await this.tenantRepository.findById(tenantId)
if (!tenant) {
throw new Error(`Tenant not found: ${tenantId}`)
}
if (tenant.status === TenantStatus.SUSPENDED) {
throw new Error('Tenant is already suspended')
}
console.log(`Suspending tenant ${tenantId}. Reason: ${reason}`)
const updatedTenant = await this.tenantRepository.update(tenantId, {
status: TenantStatus.SUSPENDED,
suspendedAt: new Date()
})
// Disable access
await this.monitoringService.disableTenantAccess(tenantId)
// Send notification
await this.notificationService.sendSuspensionNotification(updatedTenant, reason)
return updatedTenant
}
async reactivateTenant(tenantId: string): Promise<Tenant> {
const tenant = await this.tenantRepository.findById(tenantId)
if (!tenant) {
throw new Error(`Tenant not found: ${tenantId}`)
}
if (tenant.status !== TenantStatus.SUSPENDED) {
throw new Error('Only suspended tenants can be reactivated')
}
console.log(`Reactivating tenant ${tenantId}`)
const updatedTenant = await this.tenantRepository.update(tenantId, {
status: TenantStatus.ACTIVE,
suspendedAt: undefined
})
// Enable access
await this.monitoringService.enableTenantAccess(tenantId)
// Send notification
await this.notificationService.sendReactivationNotification(updatedTenant)
return updatedTenant
}
async terminateTenant(tenantId: string, reason: string): Promise<void> {
const tenant = await this.tenantRepository.findById(tenantId)
if (!tenant) {
throw new Error(`Tenant not found: ${tenantId}`)
}
console.log(`Terminating tenant ${tenantId}. Reason: ${reason}`)
// Mark as terminated
await this.tenantRepository.update(tenantId, {
status: TenantStatus.TERMINATED,
terminatedAt: new Date()
})
// Archive data
await this.monitoringService.archiveTenantData(tenantId)
// Disable access
await this.monitoringService.disableTenantAccess(tenantId)
// Send final notification
await this.notificationService.sendTerminationNotification(tenant, reason)
}
private getResourceLimitsForPlan(plan: TenantPlan): ResourceLimits {
// Implementation would get the limits for the specified plan
// This is simplified for the example
return {
maxUsers: 100,
maxStorage: 100,
maxApiCalls: 1000000,
maxProjects: 20,
maxIntegrations: 10,
bandwidthLimit: 500
}
}
private getFeaturesForPlan(plan: TenantPlan): Record<string, boolean> {
// Implementation would get the features for the specified plan
return {
'dashboard': true,
'user-management': true,
'basic-analytics': true
}
}
private getPlanPrice(plan: TenantPlan): number {
switch (plan) {
case TenantPlan.BASIC: return 29
case TenantPlan.PREMIUM: return 99
case TenantPlan.ENTERPRISE: return 499
default: return 0
}
}
}
// Supporting Interfaces and Classes
interface TenantOnboardingRequest {
name: string
domain: string
adminEmail: string
plan: string
companySize?: string
industry?: string
}
interface OnboardingStep {
step: string
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED'
message: string
timestamp: Date
error?: string
}
interface NotificationService {
sendWelcomeEmail(tenant: Tenant): Promise<void>
sendPlanUpgradeNotification(tenant: Tenant, oldPlan: TenantPlan): Promise<void>
sendSuspensionNotification(tenant: Tenant, reason: string): Promise<void>
sendReactivationNotification(tenant: Tenant): Promise<void>
sendTerminationNotification(tenant: Tenant, reason: string): Promise<void>
}
interface TenantProvisioningService {
provisionTenantResources(tenant: Tenant): Promise<OnboardingStep>
}
interface TenantMonitoringService {
disableTenantAccess(tenantId: string): Promise<void>
enableTenantAccess(tenantId: string): Promise<void>
archiveTenantData(tenantId: string): Promise<void>
}
// Mock implementations for demo purposes
class MockNotificationService implements NotificationService {
async sendWelcomeEmail(tenant: Tenant): Promise<void> {
console.log(`Sending welcome email to ${tenant.billing.billingEmail}`)
}
async sendPlanUpgradeNotification(tenant: Tenant, oldPlan: TenantPlan): Promise<void> {
console.log(`Sending plan upgrade notification to ${tenant.billing.billingEmail}`)
}
async sendSuspensionNotification(tenant: Tenant, reason: string): Promise<void> {
console.log(`Sending suspension notification to ${tenant.billing.billingEmail}`)
}
async sendReactivationNotification(tenant: Tenant): Promise<void> {
console.log(`Sending reactivation notification to ${tenant.billing.billingEmail}`)
}
async sendTerminationNotification(tenant: Tenant, reason: string): Promise<void> {
console.log(`Sending termination notification to ${tenant.billing.billingEmail}`)
}
}
class MockTenantProvisioningService implements TenantProvisioningService {
async provisionTenantResources(tenant: Tenant): Promise<OnboardingStep> {
console.log(`Provisioning resources for tenant ${tenant.id} (${tenant.plan})`)
// Simulate resource provisioning
await new Promise(resolve => setTimeout(resolve, 1000))
return {
step: 'PROVISION_RESOURCES',
status: 'COMPLETED',
message: 'Resources provisioned successfully',
timestamp: new Date()
}
}
}
class MockTenantMonitoringService implements TenantMonitoringService {
async disableTenantAccess(tenantId: string): Promise<void> {
console.log(`Disabling access for tenant ${tenantId}`)
}
async enableTenantAccess(tenantId: string): Promise<void> {
console.log(`Enabling access for tenant ${tenantId}`)
}
async archiveTenantData(tenantId: string): Promise<void> {
console.log(`Archiving data for tenant ${tenantId}`)
}
}
// Usage Example
async function tenantManagementExample() {
console.log('=== Tenant Management System ===\n')
// Setup services
const tenantRepository = new InMemoryTenantRepository()
const notificationService = new MockNotificationService()
const provisioningService = new MockTenantProvisioningService()
const monitoringService = new MockTenantMonitoringService()
const onboardingService = new TenantOnboardingService(
tenantRepository,
notificationService,
provisioningService
)
const lifecycleManager = new TenantLifecycleManager(
tenantRepository,
monitoringService,
notificationService
)
// Onboard new tenants
console.log('1. Onboarding New Tenants')
const basicTenantRequest: TenantOnboardingRequest = {
name: 'Small Business Inc',
domain: 'smallbusiness.app',
adminEmail: '[email protected]',
plan: 'basic',
companySize: '1-10',
industry: 'Technology'
}
const basicResult = await onboardingService.onboardTenant(basicTenantRequest)
console.log(`Basic tenant onboarded: ${basicResult.tenant.name} (${basicResult.tenant.id})`)
const enterpriseTenantRequest: TenantOnboardingRequest = {
name: 'Enterprise Corp',
domain: 'enterprise.corp',
adminEmail: '[email protected]',
plan: 'enterprise',
companySize: '1000+',
industry: 'Finance'
}
const enterpriseResult = await onboardingService.onboardTenant(enterpriseTenantRequest)
console.log(`Enterprise tenant onboarded: ${enterpriseResult.tenant.name} (${enterpriseResult.tenant.id})`)
// Display onboarding steps
console.log('\nOnboarding steps for ${basicResult.tenant.name}:')
basicResult.onboardingSteps.forEach(step => {
console.log(` ${step.step}: ${step.status} - ${step.message}`)
})
// List all active tenants
console.log('\n2. Active Tenants')
const activeTenants = await tenantRepository.findActiveTenants()
activeTenants.forEach(tenant => {
console.log(` ${tenant.name} (${tenant.plan}) - ${tenant.domain}`)
})
// Upgrade tenant plan
console.log('\n3. Upgrading Tenant Plan')
const upgradedTenant = await lifecycleManager.upgradeTenantPlan(
basicResult.tenant.id,
TenantPlan.PREMIUM
)
console.log(`Upgraded ${upgradedTenant.name} to ${upgradedTenant.plan}`)
// Suspend tenant
console.log('\n4. Suspending Tenant')
const suspendedTenant = await lifecycleManager.suspendTenant(
enterpriseResult.tenant.id,
'Payment overdue'
)
console.log(`Suspended ${suspendedTenant.name} for: Payment overdue`)
// Reactivate tenant
console.log('\n5. Reactivating Tenant')
await new Promise(resolve => setTimeout(resolve, 1000)) // Simulate payment
const reactivatedTenant = await lifecycleManager.reactivateTenant(suspendedTenant.id)
console.log(`Reactivated ${reactivatedTenant.name}`)
// Get tenant statistics
console.log('\n6. Tenant Statistics')
const allTenants = await tenantRepository.findAll()
const statsByPlan = allTenants.reduce((acc, tenant) => {
acc[tenant.plan] = (acc[tenant.plan] || 0) + 1
return acc
}, {} as Record<string, number>)
console.log('Tenants by plan:', statsByPlan)
const statsByStatus = allTenants.reduce((acc, tenant) => {
acc[tenant.status] = (acc[tenant.status] || 0) + 1
return acc
}, {} as Record<string, number>)
console.log('Tenants by status:', statsByStatus)
}
export {
Tenant,
TenantPlan,
TenantStatus,
TenantSettings,
BillingInfo,
ResourceLimits,
NotificationSettings,
ITenantRepository,
InMemoryTenantRepository,
TenantOnboardingService,
TenantLifecycleManager,
TenantOnboardingRequest,
OnboardingStep,
MockNotificationService,
MockTenantProvisioningService,
MockTenantMonitoringService,
tenantManagementExample
}
💻 Ressourcenquoten und Limits
🔴 complex
⭐⭐⭐⭐
Implementierung von Ressourcenquoten-Management mit Monitoring, Alerts und Durchsetzung
⏱️ 50 min
🏷️ resource-quotas, limits-management, usage-monitoring
Prerequisites:
Monitoring systems knowledge, Rate limiting concepts
// Resource Quota Management System
// Resource Types and Quota Definitions
enum ResourceType {
USERS = 'users',
STORAGE = 'storage',
API_CALLS = 'api_calls',
PROJECTS = 'projects',
INTEGRATIONS = 'integrations',
BANDWIDTH = 'bandwidth'
}
interface ResourceQuota {
resourceType: ResourceType
limit: number
current: number
period?: 'monthly' | 'daily' | 'yearly'
resetDate?: Date
unit: string
}
interface TenantQuota {
tenantId: string
quotas: Map<ResourceType, ResourceQuota>
lastUpdated: Date
}
interface UsageRecord {
tenantId: string
resourceType: ResourceType
amount: number
timestamp: Date
metadata?: Record<string, any>
}
interface QuotaAlert {
tenantId: string
resourceType: ResourceType
alertType: 'WARNING' | 'CRITICAL' | 'EXCEEDED'
currentUsage: number
limit: number
percentage: number
timestamp: Date
}
// Resource Usage Tracker
class ResourceUsageTracker {
private usageRecords: Map<string, UsageRecord[]> = new Map()
private tenantQuotas: Map<string, TenantQuota> = new Map()
// Record resource usage
recordUsage(tenantId: string, resourceType: ResourceType, amount: number, metadata?: Record<string, any>): void {
const record: UsageRecord = {
tenantId,
resourceType,
amount,
timestamp: new Date(),
metadata
}
const key = `${tenantId}:${resourceType}`
if (!this.usageRecords.has(key)) {
this.usageRecords.set(key, [])
}
this.usageRecords.get(key)!.push(record)
// Keep only last 10000 records per resource per tenant
const records = this.usageRecords.get(key)!
if (records.length > 10000) {
records.splice(0, records.length - 10000)
}
}
// Get current usage for a specific resource
getCurrentUsage(tenantId: string, resourceType: ResourceType, period?: 'daily' | 'monthly'): number {
const key = `${tenantId}:${resourceType}`
const records = this.usageRecords.get(key) || []
if (!period) {
// Return total usage
return records.reduce((total, record) => total + record.amount, 0)
}
const now = new Date()
const cutoffDate = this.getCutoffDate(now, period)
return records
.filter(record => record.timestamp >= cutoffDate)
.reduce((total, record) => total + record.amount, 0)
}
// Get usage trend for analytics
getUsageTrend(tenantId: string, resourceType: ResourceType, days: number = 30): Array<{
date: string
usage: number
}> {
const key = `${tenantId}:${resourceType}`
const records = this.usageRecords.get(key) || []
const endDate = new Date()
const startDate = new Date(endDate.getTime() - (days * 24 * 60 * 60 * 1000))
// Group usage by day
const dailyUsage: Record<string, number> = {}
for (const record of records) {
if (record.timestamp >= startDate && record.timestamp <= endDate) {
const dateKey = record.timestamp.toISOString().split('T')[0]
dailyUsage[dateKey] = (dailyUsage[dateKey] || 0) + record.amount
}
}
// Fill missing days with 0 usage
const trend: Array<{ date: string; usage: number }> = []
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
const dateKey = date.toISOString().split('T')[0]
trend.push({
date: dateKey,
usage: dailyUsage[dateKey] || 0
})
}
return trend
}
// Get top resource consumers
getTopConsumers(resourceType: ResourceType, limit: number = 10): Array<{
tenantId: string
usage: number
}> {
const tenantUsage: Record<string, number> = {}
for (const [key, records] of this.usageRecords.entries()) {
if (key.endsWith(`:${resourceType}`)) {
const tenantId = key.split(':')[0]
const usage = records.reduce((total, record) => total + record.amount, 0)
tenantUsage[tenantId] = usage
}
}
return Object.entries(tenantUsage)
.map(([tenantId, usage]) => ({ tenantId, usage }))
.sort((a, b) => b.usage - a.usage)
.slice(0, limit)
}
private getCutoffDate(date: Date, period: 'daily' | 'monthly'): Date {
if (period === 'daily') {
const cutoff = new Date(date)
cutoff.setHours(0, 0, 0, 0)
return cutoff
} else if (period === 'monthly') {
const cutoff = new Date(date.getFullYear(), date.getMonth(), 1)
return cutoff
}
return new Date(0)
}
// Set tenant quota
setTenantQuota(tenantId: string, quotas: Map<ResourceType, ResourceQuota>): void {
this.tenantQuotas.set(tenantId, {
tenantId,
quotas: new Map(quotas),
lastUpdated: new Date()
})
}
// Get tenant quota
getTenantQuota(tenantId: string): TenantQuota | null {
return this.tenantQuotas.get(tenantId) || null
}
}
// Quota Enforcement Engine
class QuotaEnforcementEngine {
private alertThresholds: Map<ResourceType, { warning: number; critical: number }> = new Map([
[ResourceType.USERS, { warning: 0.8, critical: 0.95 }],
[ResourceType.STORAGE, { warning: 0.8, critical: 0.95 }],
[ResourceType.API_CALLS, { warning: 0.8, critical: 0.95 }],
[ResourceType.PROJECTS, { warning: 0.8, critical: 0.95 }],
[ResourceType.INTEGRATIONS, { warning: 0.8, critical: 0.95 }],
[ResourceType.BANDWIDTH, { warning: 0.8, critical: 0.95 }]
])
constructor(
private usageTracker: ResourceUsageTracker,
private alertService: QuotaAlertService,
private enforcementActions: QuotaEnforcementActions
) {}
// Check if action can be performed (soft check)
canPerformAction(
tenantId: string,
resourceType: ResourceType,
additionalAmount: number = 1
): {
allowed: boolean
currentUsage: number
limit: number
remaining: number
percentageUsed: number
} {
const quota = this.usageTracker.getTenantQuota(tenantId)
if (!quota) {
return { allowed: false, currentUsage: 0, limit: 0, remaining: 0, percentageUsed: 100 }
}
const resourceQuota = quota.quotas.get(resourceType)
if (!resourceQuota) {
return { allowed: false, currentUsage: 0, limit: 0, remaining: 0, percentageUsed: 100 }
}
const currentUsage = this.getCurrentUsage(tenantId, resourceType, resourceQuota.period)
const newTotalUsage = currentUsage + additionalAmount
const remaining = Math.max(0, resourceQuota.limit - newTotalUsage)
const percentageUsed = (newTotalUsage / resourceQuota.limit) * 100
const allowed = newTotalUsage <= resourceQuota.limit
return {
allowed,
currentUsage,
limit: resourceQuota.limit,
remaining,
percentageUsed
}
}
// Enforce quota check (hard check with potential action)
async enforceQuota(
tenantId: string,
resourceType: ResourceType,
additionalAmount: number = 1,
actionContext?: Record<string, any>
): Promise<{
allowed: boolean
reason?: string
action?: string
}> {
const check = this.canPerformAction(tenantId, resourceType, additionalAmount)
if (!check.allowed) {
// Quota exceeded
await this.handleQuotaExceeded(tenantId, resourceType, check, actionContext)
return {
allowed: false,
reason: `Quota exceeded for ${resourceType}. Current: ${check.currentUsage}, Limit: ${check.limit}`,
action: await this.enforcementActions.getEnforcementAction(tenantId, resourceType, check.percentageUsed)
}
}
// Check for approaching limits
await this.checkApproachingLimits(tenantId, resourceType, check)
// Record the usage
this.usageTracker.recordUsage(tenantId, resourceType, additionalAmount, actionContext)
return { allowed: true }
}
// Reset quotas that are period-based
async resetPeriodicQuotas(): Promise<void> {
const now = new Date()
const tenants = await this.getAllTenants()
for (const tenant of tenants) {
const quota = this.usageTracker.getTenantQuota(tenant.id)
if (!quota) continue
for (const [resourceType, resourceQuota] of quota.quotas.entries()) {
if (resourceQuota.period && resourceQuota.resetDate && now >= resourceQuota.resetDate) {
await this.resetQuota(tenant.id, resourceType)
}
}
}
}
private async handleQuotaExceeded(
tenantId: string,
resourceType: ResourceType,
check: any,
context?: Record<string, any>
): Promise<void> {
await this.alertService.sendQuotaAlert({
tenantId,
resourceType,
alertType: 'EXCEEDED',
currentUsage: check.currentUsage,
limit: check.limit,
percentage: check.percentageUsed,
timestamp: new Date()
})
await this.enforcementActions.executeEnforcementAction(tenantId, resourceType, 'EXCEEDED', context)
}
private async checkApproachingLimits(
tenantId: string,
resourceType: ResourceType,
check: any
): Promise<void> {
const thresholds = this.alertThresholds.get(resourceType)
if (!thresholds) return
const { warning, critical } = thresholds
if (check.percentageUsed >= critical) {
await this.alertService.sendQuotaAlert({
tenantId,
resourceType,
alertType: 'CRITICAL',
currentUsage: check.currentUsage,
limit: check.limit,
percentage: check.percentageUsed,
timestamp: new Date()
})
} else if (check.percentageUsed >= warning) {
await this.alertService.sendQuotaAlert({
tenantId,
resourceType,
alertType: 'WARNING',
currentUsage: check.currentUsage,
limit: check.limit,
percentage: check.percentageUsed,
timestamp: new Date()
})
}
}
private getCurrentUsage(tenantId: string, resourceType: ResourceType, period?: string): number {
if (period === 'monthly') {
return this.usageTracker.getCurrentUsage(tenantId, resourceType, 'monthly')
} else if (period === 'daily') {
return this.usageTracker.getCurrentUsage(tenantId, resourceType, 'daily')
}
return this.usageTracker.getCurrentUsage(tenantId, resourceType)
}
private async resetQuota(tenantId: string, resourceType: ResourceType): Promise<void> {
const quota = this.usageTracker.getTenantQuota(tenantId)
if (!quota) return
const resourceQuota = quota.quotas.get(resourceType)
if (!resourceQuota || !resourceQuota.period) return
// Update reset date for next period
const now = new Date()
let nextResetDate: Date
if (resourceQuota.period === 'monthly') {
nextResetDate = new Date(now.getFullYear(), now.getMonth() + 1, 1)
} else if (resourceQuota.period === 'daily') {
nextResetDate = new Date(now.getTime() + 24 * 60 * 60 * 1000)
} else {
return
}
resourceQuota.resetDate = nextResetDate
console.log(`Reset quota for tenant ${tenantId}, resource ${resourceType}`)
}
private async getAllTenants(): Promise<Array<{ id: string }>> {
// In real implementation, this would query the tenant repository
// For demo, return sample data
return [
{ id: 'tenant-1' },
{ id: 'tenant-2' },
{ id: 'tenant-3' }
]
}
}
// Quota Alert Service
interface QuotaAlertService {
sendQuotaAlert(alert: QuotaAlert): Promise<void>
}
class EmailQuotaAlertService implements QuotaAlertService {
async sendQuotaAlert(alert: QuotaAlert): Promise<void> {
const alertLevel = alert.alertType.toLowerCase()
const subject = `Quota ${alertLevel}: ${alert.resourceType} for tenant ${alert.tenantId}`
console.log(`=== EMAIL ALERT ===`)
console.log(`To: admin@${alert.tenantId}.com`)
console.log(`Subject: ${subject}`)
console.log(`Message: `)
console.log(` Resource: ${alert.resourceType}`)
console.log(` Current Usage: ${alert.currentUsage}`)
console.log(` Limit: ${alert.limit}`)
console.log(` Usage: ${alert.percentage.toFixed(1)}%`)
console.log(` Alert Level: ${alert.alertType}`)
console.log(`================`)
}
}
// Quota Enforcement Actions
interface QuotaEnforcementActions {
getEnforcementAction(tenantId: string, resourceType: ResourceType, percentageUsed: number): Promise<string>
executeEnforcementAction(tenantId: string, resourceType: ResourceType, alertType: string, context?: Record<string, any>): Promise<void>
}
class DefaultQuotaEnforcementActions implements QuotaEnforcementActions {
async getEnforcementAction(tenantId: string, resourceType: ResourceType, percentageUsed: number): Promise<string> {
if (percentageUsed >= 100) {
switch (resourceType) {
case ResourceType.API_CALLS:
return 'THROTTLE_API'
case ResourceType.STORAGE:
return 'BLOCK_UPLOADS'
case ResourceType.USERS:
return 'BLOCK_USER_CREATION'
case ResourceType.BANDWIDTH:
return 'THROTTLE_BANDWIDTH'
default:
return 'BLOCK_ACTION'
}
}
return 'ALLOW_WITH_WARNING'
}
async executeEnforcementAction(
tenantId: string,
resourceType: ResourceType,
alertType: string,
context?: Record<string, any>
): Promise<void> {
const action = await this.getEnforcementAction(tenantId, resourceType, 100)
console.log(`=== ENFORCEMENT ACTION ===`)
console.log(`Tenant: ${tenantId}`)
console.log(`Resource: ${resourceType}`)
console.log(`Alert Type: ${alertType}`)
console.log(`Action: ${action}`)
switch (action) {
case 'THROTTLE_API':
console.log(`→ API requests will be throttled for tenant ${tenantId}`)
break
case 'BLOCK_UPLOADS':
console.log(`→ File uploads blocked for tenant ${tenantId}`)
break
case 'BLOCK_USER_CREATION':
console.log(`→ New user creation blocked for tenant ${tenantId}`)
break
case 'THROTTLE_BANDWIDTH':
console.log(`→ Bandwidth throttled for tenant ${tenantId}`)
break
case 'BLOCK_ACTION':
console.log(`→ Action blocked for tenant ${tenantId}`)
break
}
console.log(`=========================`)
}
}
// Quota Analytics Dashboard
class QuotaAnalytics {
constructor(private usageTracker: ResourceUsageTracker) {}
// Get quota utilization report
async getQuotaUtilizationReport(tenantId: string): Promise<{
resourceUtilization: Array<{
resourceType: ResourceType
current: number
limit: number
percentage: number
status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'EXCEEDED'
}>
overallHealth: 'HEALTHY' | 'WARNING' | 'CRITICAL'
recommendations: string[]
}> {
const quota = this.usageTracker.getTenantQuota(tenantId)
if (!quota) {
throw new Error(`No quota found for tenant ${tenantId}`)
}
const resourceUtilization: Array<{
resourceType: ResourceType
current: number
limit: number
percentage: number
status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'EXCEEDED'
}> = []
for (const [resourceType, resourceQuota] of quota.quotas.entries()) {
const current = this.usageTracker.getCurrentUsage(
tenantId,
resourceType,
resourceQuota.period as any
)
const percentage = (current / resourceQuota.limit) * 100
let status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'EXCEEDED'
if (percentage >= 100) {
status = 'EXCEEDED'
} else if (percentage >= 95) {
status = 'CRITICAL'
} else if (percentage >= 80) {
status = 'WARNING'
} else {
status = 'HEALTHY'
}
resourceUtilization.push({
resourceType,
current,
limit: resourceQuota.limit,
percentage,
status
})
}
const overallHealth = this.calculateOverallHealth(resourceUtilization)
const recommendations = this.generateRecommendations(resourceUtilization)
return {
resourceUtilization,
overallHealth,
recommendations
}
}
// Get capacity planning insights
async getCapacityPlanningInsights(days: number = 30): Promise<{
resourceGrowth: Record<ResourceType, number>
tenantGrowth: Record<string, number>
predictedExhaustion: Array<{
tenantId: string
resourceType: ResourceType
estimatedDaysToExhaustion: number
}>
}> {
const resourceGrowth: Record<string, number> = {}
const tenantGrowth: Record<string, number> = {}
const predictedExhaustion: Array<{
tenantId: string
resourceType: ResourceType
estimatedDaysToExhaustion: number
}> = []
// Calculate growth rates for each resource type
for (const resourceType of Object.values(ResourceType)) {
const trend = this.usageTracker.getUsageTrend('sample-tenant', resourceType as ResourceType, days)
if (trend.length > 1) {
const growth = ((trend[trend.length - 1].usage - trend[0].usage) / trend[0].usage) * 100
resourceGrowth[resourceType] = growth
}
}
// Predict exhaustion for tenants approaching limits
// This is a simplified prediction algorithm
const sampleTenants = ['tenant-1', 'tenant-2', 'tenant-3']
for (const tenantId of sampleTenants) {
const quota = this.usageTracker.getTenantQuota(tenantId)
if (!quota) continue
for (const [resourceType, resourceQuota] of quota.quotas.entries()) {
const current = this.usageTracker.getCurrentUsage(tenantId, resourceType, resourceQuota.period as any)
const remaining = resourceQuota.limit - current
if (remaining > 0) {
const trend = this.usageTracker.getUsageTrend(tenantId, resourceType, 7) // Last 7 days
if (trend.length > 1) {
const dailyUsage = (trend[trend.length - 1].usage - trend[0].usage) / trend.length
if (dailyUsage > 0) {
const daysToExhaustion = Math.floor(remaining / dailyUsage)
if (daysToExhaustion < 30) { // Only show if exhausted within 30 days
predictedExhaustion.push({
tenantId,
resourceType,
estimatedDaysToExhaustion: daysToExhaustion
})
}
}
}
}
}
}
return {
resourceGrowth,
tenantGrowth,
predictedExhaustion
}
}
private calculateOverallHealth(utilization: any[]): 'HEALTHY' | 'WARNING' | 'CRITICAL' {
const criticalCount = utilization.filter(u => u.status === 'CRITICAL' || u.status === 'EXCEEDED').length
const warningCount = utilization.filter(u => u.status === 'WARNING').length
if (criticalCount > 0) {
return 'CRITICAL'
} else if (warningCount > 0) {
return 'WARNING'
} else {
return 'HEALTHY'
}
}
private generateRecommendations(utilization: any[]): string[] {
const recommendations: string[] = []
for (const util of utilization) {
if (util.percentage >= 80) {
switch (util.resourceType) {
case ResourceType.STORAGE:
recommendations.push('Consider upgrading your plan or archiving old files to free up storage space.')
break
case ResourceType.API_CALLS:
recommendations.push('Optimize API usage or upgrade to a plan with higher API limits.')
break
case ResourceType.USERS:
recommendations.push('Upgrade your plan to accommodate more users or remove inactive accounts.')
break
case ResourceType.BANDWIDTH:
recommendations.push('Monitor bandwidth usage and consider upgrading for higher limits.')
break
}
}
}
if (recommendations.length === 0) {
recommendations.push('Your resource usage is within healthy limits.')
}
return recommendations
}
}
// Usage Example
async function resourceQuotaExample() {
console.log('=== Resource Quota Management System ===\n')
// Initialize services
const usageTracker = new ResourceUsageTracker()
const alertService = new EmailQuotaAlertService()
const enforcementActions = new DefaultQuotaEnforcementActions()
const quotaEngine = new QuotaEnforcementEngine(usageTracker, alertService, enforcementActions)
const analytics = new QuotaAnalytics(usageTracker)
// Set up tenant quotas
console.log('1. Setting up tenant quotas')
const tenantQuotas = new Map<ResourceType, ResourceQuota>([
[ResourceType.USERS, { resourceType: ResourceType.USERS, limit: 100, current: 0, unit: 'users' }],
[ResourceType.STORAGE, { resourceType: ResourceType.STORAGE, limit: 1000, current: 0, unit: 'MB', period: 'monthly' }],
[ResourceType.API_CALLS, { resourceType: ResourceType.API_CALLS, limit: 10000, current: 0, unit: 'calls', period: 'monthly' }],
[ResourceType.PROJECTS, { resourceType: ResourceType.PROJECTS, limit: 50, current: 0, unit: 'projects' }]
])
usageTracker.setTenantQuota('tenant-demo', tenantQuotas)
// Simulate usage
console.log('\n2. Simulating resource usage')
// Add some users
for (let i = 0; i < 75; i++) {
await quotaEngine.enforceQuota('tenant-demo', ResourceType.USERS, 1, { action: 'create_user' })
}
// Simulate API calls (gradually approaching limit)
for (let i = 0; i < 8500; i++) {
await quotaEngine.enforceQuota('tenant-demo', ResourceType.API_CALLS, 1, { endpoint: '/api/data' })
}
// Add storage usage
for (let i = 0; i < 850; i++) {
await quotaEngine.enforceQuota('tenant-demo', ResourceType.STORAGE, 1, { file_size: 1024 * 1024 })
}
// Check quota status
console.log('\n3. Current quota status')
const currentQuota = usageTracker.getTenantQuota('tenant-demo')!
for (const [resourceType, quota] of currentQuota.quotas.entries()) {
const current = usageTracker.getCurrentUsage('tenant-demo', resourceType, quota.period as any)
const percentage = (current / quota.limit) * 100
console.log(`${resourceType}: ${current}/${quota.limit} (${percentage.toFixed(1)}%)`)
}
// Test quota enforcement
console.log('\n4. Testing quota enforcement')
const userCheck = quotaEngine.canPerformAction('tenant-demo', ResourceType.USERS, 30)
console.log('Can add 30 users:', userCheck.allowed, '- Remaining:', userCheck.remaining)
const apiCheck = quotaEngine.canPerformAction('tenant-demo', ResourceType.API_CALLS, 2000)
console.log('Can make 2000 API calls:', apiCheck.allowed, '- Remaining:', apiCheck.remaining)
// Try to exceed quota
console.log('\n5. Attempting to exceed quotas')
try {
await quotaEngine.enforceQuota('tenant-demo', ResourceType.API_CALLS, 2000, { endpoint: '/api/bulk-data' })
console.log('Bulk API call succeeded')
} catch (error) {
console.log('Bulk API call blocked:', error)
}
// Generate quota report
console.log('\n6. Quota utilization report')
const report = await analytics.getQuotaUtilizationReport('tenant-demo')
console.log('Overall health:', report.overallHealth)
console.log('\nResource utilization:')
report.resourceUtilization.forEach(util => {
console.log(` ${util.resourceType}: ${util.percentage.toFixed(1)}% (${util.status})`)
})
if (report.recommendations.length > 0) {
console.log('\nRecommendations:')
report.recommendations.forEach(rec => console.log(` • ${rec}`))
}
// Capacity planning insights
console.log('\n7. Capacity planning insights')
const insights = await analytics.getCapacityPlanningInsights(30)
console.log('Resource growth rates:')
Object.entries(insights.resourceGrowth).forEach(([resource, growth]) => {
console.log(` ${resource}: ${growth.toFixed(1)}%`)
})
if (insights.predictedExhaustion.length > 0) {
console.log('\nResources approaching limits:')
insights.predictedExhaustion.forEach(pred => {
console.log(` ${pred.tenantId} - ${pred.resourceType}: ${pred.estimatedDaysToExhaustion} days`)
})
} else {
console.log('\nNo resources predicted to exhaust within 30 days')
}
}
export {
ResourceType,
ResourceQuota,
TenantQuota,
UsageRecord,
QuotaAlert,
ResourceUsageTracker,
QuotaEnforcementEngine,
QuotaAlertService,
EmailQuotaAlertService,
QuotaEnforcementActions,
DefaultQuotaEnforcementActions,
QuotaAnalytics,
resourceQuotaExample
}
💻 White-Label Lösungen
🔴 complex
⭐⭐⭐⭐⭐
Implementierung anpassbarer White-Label Lösungen mit Branding, Themes und Multi-Marke-Unterstützung
⏱️ 70 min
🏷️ white-label-solutions, brand-customization, theme-management
Prerequisites:
Frontend development knowledge, CSS/JavaScript understanding
// White-Label Solution Management System
// Brand Configuration
interface BrandConfig {
tenantId: string
brandId: string
name: string
domain: string
logo: {
primary: string
secondary?: string
favicon?: string
emailLogo?: string
}
colors: {
primary: string
secondary: string
accent: string
background: string
text: string
error: string
success: string
warning: string
}
typography: {
fontFamily: string
headingFont?: string
baseFontSize: string
scale: number[]
}
layout: {
headerHeight: string
sidebarWidth: string
borderRadius: string
spacing: {
small: string
medium: string
large: string
}
}
customCSS?: string
customJS?: string
features: {
showBranding: boolean
allowCustomDomain: boolean
customLogin: boolean
customEmails: boolean
whiteLabelReports: boolean
}
integration: {
analytics: AnalyticsConfig
support: SupportConfig
notifications: NotificationConfig
}
createdAt: Date
updatedAt: Date
}
interface AnalyticsConfig {
provider: 'google' | 'mixpanel' | 'segment' | 'custom'
trackingId: string
customScript?: string
}
interface SupportConfig {
provider: 'intercom' | 'zendesk' | 'custom'
apiKey: string
customWidget?: string
}
interface NotificationConfig {
emailProvider: 'sendgrid' | 'ses' | 'mailgun' | 'custom'
smsProvider: 'twilio' | 'custom'
templates: EmailTemplates
}
interface EmailTemplates {
welcome: EmailTemplate
passwordReset: EmailTemplate
invitation: EmailTemplate
billing: EmailTemplate
support: EmailTemplate
}
interface EmailTemplate {
subject: string
htmlContent: string
textContent?: string
variables: string[]
}
// Theme Management Service
class ThemeManagementService {
private themes: Map<string, BrandConfig> = new Map()
private defaultTheme: BrandConfig
constructor() {
this.defaultTheme = this.createDefaultTheme()
}
// Create or update brand configuration
async saveBrandConfig(brandConfig: BrandConfig): Promise<BrandConfig> {
// Validate brand configuration
this.validateBrandConfig(brandConfig)
// Generate CSS theme
const themeCSS = this.generateThemeCSS(brandConfig)
// Store the configuration
this.themes.set(brandConfig.brandId, {
...brandConfig,
updatedAt: new Date()
})
console.log(`Saved brand config for ${brandConfig.name} (${brandConfig.brandId})`)
return brandConfig
}
// Get brand configuration
getBrandConfig(brandId: string): BrandConfig | null {
return this.themes.get(brandId) || null
}
// Get brand configuration by domain
getBrandConfigByDomain(domain: string): BrandConfig | null {
for (const brand of this.themes.values()) {
if (brand.domain === domain) {
return brand
}
}
return null
}
// Generate CSS theme from brand configuration
generateThemeCSS(brandConfig: BrandConfig): string {
const { colors, typography, layout } = brandConfig
return `
/* Custom Theme for ${brandConfig.name} */
:root {
/* Colors */
--primary-color: ${colors.primary};
--primary-hover: ${this.adjustColor(colors.primary, -10)};
--primary-light: ${this.adjustColor(colors.primary, 80)};
--secondary-color: ${colors.secondary};
--accent-color: ${colors.accent};
--background-color: ${colors.background};
--text-color: ${colors.text};
--text-muted: ${this.adjustColor(colors.text, 60)};
--border-color: ${this.adjustColor(colors.text, 20)};
--error-color: ${colors.error};
--success-color: ${colors.success};
--warning-color: ${colors.warning};
/* Typography */
--font-family: ${typography.fontFamily};
--heading-font: ${typography.headingFont || typography.fontFamily};
--font-size-base: ${typography.baseFontSize};
--font-scale: ${typography.scale.join(', ')};
/* Layout */
--header-height: ${layout.headerHeight};
--sidebar-width: ${layout.sidebarWidth};
--border-radius: ${layout.borderRadius};
--spacing-small: ${layout.spacing.small};
--spacing-medium: ${layout.spacing.medium};
--spacing-large: ${layout.spacing.large};
}
/* Component Styles */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
.card {
background-color: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
height: var(--header-height);
background-color: var(--primary-color);
color: white;
}
.sidebar {
width: var(--sidebar-width);
background-color: var(--background-color);
border-right: 1px solid var(--border-color);
}
.text-primary { color: var(--primary-color); }
.text-secondary { color: var(--secondary-color); }
.text-accent { color: var(--accent-color); }
.text-muted { color: var(--text-muted); }
.bg-primary { background-color: var(--primary-color); }
.bg-secondary { background-color: var(--secondary-color); }
.bg-accent { background-color: var(--accent-color); }
/* Custom styles */
${brandConfig.customCSS || ''}
`
}
// Generate theme JavaScript for dynamic styling
generateThemeJS(brandConfig: BrandConfig): string {
return `
// Dynamic Theme for ${brandConfig.name}
(function() {
const theme = ${JSON.stringify(brandConfig, null, 2)};
// Apply theme dynamically
function applyTheme() {
const root = document.documentElement;
// Set CSS custom properties
Object.entries(theme.colors).forEach(([key, value]) => {
const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}-color`;
root.style.setProperty(cssVar, value);
});
// Set custom fonts
if (theme.typography.fontFamily) {
root.style.setProperty('--font-family', theme.typography.fontFamily);
}
// Update logos
const logoElements = document.querySelectorAll('[data-brand-logo]');
logoElements.forEach(el => {
el.src = theme.logo.primary;
el.alt = theme.name;
});
// Update favicon
if (theme.logo.favicon) {
const favicon = document.querySelector('link[rel="icon"]') || document.createElement('link');
favicon.rel = 'icon';
favicon.href = theme.logo.favicon;
document.head.appendChild(favicon);
}
// Update page title
if (document.title.includes('[BRAND_NAME]')) {
document.title = document.title.replace('[BRAND_NAME]', theme.name);
}
}
// Apply theme immediately
applyTheme();
// Re-apply theme on DOM changes
const observer = new MutationObserver(applyTheme);
observer.observe(document.body, { childList: true, subtree: true });
// Export theme for other components
window.currentTheme = theme;
})();
${brandConfig.customJS || ''}
`
}
// Clone existing theme
cloneTheme(sourceBrandId: string, newBrandId: string, overrides: Partial<BrandConfig>): BrandConfig {
const sourceTheme = this.themes.get(sourceBrandId)
if (!sourceTheme) {
throw new Error(`Source theme not found: ${sourceBrandId}`)
}
const clonedTheme: BrandConfig = {
...sourceTheme,
brandId: newBrandId,
...overrides,
createdAt: new Date(),
updatedAt: new Date()
}
return clonedTheme
}
// Reset theme to default
resetToDefault(brandId: string): BrandConfig {
const currentTheme = this.themes.get(brandId)
if (!currentTheme) {
throw new Error(`Theme not found: ${brandId}`)
}
const defaultTheme: BrandConfig = {
...this.defaultTheme,
brandId: currentTheme.brandId,
tenantId: currentTheme.tenantId,
name: currentTheme.name,
domain: currentTheme.domain
}
this.themes.set(brandId, defaultTheme)
return defaultTheme
}
// Export theme configuration
exportTheme(brandId: string): string {
const theme = this.themes.get(brandId)
if (!theme) {
throw new Error(`Theme not found: ${brandId}`)
}
return JSON.stringify(theme, null, 2)
}
// Import theme configuration
importTheme(themeJson: string): BrandConfig {
try {
const theme = JSON.parse(themeJson) as BrandConfig
this.validateBrandConfig(theme)
this.themes.set(theme.brandId, theme)
return theme
} catch (error) {
throw new Error(`Invalid theme configuration: ${error}`)
}
}
private validateBrandConfig(brandConfig: BrandConfig): void {
if (!brandConfig.brandId || !brandConfig.tenantId || !brandConfig.name) {
throw new Error('Required fields missing: brandId, tenantId, name')
}
// Validate color format (hex)
const colorFields = Object.values(brandConfig.colors)
for (const color of colorFields) {
if (!this.isValidHexColor(color)) {
throw new Error(`Invalid color format: ${color}`)
}
}
// Validate logo URLs
if (!this.isValidUrl(brandConfig.logo.primary)) {
throw new Error(`Invalid primary logo URL: ${brandConfig.logo.primary}`)
}
}
private createDefaultTheme(): BrandConfig {
return {
tenantId: 'default',
brandId: 'default',
name: 'Default Brand',
domain: 'app.example.com',
logo: {
primary: '/assets/default-logo.png',
secondary: '/assets/default-logo-light.png',
favicon: '/assets/default-favicon.ico',
emailLogo: '/assets/default-email-logo.png'
},
colors: {
primary: '#007bff',
secondary: '#6c757d',
accent: '#28a745',
background: '#ffffff',
text: '#212529',
error: '#dc3545',
success: '#28a745',
warning: '#ffc107'
},
typography: {
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
baseFontSize: '16px',
scale: [0.75, 0.875, 1, 1.125, 1.25, 1.5, 1.875, 2.25, 3]
},
layout: {
headerHeight: '64px',
sidebarWidth: '250px',
borderRadius: '4px',
spacing: {
small: '8px',
medium: '16px',
large: '24px'
}
},
features: {
showBranding: true,
allowCustomDomain: false,
customLogin: false,
customEmails: false,
whiteLabelReports: false
},
integration: {
analytics: {
provider: 'google',
trackingId: 'GA-DEFAULT'
},
support: {
provider: 'intercom',
apiKey: 'default-key'
},
notifications: {
emailProvider: 'sendgrid',
smsProvider: 'twilio',
templates: this.getDefaultEmailTemplates()
}
},
createdAt: new Date(),
updatedAt: new Date()
}
}
private getDefaultEmailTemplates(): EmailTemplates {
return {
welcome: {
subject: 'Welcome to {{brand_name}}!',
htmlContent: `
<h1>Welcome to {{brand_name}}!</h1>
<p>Thank you for joining us. Your account has been successfully created.</p>
<p>Click <a href="{{login_url}}">here</a> to log in.</p>
`,
variables: ['brand_name', 'login_url', 'user_name']
},
passwordReset: {
subject: 'Password Reset Request',
htmlContent: `
<h1>Password Reset</h1>
<p>You requested a password reset for your {{brand_name}} account.</p>
<p>Click <a href="{{reset_url}}">here</a> to reset your password.</p>
`,
variables: ['brand_name', 'reset_url', 'user_name']
},
invitation: {
subject: 'Invitation to join {{brand_name}}',
htmlContent: `
<h1>You're Invited!</h1>
<p>You have been invited to join {{brand_name}}.</p>
<p>Click <a href="{{invite_url}}">here</a> to accept the invitation.</p>
`,
variables: ['brand_name', 'invite_url', 'inviter_name', 'team_name']
},
billing: {
subject: 'Invoice from {{brand_name}}',
htmlContent: `
<h1>Invoice {{invoice_number}}</h1>
<p>Your invoice from {{brand_name}} is ready.</p>
<p>Amount: {{amount}} {{currency}}</p>
<p>Due date: {{due_date}}</p>
`,
variables: ['brand_name', 'invoice_number', 'amount', 'currency', 'due_date']
},
support: {
subject: 'Support Ticket Update - {{ticket_id}}',
htmlContent: `
<h1>Support Ticket Update</h1>
<p>Your support ticket {{ticket_id}} has been updated.</p>
<p>Message: {{message}}</p>
`,
variables: ['brand_name', 'ticket_id', 'message', 'support_agent']
}
}
}
private isValidHexColor(color: string): boolean {
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color)
}
private isValidUrl(url: string): boolean {
try {
new URL(url)
return true
} catch {
return false
}
}
private adjustColor(color: string, amount: number): string {
const num = parseInt(color.replace('#', ''), 16)
const r = Math.min(255, Math.max(0, (num >> 16) + amount))
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amount))
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + amount))
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
}
}
// Brand Middleware for Web Applications
class BrandMiddleware {
constructor(private themeService: ThemeManagementService) {}
// Express.js middleware example
expressMiddleware() {
return (req: any, res: any, next: any) => {
const domain = req.hostname
const brand = this.themeService.getBrandConfigByDomain(domain)
if (brand) {
req.brand = brand
res.locals.brand = brand
// Set brand-specific headers
res.setHeader('X-Brand-ID', brand.brandId)
res.setHeader('X-Brand-Name', brand.name)
// Inject brand variables into templates
res.locals.theme = {
css: this.themeService.generateThemeCSS(brand),
js: this.themeService.generateThemeJS(brand)
}
} else {
// Use default theme
req.brand = this.themeService.getBrandConfig('default')
res.locals.brand = req.brand
res.locals.theme = {
css: this.themeService.generateThemeCSS(req.brand),
js: this.themeService.generateThemeJS(req.brand)
}
}
next()
}
}
// Generate HTML with brand styling
generateBrandedHTML(template: string, brand: BrandConfig): string {
const themeCSS = this.themeService.generateThemeCSS(brand)
const themeJS = this.themeService.generateThemeJS(brand)
return template
.replace('{{BRAND_CSS}}', themeCSS)
.replace('{{BRAND_JS}}', themeJS)
.replace('{{BRAND_NAME}}', brand.name)
.replace('{{BRAND_LOGO}}', brand.logo.primary)
.replace('{{FAVICON}}', brand.logo.favicon || '')
.replace('{{PRIMARY_COLOR}}', brand.colors.primary)
.replace('{{SECONDARY_COLOR}}', brand.colors.secondary)
}
}
// Email Template Service for White-Label
class WhiteLabelEmailService {
constructor(private themeService: ThemeManagementService) {}
// Generate branded email template
generateBrandedEmail(
brandId: string,
templateType: keyof EmailTemplates,
variables: Record<string, any>
): {
subject: string
htmlContent: string
textContent?: string
} {
const brand = this.themeService.getBrandConfig(brandId)
if (!brand) {
throw new Error(`Brand not found: ${brandId}`)
}
const template = brand.integration.notifications.templates[templateType]
// Replace template variables
let subject = template.subject
let htmlContent = template.htmlContent
let textContent = template.textContent
for (const [key, value] of Object.entries(variables)) {
const placeholder = `{{${key}}}`
subject = subject.replace(new RegExp(placeholder, 'g'), String(value))
htmlContent = htmlContent.replace(new RegExp(placeholder, 'g'), String(value))
if (textContent) {
textContent = textContent.replace(new RegExp(placeholder, 'g'), String(value))
}
}
// Add brand styling to email
const brandedHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${subject}</title>
<style>
.email-header {
background-color: ${brand.colors.primary};
color: white;
padding: 20px;
text-align: center;
}
.email-body {
font-family: ${brand.typography.fontFamily};
color: ${brand.colors.text};
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.email-footer {
background-color: ${brand.colors.background};
border-top: 1px solid ${brand.colors.secondary};
padding: 20px;
text-align: center;
color: ${brand.colors.text};
font-size: 14px;
}
.brand-logo {
max-height: 40px;
margin-bottom: 10px;
}
.btn-primary {
background-color: ${brand.colors.primary};
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: ${brand.layout.borderRadius};
display: inline-block;
}
</style>
</head>
<body>
<div class="email-header">
<img src="${brand.logo.emailLogo}" alt="${brand.name}" class="brand-logo">
<h1>${brand.name}</h1>
</div>
<div class="email-body">
${htmlContent}
</div>
<div class="email-footer">
<p>© ${new Date().getFullYear()} ${brand.name}. All rights reserved.</p>
<p>This email was sent to {{recipient_email}}</p>
</div>
</body>
</html>
`
return {
subject,
htmlContent: brandedHTML,
textContent
}
}
// Send branded email
async sendBrandedEmail(
brandId: string,
to: string[],
templateType: keyof EmailTemplates,
variables: Record<string, any>
): Promise<void> {
const email = this.generateBrandedEmail(brandId, templateType, variables)
console.log(`=== SENDING BRANDED EMAIL ===`)
console.log(`Brand: ${brandId}`)
console.log(`To: ${to.join(', ')}`)
console.log(`Subject: ${email.subject}`)
console.log(`Template: ${templateType}`)
// In real implementation, this would integrate with email service
// await emailProvider.send(email)
console.log(`Email sent successfully`)
console.log(`=========================`)
}
}
// Custom Domain Management
class CustomDomainService {
private customDomains: Map<string, { tenantId: string; brandId: string; ssl: boolean; verified: boolean }> = new Map()
// Add custom domain
async addCustomDomain(tenantId: string, brandId: string, domain: string): Promise<void> {
if (this.customDomains.has(domain)) {
throw new Error(`Domain ${domain} is already registered`)
}
// Validate domain
if (!this.isValidDomain(domain)) {
throw new Error(`Invalid domain: ${domain}`)
}
// Generate DNS records for verification
const dnsRecords = this.generateDNSRecords(domain, tenantId)
console.log(`=== CUSTOM DOMAIN SETUP ===`)
console.log(`Domain: ${domain}`)
console.log(`Tenant: ${tenantId}`)
console.log(`Brand: ${brandId}`)
console.log(`\nDNS Records to configure:`)
console.log(`CNAME: ${domain} -> ${dnsRecords.cname}`)
console.log(`TXT: ${dnsRecords.txt.name} -> ${dnsRecords.txt.value}`)
console.log(`========================`)
// Store domain (pending verification)
this.customDomains.set(domain, {
tenantId,
brandId,
ssl: false,
verified: false
})
}
// Verify custom domain
async verifyCustomDomain(domain: string): Promise<boolean> {
const domainConfig = this.customDomains.get(domain)
if (!domainConfig) {
throw new Error(`Domain not found: ${domain}`)
}
// Simulate DNS verification
console.log(`Verifying domain: ${domain}`)
// In real implementation, this would check DNS records
const isVerified = Math.random() > 0.2 // 80% success rate for demo
if (isVerified) {
domainConfig.verified = true
domainConfig.ssl = true // Auto-provision SSL
console.log(`Domain ${domain} verified successfully!`)
} else {
console.log(`Domain ${domain} verification failed. Please check DNS records.`)
}
return isVerified
}
// Get domain configuration
getDomainConfig(domain: string) {
return this.customDomains.get(domain) || null
}
// Get all domains for tenant
getTenantDomains(tenantId: string): Array<{
domain: string
brandId: string
verified: boolean
ssl: boolean
}> {
const domains = []
for (const [domain, config] of this.customDomains.entries()) {
if (config.tenantId === tenantId) {
domains.push({
domain,
brandId: config.brandId,
verified: config.verified,
ssl: config.ssl
})
}
}
return domains
}
private isValidDomain(domain: string): boolean {
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$/;
return domainRegex.test(domain)
}
private generateDNSRecords(domain: string, tenantId: string): {
cname: string
txt: { name: string; value: string }
} {
const verificationToken = crypto.randomUUID().replace(/-/g, '')
return {
cname: `app.example.com`,
txt: {
name: `_verification-challenge.${domain}`,
value: `tenant-${tenantId}-${verificationToken}`
}
}
}
}
// Usage Example
async function whiteLabelExample() {
console.log('=== White-Label Solution Management ===\n')
// Initialize services
const themeService = new ThemeManagementService()
const brandMiddleware = new BrandMiddleware(themeService)
const emailService = new WhiteLabelEmailService(themeService)
const domainService = new CustomDomainService()
// Create custom brand configuration
console.log('1. Creating custom brand configuration')
const techCorpBrand: BrandConfig = {
tenantId: 'tech-corp-123',
brandId: 'tech-corp-brand',
name: 'TechCorp Analytics',
domain: 'analytics.techcorp.com',
logo: {
primary: 'https://assets.techcorp.com/logo-primary.png',
secondary: 'https://assets.techcorp.com/logo-light.png',
favicon: 'https://assets.techcorp.com/favicon.ico',
emailLogo: 'https://assets.techcorp.com/email-logo.png'
},
colors: {
primary: '#2563eb',
secondary: '#64748b',
accent: '#10b981',
background: '#ffffff',
text: '#1f2937',
error: '#ef4444',
success: '#10b981',
warning: '#f59e0b'
},
typography: {
fontFamily: '"Inter", sans-serif',
headingFont: '"Inter Display", sans-serif',
baseFontSize: '16px',
scale: [0.75, 0.875, 1, 1.125, 1.25, 1.5, 1.875, 2.25, 3]
},
layout: {
headerHeight: '72px',
sidebarWidth: '280px',
borderRadius: '8px',
spacing: {
small: '12px',
medium: '20px',
large: '32px'
}
},
customCSS: `
.tech-corp-button {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
border: none;
transition: all 0.3s ease;
}
.tech-corp-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
`,
features: {
showBranding: false,
allowCustomDomain: true,
customLogin: true,
customEmails: true,
whiteLabelReports: true
},
integration: {
analytics: {
provider: 'segment',
trackingId: 'seg_techcorp_123abc'
},
support: {
provider: 'zendesk',
apiKey: 'zendesk_techcorp_key'
},
notifications: {
emailProvider: 'sendgrid',
smsProvider: 'twilio',
templates: themeService['getDefaultEmailTemplates']()
}
},
createdAt: new Date(),
updatedAt: new Date()
}
await themeService.saveBrandConfig(techCorpBrand)
// Create second brand (agency model)
console.log('\n2. Creating agency brand (cloned from first)')
const agencyBrand = themeService.cloneTheme('tech-corp-brand', 'digital-agency-brand', {
tenantId: 'digital-agency-456',
name: 'Digital Agency Pro',
domain: 'pro.digitalagency.io',
colors: {
primary: '#7c3aed',
secondary: '#6b7280',
accent: '#ec4899',
background: '#ffffff',
text: '#111827',
error: '#dc2626',
success: '#059669',
warning: '#d97706'
},
logo: {
primary: 'https://assets.digitalagency.io/logo.png',
favicon: 'https://assets.digitalagency.io/favicon.ico'
}
})
await themeService.saveBrandConfig(agencyBrand)
// Generate theme files
console.log('\n3. Generating theme assets')
const techCorpCSS = themeService.generateThemeCSS(techCorpBrand)
const techCorpJS = themeService.generateThemeJS(techCorpBrand)
console.log(`Generated CSS for ${techCorpBrand.name} (${techCorpCSS.length} chars)`)
console.log(`Generated JS for ${techCorpBrand.name} (${techCorpJS.length} chars)`)
// Template rendering example
console.log('\n4. Template rendering example')
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<title>{{BRAND_NAME}} - Dashboard</title>
<link rel="icon" href="{{FAVICON}}">
<style>{{BRAND_CSS}}</style>
</head>
<body>
<header class="header">
<img src="{{BRAND_LOGO}}" alt="{{BRAND_NAME}}" height="32">
</header>
<main>
<h1 class="text-primary">Welcome to {{BRAND_NAME}}</h1>
<p>Your customized dashboard experience.</p>
</main>
<script>{{BRAND_JS}}</script>
</body>
</html>
`
const brandedHTML = brandMiddleware.generateBrandedHTML(htmlTemplate, techCorpBrand)
console.log(`Generated branded HTML for ${techCorpBrand.name} (${brandedHTML.length} chars)`)
// Branded email example
console.log('\n5. Branded email generation')
const welcomeEmail = emailService.generateBrandedEmail('tech-corp-brand', 'welcome', {
brand_name: techCorpBrand.name,
login_url: 'https://analytics.techcorp.com/login',
user_name: 'John Doe'
})
console.log(`Generated welcome email subject: ${welcomeEmail.subject}`)
// Send branded email
await emailService.sendBrandedEmail(
'tech-corp-brand',
['[email protected]'],
'welcome',
{
brand_name: techCorpBrand.name,
login_url: 'https://analytics.techcorp.com/login',
user_name: 'John Doe',
recipient_email: '[email protected]'
}
)
// Custom domain setup
console.log('\n6. Custom domain configuration')
await domainService.addCustomDomain(
'tech-corp-123',
'tech-corp-brand',
'analytics.techcorp.com'
)
await domainService.addCustomDomain(
'digital-agency-456',
'digital-agency-brand',
'clients.pro.digitalagency.io'
)
// Simulate domain verification
await domainService.verifyCustomDomain('analytics.techcorp.com')
// Get domain information
const tenantDomains = domainService.getTenantDomains('tech-corp-123')
console.log('\n7. Tenant domain status')
tenantDomains.forEach(domain => {
console.log(` ${domain.domain}: ${domain.verified ? 'Verified' : 'Pending'} (SSL: ${domain.ssl})`)
})
// Theme analytics
console.log('\n8. Theme management summary')
console.log('Total brands configured:', Array.from(themeService['themes'].keys()).length)
console.log('Custom domains configured:', Array.from(domainService['customDomains'].keys()).length)
}
export {
BrandConfig,
ThemeManagementService,
BrandMiddleware,
WhiteLabelEmailService,
CustomDomainService,
whiteLabelExample
}