/** * Initial Migration: Create all base tables * This migration creates the core database structure */ 'use strict'; module.exports = { async up(queryInterface, Sequelize) { // Create tenants table first (referenced by other tables) try { await queryInterface.createTable('tenants', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true, allowNull: false }, name: { type: Sequelize.STRING, allowNull: false, comment: 'Organization or tenant name' }, slug: { type: Sequelize.STRING, allowNull: false, unique: true, comment: 'URL-friendly identifier' }, domain: { type: Sequelize.STRING, allowNull: true, comment: 'Domain for SSO integration' }, subdomain: { type: Sequelize.STRING, 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 }, 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; } } // Create users table await queryInterface.createTable('users', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true, allowNull: false }, username: { type: Sequelize.STRING, allowNull: false, unique: true }, email: { type: Sequelize.STRING, allowNull: true, validate: { isEmail: true } }, password_hash: { type: Sequelize.STRING, allowNull: false }, first_name: { type: Sequelize.STRING, allowNull: true }, last_name: { type: Sequelize.STRING, allowNull: true }, phone_number: { type: Sequelize.STRING, allowNull: true, comment: 'Phone number for SMS alerts (include country code)' }, role: { type: Sequelize.ENUM('admin', 'operator', 'viewer'), 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' }, sms_alerts_enabled: { type: Sequelize.BOOLEAN, defaultValue: false, comment: 'Whether user wants to receive SMS alerts' }, is_active: { type: Sequelize.BOOLEAN, defaultValue: true, comment: 'Whether user is active' }, email_alerts_enabled: { type: Sequelize.BOOLEAN, defaultValue: true, comment: 'Whether user wants to receive email alerts' }, last_login: { type: Sequelize.DATE, allowNull: true }, timezone: { type: Sequelize.STRING, defaultValue: 'UTC', comment: 'User timezone for alert scheduling' }, tenant_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' }, onUpdate: 'CASCADE', onDelete: 'SET NULL' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false } }); // Create devices table await queryInterface.createTable('devices', { id: { type: Sequelize.STRING(255), primaryKey: true, allowNull: false, comment: 'Unique device identifier' }, name: { type: Sequelize.STRING, allowNull: true, comment: 'Human-readable device name' }, geo_lat: { type: Sequelize.DECIMAL(10, 8), allowNull: true, comment: 'Device latitude coordinate' }, geo_lon: { type: Sequelize.DECIMAL(11, 8), allowNull: true, comment: 'Device longitude coordinate' }, location_description: { type: Sequelize.TEXT, allowNull: true, comment: 'Human-readable location description' }, is_active: { type: Sequelize.BOOLEAN, defaultValue: true, comment: 'Whether the device is currently active' }, last_heartbeat: { type: Sequelize.DATE, allowNull: true, comment: 'Timestamp of last heartbeat received' }, heartbeat_interval: { type: Sequelize.INTEGER, defaultValue: 300, comment: 'Expected heartbeat interval in seconds' }, firmware_version: { type: Sequelize.STRING, allowNull: true, comment: 'Device firmware version' }, installation_date: { type: Sequelize.DATE, allowNull: true, comment: 'When the device was installed' }, notes: { type: Sequelize.TEXT, allowNull: true, comment: 'Additional notes about the device' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false } }); // Create heartbeats table await queryInterface.createTable('heartbeats', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, device_id: { type: Sequelize.STRING(255), allowNull: false, references: { model: 'devices', key: 'id' }, comment: 'ID of the device sending heartbeat' }, tenant_id: { type: Sequelize.UUID, allowNull: true, // Nullable for backward compatibility references: { model: 'tenants', key: 'id' }, }, device_key: { type: Sequelize.STRING, allowNull: true, defaultValue: 'test-device-key', comment: 'Unique key of the sensor from heartbeat message' }, status: { type: Sequelize.STRING, allowNull: true, comment: 'Device status (online, offline, error, etc.)' }, timestamp: { type: Sequelize.DATE, allowNull: true, comment: 'Timestamp from device' }, uptime: { type: Sequelize.BIGINT, allowNull: true, comment: 'Device uptime in seconds' }, memory_usage: { type: Sequelize.FLOAT, allowNull: true, comment: 'Memory usage percentage' }, cpu_usage: { type: Sequelize.FLOAT, allowNull: true, comment: 'CPU usage percentage' }, disk_usage: { type: Sequelize.FLOAT, allowNull: true, comment: 'Disk usage percentage' }, firmware_version: { type: Sequelize.STRING, allowNull: true, comment: 'Firmware version reported in heartbeat' }, received_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, comment: 'When heartbeat was received by server' }, raw_payload: { type: Sequelize.JSON, allowNull: true, comment: 'Complete raw payload received from detector (for debugging)' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create drone_detections table await queryInterface.createTable('drone_detections', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, device_id: { type: Sequelize.STRING(255), allowNull: false, references: { model: 'devices', key: 'id' }, comment: 'ID of the detecting device' }, drone_id: { type: Sequelize.BIGINT, allowNull: false, comment: 'ID of the detected drone' }, drone_type: { type: Sequelize.INTEGER, allowNull: true, comment: 'Type of drone detected' }, rssi: { type: Sequelize.INTEGER, allowNull: true, comment: 'Signal strength in dBm' }, freq: { type: Sequelize.BIGINT, allowNull: true, comment: 'Frequency detected' }, geo_lat: { type: Sequelize.DECIMAL(10, 8), allowNull: true, comment: 'Latitude where detection occurred' }, geo_lon: { type: Sequelize.DECIMAL(11, 8), allowNull: true, comment: 'Longitude where detection occurred' }, device_timestamp: { type: Sequelize.BIGINT, allowNull: true, comment: 'Unix timestamp from the device' }, server_timestamp: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, comment: 'When the detection was received by server' }, confidence_level: { type: Sequelize.DECIMAL(3, 2), allowNull: true, comment: 'Confidence level of detection (0.00-1.00)' }, signal_duration: { type: Sequelize.INTEGER, allowNull: true, comment: 'Duration of signal in milliseconds' }, processed: { type: Sequelize.BOOLEAN, defaultValue: false, comment: 'Whether this detection has been processed for alerts' }, threat_level: { type: Sequelize.STRING, allowNull: true, comment: 'Assessed threat level based on RSSI and drone type' }, estimated_distance: { type: Sequelize.INTEGER, allowNull: true, comment: 'Estimated distance to drone in meters' }, requires_action: { type: Sequelize.BOOLEAN, defaultValue: false, comment: 'Whether this detection requires immediate security action' }, raw_payload: { type: Sequelize.JSON, allowNull: true, comment: 'Complete raw payload received from detector (for debugging)' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create alert_rules table await queryInterface.createTable('alert_rules', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, tenant_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' } }, user_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'users', key: 'id' } }, name: { type: Sequelize.STRING, allowNull: false }, description: { type: Sequelize.TEXT, allowNull: true }, device_ids: { type: Sequelize.JSON, allowNull: true, comment: 'Array of device IDs to monitor (null = all devices)' }, drone_types: { type: Sequelize.JSON, allowNull: true, comment: 'Array of drone types to alert on (null = all types)' }, min_rssi: { type: Sequelize.INTEGER, allowNull: true, comment: 'Minimum RSSI threshold for alert' }, max_rssi: { type: Sequelize.INTEGER, allowNull: true, comment: 'Maximum RSSI threshold for alert' }, frequency_ranges: { type: Sequelize.JSON, allowNull: true, comment: 'Array of frequency ranges to monitor [{min: 20, max: 30}]' }, time_window: { type: Sequelize.INTEGER, defaultValue: 300, comment: 'Time window in seconds to group detections' }, min_detections: { type: Sequelize.INTEGER, defaultValue: 1, comment: 'Minimum number of detections in time window to trigger alert' }, cooldown_period: { type: Sequelize.INTEGER, defaultValue: 600, comment: 'Cooldown period in seconds between alerts for same drone' }, alert_channels: { type: Sequelize.JSON, defaultValue: ['sms'], comment: 'Array of alert channels: sms, email, webhook' }, sms_phone_number: { type: Sequelize.STRING, allowNull: true, comment: 'Phone number for SMS alerts' }, webhook_url: { type: Sequelize.STRING, allowNull: true, comment: 'Webhook URL for custom integrations' }, active_hours: { type: Sequelize.JSON, allowNull: true, comment: 'Active hours for alerts {start: "09:00", end: "17:00"}' }, active_days: { type: Sequelize.JSON, defaultValue: [1, 2, 3, 4, 5, 6, 7], comment: 'Active days of week (1=Monday, 7=Sunday)' }, priority: { type: Sequelize.ENUM('low', 'medium', 'high', 'critical'), defaultValue: 'medium', comment: 'Alert priority level' }, min_threat_level: { type: Sequelize.ENUM('monitoring', 'low', 'medium', 'high', 'critical'), allowNull: true, comment: 'Minimum threat level required to trigger alert' }, is_active: { type: Sequelize.BOOLEAN, defaultValue: true }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create alert_logs table await queryInterface.createTable('alert_logs', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, alert_event_id: { type: Sequelize.UUID, allowNull: true, comment: 'Groups related alerts (SMS, email, webhook) that are part of the same detection event' }, alert_rule_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'alert_rules', key: 'id' } }, detection_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'drone_detections', key: 'id' } }, device_id: { type: Sequelize.STRING(255), allowNull: true, references: { model: 'devices', key: 'id' } }, alert_type: { type: Sequelize.ENUM('sms', 'email', 'webhook', 'push'), allowNull: true, defaultValue: 'sms' }, recipient: { type: Sequelize.STRING, allowNull: true }, message: { type: Sequelize.TEXT, allowNull: false }, status: { type: Sequelize.ENUM('pending', 'sent', 'failed', 'delivered'), defaultValue: 'pending' }, sent_at: { type: Sequelize.DATE, allowNull: true }, delivered_at: { type: Sequelize.DATE, allowNull: true }, error_message: { type: Sequelize.TEXT, allowNull: true }, external_id: { type: Sequelize.STRING, allowNull: true }, cost: { type: Sequelize.DECIMAL(10, 4), allowNull: true }, retry_count: { type: Sequelize.INTEGER, defaultValue: 0 }, priority: { type: Sequelize.ENUM('low', 'normal', 'medium', 'high', 'critical'), defaultValue: 'normal' }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create AuditLogs table await queryInterface.createTable('audit_logs', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, tenant_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'tenants', key: 'id' } }, user_id: { type: Sequelize.UUID, allowNull: true, references: { model: 'users', key: 'id' } }, action: { type: Sequelize.STRING, allowNull: false }, resource_type: { type: Sequelize.STRING, allowNull: true }, resource_id: { type: Sequelize.STRING, allowNull: true }, details: { type: Sequelize.JSON, allowNull: true }, ip_address: { type: Sequelize.INET, allowNull: true }, user_agent: { type: Sequelize.TEXT, allowNull: true }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create management_users table await queryInterface.createTable('management_users', { id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true }, username: { type: Sequelize.STRING, allowNull: false, unique: true }, email: { type: Sequelize.STRING, allowNull: false, unique: true, validate: { isEmail: true } }, first_name: { type: Sequelize.STRING, allowNull: true }, last_name: { type: Sequelize.STRING, allowNull: true }, login_attempts: { type: Sequelize.INTEGER, defaultValue: 0, comment: 'Failed login attempt counter' }, locked_until: { type: Sequelize.DATE, allowNull: true, comment: 'Account lock expiration time' }, two_factor_enabled: { type: Sequelize.BOOLEAN, defaultValue: false, comment: 'Whether 2FA is enabled' }, two_factor_secret: { type: Sequelize.STRING, allowNull: true, comment: 'TOTP secret for 2FA' }, api_access: { type: Sequelize.BOOLEAN, defaultValue: true, comment: 'Whether user can access management API' }, created_by: { type: Sequelize.STRING, allowNull: true, comment: 'Username of who created this management user' }, notes: { type: Sequelize.TEXT, allowNull: true, comment: 'Admin notes about this user' }, password_hash: { type: Sequelize.STRING, allowNull: false }, role: { type: Sequelize.ENUM('super_admin', 'tenant_admin'), defaultValue: 'tenant_admin', allowNull: false }, permissions: { type: Sequelize.JSON, allowNull: true, defaultValue: [] }, is_active: { type: Sequelize.BOOLEAN, defaultValue: true }, last_login: { type: Sequelize.DATE, allowNull: true }, created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW } }); // Create basic indexes await queryInterface.addIndex('devices', ['geo_lat', 'geo_lon']); await queryInterface.addIndex('devices', ['is_active']); await queryInterface.addIndex('heartbeats', ['device_id']); await queryInterface.addIndex('heartbeats', ['received_at']); await queryInterface.addIndex('drone_detections', ['device_id']); await queryInterface.addIndex('drone_detections', ['drone_id']); await queryInterface.addIndex('drone_detections', ['server_timestamp']); await queryInterface.addIndex('alert_rules', ['user_id']); await queryInterface.addIndex('alert_logs', ['alert_rule_id']); await queryInterface.addIndex('audit_logs', ['tenant_id']); await queryInterface.addIndex('audit_logs', ['user_id']); await queryInterface.addIndex('audit_logs', ['created_at']); }, async down(queryInterface, Sequelize) { // Drop tables in reverse order due to foreign key constraints await queryInterface.dropTable('audit_logs'); await queryInterface.dropTable('management_users'); await queryInterface.dropTable('alert_logs'); await queryInterface.dropTable('alert_rules'); await queryInterface.dropTable('drone_detections'); await queryInterface.dropTable('heartbeats'); await queryInterface.dropTable('devices'); await queryInterface.dropTable('users'); await queryInterface.dropTable('tenants'); } };