/** * Migration: Add Multi-Tenant Support * Adds tenant table and updates user table for multi-tenancy */ 'use strict'; module.exports = { async up(queryInterface, Sequelize) { // Create tenants table await queryInterface.createTable('tenants', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, name: { type: Sequelize.STRING, allowNull: false, comment: 'Human-readable tenant name' }, slug: { type: Sequelize.STRING, allowNull: false, unique: true, comment: 'URL-safe tenant identifier' }, domain: { type: Sequelize.STRING, allowNull: true, comment: 'Custom domain for this tenant' }, subscription_type: { type: Sequelize.ENUM('free', 'basic', 'premium', 'enterprise'), defaultValue: 'basic', comment: 'Subscription tier' }, is_active: { type: Sequelize.BOOLEAN, defaultValue: true, comment: 'Whether tenant is active' }, auth_provider: { type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), defaultValue: 'local', comment: 'Primary authentication provider' }, auth_config: { type: Sequelize.JSONB, allowNull: true, comment: 'Authentication provider configuration' }, user_mapping: { type: Sequelize.JSONB, allowNull: true, comment: 'User attribute mapping from external provider' }, role_mapping: { type: Sequelize.JSONB, allowNull: true, comment: 'Role mapping from external provider to internal roles' }, branding: { type: Sequelize.JSONB, allowNull: true, comment: 'Tenant-specific branding' }, features: { type: Sequelize.JSONB, defaultValue: { max_devices: 10, max_users: 5, api_rate_limit: 1000, data_retention_days: 90, features: ['basic_detection', 'alerts', 'dashboard'] }, comment: 'Tenant feature limits and enabled features' }, admin_email: { type: Sequelize.STRING, allowNull: true, comment: 'Primary admin email for this tenant' }, admin_phone: { type: Sequelize.STRING, allowNull: true, comment: 'Primary admin phone for this tenant' }, billing_email: { type: Sequelize.STRING, allowNull: true }, payment_method_id: { type: Sequelize.STRING, allowNull: true, comment: 'Payment provider customer ID' }, metadata: { type: Sequelize.JSONB, allowNull: true, comment: 'Additional tenant metadata' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Add indexes to tenants table await queryInterface.addIndex('tenants', ['slug'], { unique: true }); await queryInterface.addIndex('tenants', ['domain'], { unique: true, where: { domain: { [Sequelize.Op.ne]: null } } }); await queryInterface.addIndex('tenants', ['is_active']); await queryInterface.addIndex('tenants', ['auth_provider']); // Add tenant-related columns to users table await queryInterface.addColumn('users', 'tenant_id', { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' }, comment: 'Tenant this user belongs to' }); await queryInterface.addColumn('users', 'external_provider', { type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), defaultValue: 'local', comment: 'Authentication provider used for this user' }); await queryInterface.addColumn('users', 'external_id', { type: Sequelize.STRING, allowNull: true, comment: 'User ID from external authentication provider' }); // Add indexes to users table await queryInterface.addIndex('users', ['tenant_id']); await queryInterface.addIndex('users', ['external_provider']); await queryInterface.addIndex('users', ['external_id', 'tenant_id'], { unique: true, name: 'users_external_id_tenant_unique', where: { external_id: { [Sequelize.Op.ne]: null } } }); // Create default tenant for backward compatibility const defaultTenantId = await queryInterface.bulkInsert('tenants', [{ id: Sequelize.literal('gen_random_uuid()'), name: 'Default Organization', slug: 'default', subscription_type: 'enterprise', is_active: true, auth_provider: 'local', features: JSON.stringify({ max_devices: -1, max_users: -1, api_rate_limit: 50000, data_retention_days: -1, features: ['all'] }), created_at: new Date(), updated_at: new Date() }], { returning: true }); // Associate existing users with default tenant await queryInterface.sequelize.query(` UPDATE users SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default') WHERE tenant_id IS NULL `); // Add tenant_id to devices table if it exists try { await queryInterface.addColumn('devices', 'tenant_id', { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' }, comment: 'Tenant this device belongs to' }); await queryInterface.addIndex('devices', ['tenant_id']); // Associate existing devices with default tenant await queryInterface.sequelize.query(` UPDATE devices SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default') WHERE tenant_id IS NULL `); } catch (error) { console.log('Devices table not found or already has tenant_id column'); } // Add tenant_id to alert_rules table if it exists try { await queryInterface.addColumn('alert_rules', 'tenant_id', { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' }, comment: 'Tenant this alert rule belongs to' }); await queryInterface.addIndex('alert_rules', ['tenant_id']); // Associate existing alert rules with default tenant await queryInterface.sequelize.query(` UPDATE alert_rules SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default') WHERE tenant_id IS NULL `); } catch (error) { console.log('Alert_rules table not found or already has tenant_id column'); } console.log('✅ Multi-tenant support added successfully'); console.log('✅ Default tenant created for backward compatibility'); console.log('✅ Existing data associated with default tenant'); }, async down(queryInterface, Sequelize) { // Remove indexes from users table await queryInterface.removeIndex('users', ['tenant_id']); await queryInterface.removeIndex('users', ['external_provider']); await queryInterface.removeIndex('users', 'users_external_id_tenant_unique'); // Remove columns from users table await queryInterface.removeColumn('users', 'tenant_id'); await queryInterface.removeColumn('users', 'external_provider'); await queryInterface.removeColumn('users', 'external_id'); // Remove tenant_id from other tables try { await queryInterface.removeColumn('devices', 'tenant_id'); } catch (error) { console.log('Devices table tenant_id column not found'); } try { await queryInterface.removeColumn('alert_rules', 'tenant_id'); } catch (error) { console.log('Alert_rules table tenant_id column not found'); } // Drop ENUMs await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_users_external_provider"'); await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_auth_provider"'); await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_subscription_type"'); // Drop tenants table await queryInterface.dropTable('tenants'); console.log('✅ Multi-tenant support removed'); } };