const mysql = require('mysql2/promise'); const { hashPassword } = require('../auth'); const { PERMISSIONS, DEFAULT_ROLE } = require('../rbac'); const migrations = require('./migrations'); function createPool() { return mysql.createPool({ host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 3306), user: process.env.DB_USER || 'signage_user', password: process.env.DB_PASSWORD || 'signage_password', database: process.env.DB_NAME || 'signage', waitForConnections: true, connectionLimit: 10, namedPlaceholders: true }); } async function pruneStaleOnboardingDevices(pool) { await pool.query( `DELETE FROM player_onboarding_devices WHERE modified_at < (CURRENT_TIMESTAMP - INTERVAL 1 MINUTE)` ); } async function ensureSchema(pool, options) { await pool.query(` CREATE TABLE IF NOT EXISTS canvas_sizes ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, width INT NOT NULL, height INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, UNIQUE KEY uq_canvas_sizes_dimensions (width, height) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS playlists ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, fade_between_slides TINYINT(1) NOT NULL DEFAULT 0, skip_unavailable_rtmp TINYINT(1) NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS slide_templates ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, canvas_size_id INT NULL, background_image_path VARCHAR(512) NULL, background_color VARCHAR(32) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` INSERT IGNORE INTO canvas_sizes (name, width, height) VALUES ('Full HD', 1920, 1080), ('HD', 1280, 720), ('4K UHD', 3840, 2160), ('Portrait Full HD', 1080, 1920), ('Portrait HD', 720, 1280) `); await pool.query(` CREATE TABLE IF NOT EXISTS slide_template_regions ( id INT AUTO_INCREMENT PRIMARY KEY, template_id INT NOT NULL, region_key VARCHAR(100) NOT NULL, region_type VARCHAR(20) NOT NULL, label VARCHAR(255) NOT NULL, lock_ratio VARCHAR(20) NULL, x INT NOT NULL DEFAULT 0, y INT NOT NULL DEFAULT 0, width INT NOT NULL DEFAULT 100, height INT NOT NULL DEFAULT 100, z_index INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS slides ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, body TEXT NULL, template_id INT NULL, content_json JSON NULL, media_path VARCHAR(512) NULL, media_type VARCHAR(100) NULL, thumbnail_path VARCHAR(512) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS playlist_slides ( id INT AUTO_INCREMENT PRIMARY KEY, playlist_id INT NOT NULL, slide_id INT NOT NULL, position INT NOT NULL DEFAULT 0, duration_seconds INT NOT NULL DEFAULT 10, schedule_mode VARCHAR(20) NOT NULL DEFAULT 'always', schedule_start_datetime DATETIME NULL, schedule_end_datetime DATETIME NULL, schedule_start_time TIME NULL, schedule_end_time TIME NULL, schedule_days_json JSON NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_playlist_slides_playlist FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, CONSTRAINT fk_playlist_slides_slide FOREIGN KEY (slide_id) REFERENCES slides(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS screens ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, playlist_id INT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_screens_playlist FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS rss_feeds ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, feed_url VARCHAR(1024) NOT NULL, update_interval_value INT NOT NULL DEFAULT 60, update_interval_unit VARCHAR(10) NOT NULL DEFAULT 'minutes', item_limit INT NOT NULL DEFAULT 1, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS rss_feed_items ( id INT AUTO_INCREMENT PRIMARY KEY, rss_feed_id INT NOT NULL, position INT NOT NULL, item_json MEDIUMTEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_rss_feed_items_rss_feed FOREIGN KEY (rss_feed_id) REFERENCES rss_feeds(id) ON DELETE CASCADE, UNIQUE KEY uq_rss_feed_items_feed_position (rss_feed_id, position) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS api_sources ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, api_url VARCHAR(1024) NOT NULL, update_interval_value INT NOT NULL DEFAULT 60, update_interval_unit VARCHAR(10) NOT NULL DEFAULT 'minutes', last_pulled_at TIMESTAMP NULL, last_pull_error MEDIUMTEXT NULL, last_response_status INT NULL, last_response_content_type VARCHAR(255) NULL, last_response_json MEDIUMTEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS player_onboarding_devices ( device_id VARCHAR(128) PRIMARY KEY, client_name VARCHAR(255) NULL, screen_id INT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_player_onboarding_devices_screen FOREIGN KEY (screen_id) REFERENCES screens(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NULL, username VARCHAR(255) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL, password_salt VARCHAR(64) NOT NULL, password_iterations INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS roles ( id INT AUTO_INCREMENT PRIMARY KEY, role_key VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS permissions ( id INT AUTO_INCREMENT PRIMARY KEY, permission_key VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, section_name VARCHAR(255) NOT NULL, description TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); for (const permission of PERMISSIONS) { await pool.query( `INSERT INTO permissions (permission_key, name, section_name, description, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), section_name = VALUES(section_name), description = VALUES(description), modified_by = VALUES(modified_by)` , [permission.key, permission.name, permission.sectionName, permission.description || null, null, null] ); } await pool.query(` CREATE TABLE IF NOT EXISTS role_permissions ( role_id INT NOT NULL, permission_id INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, PRIMARY KEY (role_id, permission_id), CONSTRAINT fk_role_permissions_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, CONSTRAINT fk_role_permissions_permission FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS user_roles ( user_id INT NOT NULL, role_id INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, PRIMARY KEY (user_id, role_id), CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS auth_sessions ( session_hash CHAR(64) PRIMARY KEY, user_id INT NOT NULL, expires_at DATETIME NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS background_tasks ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_key VARCHAR(191) NULL, task_type VARCHAR(100) NOT NULL, title VARCHAR(255) NOT NULL, category VARCHAR(100) NOT NULL DEFAULT 'general', status VARCHAR(20) NOT NULL, payload_json MEDIUMTEXT NULL, metadata_json MEDIUMTEXT NULL, attempts INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, started_at TIMESTAMP NULL, finished_at TIMESTAMP NULL, error_message MEDIUMTEXT NULL, INDEX idx_background_tasks_status (status), INDEX idx_background_tasks_key (task_key), INDEX idx_background_tasks_type (task_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users'); const username = String(process.env.DEFAULT_ADMIN_USERNAME || 'admin').trim() || 'admin'; if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) { const name = String(process.env.DEFAULT_ADMIN_NAME || 'Admin').trim() || 'Admin'; const password = String(process.env.DEFAULT_ADMIN_PASSWORD || 'admin').trim() || 'admin'; const passwordRecord = hashPassword(password); await pool.query( 'INSERT INTO users (name, username, password_hash, password_salt, password_iterations, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)', [name, username, passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, null, null] ); } await pool.query('UPDATE users SET name = username WHERE name IS NULL OR name = ""'); await pool.query( `INSERT INTO roles (role_key, name, description, created_by, modified_by) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description), modified_by = VALUES(modified_by)`, [DEFAULT_ROLE.key, DEFAULT_ROLE.name, DEFAULT_ROLE.description || null, null, null] ); await migrations.runMigrations(pool, options || {}); if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) { const [roleRows] = await pool.query('SELECT id FROM roles WHERE role_key = ? LIMIT 1', [DEFAULT_ROLE.key]); const defaultRoleId = roleRows.length ? Number(roleRows[0].id) : null; if (defaultRoleId) { await pool.query( `INSERT IGNORE INTO user_roles (user_id, role_id, created_by, modified_by) SELECT id, ?, NULL, NULL FROM users`, [defaultRoleId] ); } } const [defaultAdminRows] = await pool.query('SELECT id FROM users WHERE username = ? LIMIT 1', [username]); if (defaultAdminRows.length) { const defaultAdminId = Number(defaultAdminRows[0].id); const [defaultAdminRoleRows] = await pool.query('SELECT COUNT(*) AS role_count FROM user_roles WHERE user_id = ?', [defaultAdminId]); if (!defaultAdminRoleRows.length || Number(defaultAdminRoleRows[0].role_count) === 0) { const [roleRows] = await pool.query('SELECT id FROM roles WHERE role_key = ? LIMIT 1', [DEFAULT_ROLE.key]); const defaultRoleId = roleRows.length ? Number(roleRows[0].id) : null; if (defaultRoleId) { await pool.query( 'INSERT IGNORE INTO user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [defaultAdminId, defaultRoleId, null, null] ); } } } } module.exports = { createPool, ensureSchema, pruneStaleOnboardingDevices };