streamflow/backend/database/db.js

408 lines
15 KiB
JavaScript
Raw Normal View History

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
};