From e52d6a83846706e1dcc1856d7f070bb5c99e4b2a Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sun, 21 Sep 2025 09:03:09 +0200 Subject: [PATCH] Fix jwt-token --- server/docker-entrypoint.sh | 25 +- .../20250820000001-create-initial-tables.js | 614 ++++++++++++++++++ server/scripts/setup-database.js | 22 +- 3 files changed, 633 insertions(+), 28 deletions(-) create mode 100644 server/migrations/20250820000001-create-initial-tables.js diff --git a/server/docker-entrypoint.sh b/server/docker-entrypoint.sh index a68a29a..57cff39 100644 --- a/server/docker-entrypoint.sh +++ b/server/docker-entrypoint.sh @@ -17,28 +17,9 @@ echo "Database is ready!" # Check if this is a fresh database by looking for the devices table echo "Checking database state..." -if su-exec nodejs node -e " -const { Sequelize } = require('sequelize'); -require('dotenv').config(); -const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { - host: process.env.DB_HOST, port: process.env.DB_PORT, dialect: 'postgres', logging: false -}); -sequelize.authenticate().then(() => { - return sequelize.getQueryInterface().describeTable('devices'); -}).then(() => { - console.log('TABLES_EXIST'); - process.exit(0); -}).catch(() => { - console.log('FRESH_DATABASE'); - process.exit(0); -}); -" | grep -q "FRESH_DATABASE"; then - echo "Fresh database detected. Running initial setup..." - su-exec nodejs npm run db:setup -else - echo "Existing database detected. Running migrations..." - su-exec nodejs npm run db:migrate -fi +# Always run database setup (includes migrations + seeding if needed) +echo "Running database setup and migrations..." +su-exec nodejs npm run db:setup # Check if setup/migrations were successful if [ $? -eq 0 ]; then diff --git a/server/migrations/20250820000001-create-initial-tables.js b/server/migrations/20250820000001-create-initial-tables.js new file mode 100644 index 0000000..1b8a425 --- /dev/null +++ b/server/migrations/20250820000001-create-initial-tables.js @@ -0,0 +1,614 @@ +/** + * 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) + 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' + }, + is_active: { + type: Sequelize.BOOLEAN, + defaultValue: true, + comment: 'Whether tenant is active' + }, + created_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + allowNull: false + }, + updated_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + allowNull: false + } + }); + + // 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 + }, + role: { + type: Sequelize.ENUM('admin', 'operator', 'viewer'), + defaultValue: 'viewer', + allowNull: false + }, + 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' + }, + 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 + }, + user_id: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + name: { + type: Sequelize.STRING, + allowNull: false + }, + description: { + type: Sequelize.TEXT, + allowNull: true + }, + conditions: { + type: Sequelize.JSON, + allowNull: false + }, + actions: { + type: Sequelize.JSON, + allowNull: false + }, + cooldown_minutes: { + type: Sequelize.INTEGER, + defaultValue: 5 + }, + 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'); + } +}; \ No newline at end of file diff --git a/server/scripts/setup-database.js b/server/scripts/setup-database.js index 8e4d894..0efaacc 100644 --- a/server/scripts/setup-database.js +++ b/server/scripts/setup-database.js @@ -1,5 +1,6 @@ const { Sequelize } = require('sequelize'); const bcrypt = require('bcryptjs'); +const runMigrations = require('./migrate'); // Import models from the main models index const { @@ -25,10 +26,19 @@ const setupDatabase = async () => { // Models are already initialized through the imports console.log('� Models loaded and ready...'); - // Sync database (create tables) - console.log('🏗️ Creating database tables...'); - await sequelize.sync({ force: true }); // WARNING: This will drop existing tables - console.log('✅ Database tables created successfully.\n'); + // Run migrations first to create proper schema + console.log('🏗️ Running database migrations...'); + await runMigrations(); + console.log('✅ Database migrations completed successfully.\n'); + + // Check if sample data already exists + const existingTenants = await Tenant.count(); + if (existingTenants > 0) { + console.log('📊 Sample data already exists, skipping data creation...\n'); + console.log('🎉 Database setup completed successfully!\n'); + await sequelize.close(); + return; + } // Create sample data console.log('📊 Creating sample data...\n'); @@ -284,8 +294,8 @@ const setupDatabase = async () => { `); await sequelize.query(` - CREATE INDEX IF NOT EXISTS idx_alert_logs_user_created - ON "alert_logs" (user_id, "created_at"); + CREATE INDEX IF NOT EXISTS idx_alert_logs_rule_created + ON "alert_logs" (alert_rule_id, "created_at"); `); console.log('✅ Database indexes created\n');