/** * 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 (idempotent) try { 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 } }); console.log('✅ Created tenants table'); } catch (error) { if (error.parent?.code === '42P07') { // Table already exists console.log('⚠️ Tenants table already exists, skipping...'); } else { throw error; } } // Add indexes to tenants table (idempotent) try { await queryInterface.addIndex('tenants', ['slug'], { unique: true }); console.log('✅ Added unique index on tenants.slug'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index tenants_slug already exists, skipping...'); } else { throw error; } } try { await queryInterface.addIndex('tenants', ['domain'], { unique: true, where: { domain: { [Sequelize.Op.ne]: null } } }); console.log('✅ Added unique index on tenants.domain'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index tenants_domain already exists, skipping...'); } else { throw error; } } try { await queryInterface.addIndex('tenants', ['is_active']); console.log('✅ Added index on tenants.is_active'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index tenants_is_active already exists, skipping...'); } else { throw error; } } try { await queryInterface.addIndex('tenants', ['auth_provider']); console.log('✅ Added index on tenants.auth_provider'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index tenants_auth_provider already exists, skipping...'); } else { throw error; } } // Add tenant-related columns to users table (idempotent) const tables = await queryInterface.showAllTables(); if (!tables.includes('users')) { console.log('⚠️ Users table does not exist yet, skipping user tenant columns migration...'); } else { const usersTableDescription = await queryInterface.describeTable('users'); if (!usersTableDescription.tenant_id) { await queryInterface.addColumn('users', 'tenant_id', { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' }, comment: 'Tenant this user belongs to' }); console.log('✅ Added tenant_id column to users table'); } else { console.log('⚠️ Column tenant_id already exists in users table, skipping...'); } if (!usersTableDescription.external_provider) { await queryInterface.addColumn('users', 'external_provider', { type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), defaultValue: 'local', comment: 'Authentication provider used for this user' }); console.log('✅ Added external_provider column to users table'); } else { console.log('⚠️ Column external_provider already exists in users table, skipping...'); } if (!usersTableDescription.external_id) { await queryInterface.addColumn('users', 'external_id', { type: Sequelize.STRING, allowNull: true, comment: 'User ID from external authentication provider' }); console.log('✅ Added external_id column to users table'); } else { console.log('⚠️ Column external_id already exists in users table, skipping...'); } // Add indexes to users table (idempotent) try { await queryInterface.addIndex('users', ['tenant_id']); console.log('✅ Added index on users.tenant_id'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index users_tenant_id already exists, skipping...'); } else { throw error; } } try { await queryInterface.addIndex('users', ['external_provider']); console.log('✅ Added index on users.external_provider'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index users_external_provider already exists, skipping...'); } else { throw error; } } try { await queryInterface.addIndex('users', ['external_id', 'tenant_id'], { unique: true, name: 'users_external_id_tenant_unique', where: { external_id: { [Sequelize.Op.ne]: null } } }); console.log('✅ Added unique index on users.external_id + tenant_id'); } catch (error) { if (error.parent?.code === '42P07') { // Index already exists console.log('⚠️ Index users_external_id_tenant_unique already exists, skipping...'); } else { throw error; } } // 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'); } } // Close the else block for users table check 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'); } };