Files
drone-detector/server/migrations/20250820000001-create-initial-tables.js
2025-09-22 06:11:00 +02:00

814 lines
21 KiB
JavaScript

/**
* 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.INTEGER,
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_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', 'high', 'urgent'),
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
}
},
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');
}
};