const mysql = require('mysql2/promise'); const { hashPassword } = require('./auth'); 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 addColumnIfMissing(pool, tableName, columnName, columnDefinition) { const [rows] = await pool.query( `SELECT COUNT(*) AS column_count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`, [tableName, columnName] ); if (rows.length && Number(rows[0].column_count) > 0) { return; } await pool.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` ${columnDefinition}`); } async function ensureSchema(pool) { 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, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_canvas_sizes_dimensions (width, height) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'canvas_sizes', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'canvas_sizes', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'canvas_sizes', 'modified_by', 'INT NULL'); 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, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'playlists', 'fade_between_slides', 'TINYINT(1) NOT NULL DEFAULT 0'); await addColumnIfMissing(pool, 'playlists', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'playlists', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'playlists', 'modified_by', 'INT NULL'); 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, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'slide_templates', 'canvas_size_id', 'INT NULL'); await addColumnIfMissing(pool, 'slide_templates', 'background_image_path', 'VARCHAR(512) NULL'); await addColumnIfMissing(pool, 'slide_templates', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'slide_templates', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'slide_templates', 'modified_by', 'INT NULL'); 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) `); const [legacyTemplateColumns] = await pool.query(` SELECT COUNT(*) AS column_count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'slide_templates' AND column_name IN ('canvas_width', 'canvas_height') `); if (legacyTemplateColumns[0] && Number(legacyTemplateColumns[0].column_count) === 2) { await pool.query(` UPDATE slide_templates st JOIN canvas_sizes cs ON cs.width = st.canvas_width AND cs.height = st.canvas_height SET st.canvas_size_id = cs.id WHERE st.canvas_size_id IS NULL `); } 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, 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, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'slide_template_regions', 'font_family', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'slide_template_regions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'slide_template_regions', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'slide_template_regions', 'modified_by', 'INT NULL'); 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, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'slides', 'body', 'TEXT NULL'); await addColumnIfMissing(pool, 'slides', 'template_id', 'INT NULL'); await addColumnIfMissing(pool, 'slides', 'content_json', 'JSON NULL'); await addColumnIfMissing(pool, 'slides', 'media_path', 'VARCHAR(512) NULL'); await addColumnIfMissing(pool, 'slides', 'media_type', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'slides', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'slides', 'modified_by', 'INT NULL'); 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, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 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 addColumnIfMissing(pool, 'playlist_slides', 'duration_seconds', 'INT NOT NULL DEFAULT 10'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_mode', "VARCHAR(20) NOT NULL DEFAULT 'always'"); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_datetime', 'DATETIME NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_datetime', 'DATETIME NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_time', 'TIME NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_time', 'TIME NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_days_json', 'JSON NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'playlist_slides', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'modified_by', 'INT NULL'); 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, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 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 addColumnIfMissing(pool, 'screens', 'playlist_id', 'INT NULL'); await addColumnIfMissing(pool, 'screens', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'screens', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'screens', 'modified_by', 'INT NULL'); 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, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'users', 'name', 'VARCHAR(255) NULL'); await addColumnIfMissing(pool, 'users', 'password_hash', 'CHAR(64) NOT NULL'); await addColumnIfMissing(pool, 'users', 'password_salt', 'VARCHAR(64) NOT NULL'); await addColumnIfMissing(pool, 'users', 'password_iterations', 'INT NOT NULL'); await addColumnIfMissing(pool, 'users', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addColumnIfMissing(pool, 'users', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'users', 'modified_by', 'INT NULL'); 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, last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 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 addColumnIfMissing(pool, 'auth_sessions', 'created_by', 'INT NULL'); await addColumnIfMissing(pool, 'auth_sessions', 'modified_by', 'INT NULL'); const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users'); if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) { const username = String(process.env.DEFAULT_ADMIN_USERNAME || 'admin').trim() || 'admin'; 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 = ""'); } module.exports = { createPool, ensureSchema };