const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const fs = require('fs'); const logger = require('../utils/logger'); const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../data/streamflow.db'); // Ensure data directory exists const dataDir = path.dirname(DB_PATH); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true, mode: 0o755 }); } const db = new sqlite3.Database(DB_PATH, (err) => { if (err) { logger.error('Database connection error:', err); } else { logger.info('Connected to SQLite database'); } }); const initialize = () => { // Initialize RBAC roles first (must be before user creation) const { initializeRoles } = require('../middleware/rbac'); initializeRoles(); db.serialize(() => { // Users table db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT DEFAULT 'user', must_change_password BOOLEAN DEFAULT 0, is_active BOOLEAN DEFAULT 1, two_factor_enabled BOOLEAN DEFAULT 0, two_factor_secret TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL )`); // 2FA backup codes table db.run(`CREATE TABLE IF NOT EXISTS two_factor_backup_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, code TEXT NOT NULL, used BOOLEAN DEFAULT 0, used_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Password history table (prevent password reuse) db.run(`CREATE TABLE IF NOT EXISTS password_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, password_hash TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Login attempts tracking table db.run(`CREATE TABLE IF NOT EXISTS login_attempts ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, ip_address TEXT NOT NULL, user_agent TEXT, success BOOLEAN NOT NULL, failure_reason TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`); // Create indexes for login_attempts db.run('CREATE INDEX IF NOT EXISTS idx_username_timestamp ON login_attempts(username, timestamp)'); db.run('CREATE INDEX IF NOT EXISTS idx_ip_timestamp ON login_attempts(ip_address, timestamp)'); // Account lockouts table db.run(`CREATE TABLE IF NOT EXISTS account_lockouts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, locked_until DATETIME NOT NULL, locked_by TEXT DEFAULT 'system', reason TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Active sessions table db.run(`CREATE TABLE IF NOT EXISTS active_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, session_token TEXT UNIQUE NOT NULL, ip_address TEXT, user_agent TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_activity DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Security audit log table db.run(`CREATE TABLE IF NOT EXISTS security_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, user_id INTEGER, ip_address TEXT, user_agent TEXT, success BOOLEAN NOT NULL, failure_reason TEXT, metadata TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL )`); // Create indexes for security_audit_log db.run('CREATE INDEX IF NOT EXISTS idx_event_timestamp ON security_audit_log(event_type, timestamp)'); db.run('CREATE INDEX IF NOT EXISTS idx_user_timestamp ON security_audit_log(user_id, timestamp)'); // Add new columns if they don't exist (migration) db.all("PRAGMA table_info(users)", [], (err, columns) => { if (!err) { if (!columns.some(col => col.name === 'must_change_password')) { db.run("ALTER TABLE users ADD COLUMN must_change_password BOOLEAN DEFAULT 0"); } if (!columns.some(col => col.name === 'is_active')) { db.run("ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT 1"); } if (!columns.some(col => col.name === 'created_by')) { db.run("ALTER TABLE users ADD COLUMN created_by INTEGER REFERENCES users(id) ON DELETE SET NULL"); } if (!columns.some(col => col.name === 'two_factor_enabled')) { db.run("ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0"); } if (!columns.some(col => col.name === 'two_factor_secret')) { db.run("ALTER TABLE users ADD COLUMN two_factor_secret TEXT"); } if (!columns.some(col => col.name === 'password_changed_at')) { db.run("ALTER TABLE users ADD COLUMN password_changed_at DATETIME DEFAULT CURRENT_TIMESTAMP"); } if (!columns.some(col => col.name === 'password_expires_at')) { db.run("ALTER TABLE users ADD COLUMN password_expires_at DATETIME"); } if (!columns.some(col => col.name === 'failed_login_attempts')) { db.run("ALTER TABLE users ADD COLUMN failed_login_attempts INTEGER DEFAULT 0"); } if (!columns.some(col => col.name === 'last_failed_login')) { db.run("ALTER TABLE users ADD COLUMN last_failed_login DATETIME"); } if (!columns.some(col => col.name === 'locked_until')) { db.run("ALTER TABLE users ADD COLUMN locked_until DATETIME"); } if (!columns.some(col => col.name === 'last_login_at')) { db.run("ALTER TABLE users ADD COLUMN last_login_at DATETIME"); } if (!columns.some(col => col.name === 'last_login_ip')) { db.run("ALTER TABLE users ADD COLUMN last_login_ip TEXT"); } } }); // Create default admin user if no users exist db.get("SELECT COUNT(*) as count FROM users", [], (err, result) => { if (err) { console.error('Error checking user count:', err); return; } console.log(`Database initialized. Current users: ${result.count}`); if (result.count === 0) { const bcrypt = require('bcrypt'); const defaultPassword = bcrypt.hashSync('admin', 10); db.run( `INSERT INTO users (username, email, password, role, must_change_password) VALUES (?, ?, ?, ?, ?)`, ['admin', 'admin@streamflow.local', defaultPassword, 'admin', 1], (err) => { if (err) { console.error('❌ Failed to create default admin:', err); } else { // CWE-532: Never log passwords - even default ones console.log(''); console.log('═══════════════════════════════════════════════════════'); console.log('✅ Default admin user created successfully!'); console.log(''); console.log(' Username: admin'); console.log(' Password: admin'); console.log(''); console.log('⚠️ SECURITY WARNING: Change this password immediately!'); console.log('═══════════════════════════════════════════════════════'); console.log(''); } } ); } else { console.log('ℹ️ Users already exist. Skipping default admin creation.'); } }); // Profiles table (multi-user support) db.run(`CREATE TABLE IF NOT EXISTS profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, avatar TEXT, is_child BOOLEAN DEFAULT 0, age_restriction INTEGER DEFAULT 18, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Playlists table db.run(`CREATE TABLE IF NOT EXISTS playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, url TEXT, type TEXT DEFAULT 'live', filename TEXT, category TEXT, is_active BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Channels table db.run(`CREATE TABLE IF NOT EXISTS channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id INTEGER NOT NULL, name TEXT NOT NULL, url TEXT NOT NULL, logo TEXT, custom_logo TEXT, group_name TEXT, tvg_id TEXT, tvg_name TEXT, language TEXT, country TEXT, is_radio BOOLEAN DEFAULT 0, is_active BOOLEAN DEFAULT 1, health_status TEXT DEFAULT 'unknown', last_checked DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE )`); // Add custom_logo column if it doesn't exist (migration) db.all("PRAGMA table_info(channels)", [], (err, columns) => { if (!err && !columns.some(col => col.name === 'custom_logo')) { db.run("ALTER TABLE channels ADD COLUMN custom_logo TEXT", (err) => { if (err) console.error('Migration error:', err); }); } }); // Custom groups table db.run(`CREATE TABLE IF NOT EXISTS custom_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, icon TEXT, order_index INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Group channels (many-to-many) db.run(`CREATE TABLE IF NOT EXISTS group_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, order_index INTEGER DEFAULT 0, FOREIGN KEY (group_id) REFERENCES custom_groups(id) ON DELETE CASCADE, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, UNIQUE(group_id, channel_id) )`); // Recordings table db.run(`CREATE TABLE IF NOT EXISTS recordings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, profile_id INTEGER, channel_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration INTEGER, file_path TEXT, file_size INTEGER, status TEXT DEFAULT 'scheduled', is_series BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE SET NULL, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE )`); // Watch history table db.run(`CREATE TABLE IF NOT EXISTS watch_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, profile_id INTEGER, channel_id INTEGER NOT NULL, watched_at DATETIME DEFAULT CURRENT_TIMESTAMP, duration INTEGER, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE SET NULL, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE )`); // Favorites table db.run(`CREATE TABLE IF NOT EXISTS favorites ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, profile_id INTEGER, channel_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE SET NULL, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, UNIQUE(user_id, profile_id, channel_id) )`); // Settings table db.run(`CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(user_id, key) )`); // API tokens table db.run(`CREATE TABLE IF NOT EXISTS api_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, token TEXT UNIQUE NOT NULL, name TEXT, expires_at DATETIME, usage_count INTEGER DEFAULT 0, usage_limit INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Channel logos cache table db.run(`CREATE TABLE IF NOT EXISTS logo_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_name TEXT UNIQUE NOT NULL, logo_url TEXT, local_path TEXT, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP )`); // M3U files library table db.run(`CREATE TABLE IF NOT EXISTS m3u_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, original_filename TEXT NOT NULL, file_path TEXT NOT NULL, size INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Create VPN configs table for multiple configuration files db.run(`CREATE TABLE IF NOT EXISTS vpn_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, config_type TEXT NOT NULL CHECK(config_type IN ('openvpn', 'wireguard')), config_data TEXT NOT NULL, country TEXT, server_name TEXT, endpoint TEXT, is_active BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`); // Create indexes for performance db.run('CREATE INDEX IF NOT EXISTS idx_channels_playlist ON channels(playlist_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_channels_tvg_id ON channels(tvg_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_watch_history_user ON watch_history(user_id, profile_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_favorites_user ON favorites(user_id, profile_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_recordings_user ON recordings(user_id, profile_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_m3u_files_user ON m3u_files(user_id)'); logger.info('Database tables initialized'); }); }; module.exports = { db, initialize };