diff --git a/server/migrations/20250820000001-create-initial-tables.js b/server/migrations/20250820000001-create-initial-tables.js index 1b8a425..10c3ce5 100644 --- a/server/migrations/20250820000001-create-initial-tables.js +++ b/server/migrations/20250820000001-create-initial-tables.js @@ -8,6 +8,7 @@ module.exports = { async up(queryInterface, Sequelize) { // Create tenants table first (referenced by other tables) + try { await queryInterface.createTable('tenants', { id: { type: Sequelize.UUID, @@ -36,22 +37,94 @@ module.exports = { allowNull: true, comment: 'Subdomain for multi-tenant routing' }, + subscription_type: { + type: Sequelize.ENUM('free', 'basic', 'premium', 'enterprise'), + defaultValue: 'basic', + allowNull: false, + comment: 'Subscription tier of the tenant' + }, 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, - allowNull: false + defaultValue: Sequelize.NOW }, updated_at: { type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - allowNull: false + 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; + } + } // Create users table await queryInterface.createTable('users', { @@ -82,6 +155,16 @@ module.exports = { defaultValue: 'viewer', allowNull: false }, + external_provider: { + type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), + defaultValue: 'local', + comment: 'Authentication provider used for this user' + }, + external_id: { + type: Sequelize.STRING, + allowNull: true, + comment: 'User ID from external authentication provider' + }, tenant_id: { type: Sequelize.UUID, allowNull: true, diff --git a/server/migrations/20250912000001-add-multi-tenant-support.js b/server/migrations/20250912000001-add-multi-tenant-support.js deleted file mode 100644 index 744a782..0000000 --- a/server/migrations/20250912000001-add-multi-tenant-support.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Migration: Add Multi-Tenant Support - */ - -'use strict'; - -module.exports = { - async up(queryInterface, Sequelize) { - const tableDescription = await queryInterface.describeTable('tenants'); - - // Add essential multi-tenant columns - if (!tableDescription.subscription_type) { - await queryInterface.addColumn('tenants', 'subscription_type', { - type: Sequelize.ENUM('free', 'basic', 'premium', 'enterprise'), - defaultValue: 'basic' - }); - } - - if (!tableDescription.auth_provider) { - await queryInterface.addColumn('tenants', 'auth_provider', { - type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), - defaultValue: 'local' - }); - } - - console.log(' Multi-tenant columns added'); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.removeColumn('tenants', 'auth_provider'); - await queryInterface.removeColumn('tenants', 'subscription_type'); - await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_auth_provider"'); - await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_subscription_type"'); - } -}; diff --git a/server/migrations/20250912000001-add-multi-tenant-support.js_bak b/server/migrations/20250912000001-add-multi-tenant-support.js_bak new file mode 100644 index 0000000..fa566ef --- /dev/null +++ b/server/migrations/20250912000001-add-multi-tenant-support.js_bak @@ -0,0 +1,368 @@ +/** + * 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'); + } +};