Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
390
backend/database/db.js
Normal file
390
backend/database/db.js
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
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 && 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('✓ Default admin user created (username: admin)');
|
||||
console.log('⚠ SECURITY: Change the default admin password immediately!');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue