704 lines
28 KiB
JavaScript
704 lines
28 KiB
JavaScript
const mysql = require('mysql2/promise');
|
|
const { hashPassword } = require('./auth');
|
|
const { PERMISSIONS, DEFAULT_ROLE, normalizePermissionKeys } = require('./rbac');
|
|
|
|
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 dropColumnIfPresent(pool, tableName, columnName) {
|
|
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}\` DROP COLUMN \`${columnName}\``);
|
|
}
|
|
|
|
async function addForeignKeyIfMissing(pool, tableName, columnName, constraintName, referencedTable, referencedColumn, onDeleteAction) {
|
|
const [rows] = await pool.query(
|
|
`SELECT COUNT(*) AS constraint_count
|
|
FROM information_schema.table_constraints
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?
|
|
AND constraint_name = ?`,
|
|
[tableName, constraintName]
|
|
);
|
|
|
|
if (rows.length && Number(rows[0].constraint_count) > 0) {
|
|
return;
|
|
}
|
|
|
|
await pool.query(
|
|
`ALTER TABLE \`${tableName}\`
|
|
ADD CONSTRAINT \`${constraintName}\`
|
|
FOREIGN KEY (\`${columnName}\`) REFERENCES \`${referencedTable}\`(\`${referencedColumn}\`)
|
|
ON DELETE ${onDeleteAction}
|
|
ON UPDATE CASCADE`
|
|
);
|
|
}
|
|
|
|
async function addUserAuditColumns(pool, tableName) {
|
|
await addColumnIfMissing(pool, tableName, 'created_by', 'INT NULL');
|
|
await addColumnIfMissing(pool, tableName, 'modified_by', 'INT NULL');
|
|
|
|
await pool.query(
|
|
`UPDATE \`${tableName}\` t
|
|
LEFT JOIN users created_user ON created_user.id = t.created_by
|
|
SET t.created_by = NULL
|
|
WHERE t.created_by IS NOT NULL
|
|
AND created_user.id IS NULL`
|
|
);
|
|
await pool.query(
|
|
`UPDATE \`${tableName}\` t
|
|
LEFT JOIN users modified_user ON modified_user.id = t.modified_by
|
|
SET t.modified_by = NULL
|
|
WHERE t.modified_by IS NOT NULL
|
|
AND modified_user.id IS NULL`
|
|
);
|
|
|
|
await addForeignKeyIfMissing(pool, tableName, 'created_by', `fk_${tableName}_created_by`, 'users', 'id', 'SET NULL');
|
|
await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL');
|
|
}
|
|
|
|
async function hasSingleColumnUniqueIndex(pool, tableName, columnName) {
|
|
const [rows] = await pool.query(
|
|
`SELECT INDEX_NAME, COUNT(*) AS column_count
|
|
FROM information_schema.statistics
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?
|
|
AND non_unique = 0
|
|
AND column_name = ?
|
|
GROUP BY INDEX_NAME`,
|
|
[tableName, columnName]
|
|
);
|
|
|
|
return (rows || []).some(function (row) {
|
|
return Number(row.column_count) === 1;
|
|
});
|
|
}
|
|
|
|
async function addUniqueIndexIfMissing(pool, tableName, columnName, indexName) {
|
|
const hasUniqueIndex = await hasSingleColumnUniqueIndex(pool, tableName, columnName);
|
|
if (hasUniqueIndex) {
|
|
return;
|
|
}
|
|
|
|
await pool.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE KEY \`${indexName}\` (\`${columnName}\`)`);
|
|
}
|
|
|
|
async function dedupePermissionRows(pool) {
|
|
const [rows] = await pool.query('SELECT id, permission_key FROM permissions ORDER BY id ASC');
|
|
const canonicalIdByKey = new Map();
|
|
const duplicateRowsByKey = new Map();
|
|
|
|
for (const row of rows || []) {
|
|
const permissionKey = getPermissionKey(row);
|
|
const permissionId = Number(row.id);
|
|
if (!permissionKey || !Number.isInteger(permissionId) || permissionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (!canonicalIdByKey.has(permissionKey)) {
|
|
canonicalIdByKey.set(permissionKey, permissionId);
|
|
continue;
|
|
}
|
|
|
|
if (!duplicateRowsByKey.has(permissionKey)) {
|
|
duplicateRowsByKey.set(permissionKey, []);
|
|
}
|
|
duplicateRowsByKey.get(permissionKey).push(permissionId);
|
|
}
|
|
|
|
if (!duplicateRowsByKey.size) {
|
|
return;
|
|
}
|
|
|
|
for (const [permissionKey, duplicateIds] of duplicateRowsByKey.entries()) {
|
|
const canonicalId = canonicalIdByKey.get(permissionKey);
|
|
for (const duplicateId of duplicateIds) {
|
|
await pool.query(
|
|
'UPDATE IGNORE role_permissions SET permission_id = ? WHERE permission_id = ?',
|
|
[canonicalId, duplicateId]
|
|
);
|
|
}
|
|
}
|
|
|
|
const duplicateIds = [];
|
|
for (const duplicateList of duplicateRowsByKey.values()) {
|
|
duplicateIds.push.apply(duplicateIds, duplicateList);
|
|
}
|
|
|
|
if (duplicateIds.length) {
|
|
await pool.query('DELETE FROM permissions WHERE id IN (?)', [duplicateIds]);
|
|
}
|
|
}
|
|
|
|
async function pruneStaleOnboardingDevices(pool) {
|
|
await pool.query(
|
|
`DELETE FROM player_onboarding_devices
|
|
WHERE modified_at < (CURRENT_TIMESTAMP - INTERVAL 1 MINUTE)`
|
|
);
|
|
}
|
|
|
|
async function getTableColumnNames(pool, tableName) {
|
|
const [rows] = await pool.query(
|
|
`SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?`,
|
|
[tableName]
|
|
);
|
|
|
|
return new Set((rows || []).map(function (row) {
|
|
return String(row.COLUMN_NAME || row.column_name || '').trim().toLowerCase();
|
|
}).filter(Boolean));
|
|
}
|
|
|
|
function getPermissionKey(row) {
|
|
return String((row && row.permission_key) || '').trim().toLowerCase();
|
|
}
|
|
|
|
function getLegacyPermissionTargets(permissionKey) {
|
|
const normalizedKey = String(permissionKey || '').trim().toLowerCase();
|
|
const parts = normalizedKey.split('.');
|
|
if (parts.length !== 2) {
|
|
return [normalizedKey].filter(Boolean);
|
|
}
|
|
|
|
const sectionKey = parts[0];
|
|
const actionKey = parts[1];
|
|
if (normalizedKey === 'screens.allow') {
|
|
return ['clients.allow'];
|
|
}
|
|
if (actionKey === 'view') {
|
|
return [`${sectionKey}.read`];
|
|
}
|
|
if (actionKey === 'manage') {
|
|
return [`${sectionKey}.read`, `${sectionKey}.create`, `${sectionKey}.edit`, `${sectionKey}.delete`];
|
|
}
|
|
|
|
return [normalizedKey].filter(Boolean);
|
|
}
|
|
|
|
function buildPermissionSeedColumns(columnNames) {
|
|
const columns = [];
|
|
if (columnNames.has('permission_key')) {
|
|
columns.push('permission_key');
|
|
}
|
|
if (columnNames.has('name')) {
|
|
columns.push('name');
|
|
}
|
|
if (columnNames.has('section_name')) {
|
|
columns.push('section_name');
|
|
}
|
|
if (columnNames.has('description')) {
|
|
columns.push('description');
|
|
}
|
|
if (columnNames.has('created_by')) {
|
|
columns.push('created_by');
|
|
}
|
|
if (columnNames.has('modified_by')) {
|
|
columns.push('modified_by');
|
|
}
|
|
return columns;
|
|
}
|
|
|
|
async function backfillLegacyRbacSchema(pool) {
|
|
const permissionColumnNames = await getTableColumnNames(pool, 'permissions');
|
|
|
|
const [permissionRows] = await pool.query('SELECT id, permission_key, name, section_name FROM permissions ORDER BY id ASC');
|
|
const [rolePermissionRows] = await pool.query(
|
|
`SELECT rp.role_id, p.permission_key
|
|
FROM role_permissions rp
|
|
JOIN permissions p ON p.id = rp.permission_id`
|
|
);
|
|
|
|
const rolePermissionTargets = new Map();
|
|
const desiredPermissionKeys = new Set(PERMISSIONS.map(function (permission) {
|
|
return permission.key;
|
|
}));
|
|
const legacyPermissionRowIds = [];
|
|
|
|
function addRoleTarget(roleId, permissionKey) {
|
|
const normalizedPermissionKey = String(permissionKey || '').trim().toLowerCase();
|
|
if (!normalizedPermissionKey) {
|
|
return;
|
|
}
|
|
if (!rolePermissionTargets.has(roleId)) {
|
|
rolePermissionTargets.set(roleId, new Set());
|
|
}
|
|
rolePermissionTargets.get(roleId).add(normalizedPermissionKey);
|
|
}
|
|
|
|
for (const row of rolePermissionRows || []) {
|
|
const currentKey = getPermissionKey(row);
|
|
const targetKeys = getLegacyPermissionTargets(currentKey);
|
|
if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey === 'screens.allow') {
|
|
for (const targetKey of targetKeys) {
|
|
addRoleTarget(Number(row.role_id), targetKey);
|
|
}
|
|
} else {
|
|
addRoleTarget(Number(row.role_id), currentKey);
|
|
}
|
|
}
|
|
|
|
for (const row of permissionRows || []) {
|
|
const currentKey = getPermissionKey(row);
|
|
if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey === 'screens.allow') {
|
|
legacyPermissionRowIds.push(Number(row.id));
|
|
}
|
|
}
|
|
|
|
const roleRows = await pool.query('SELECT id, role_key, name FROM roles ORDER BY id ASC').then(function (result) {
|
|
return result[0] || [];
|
|
});
|
|
const defaultRoleRow = roleRows.find(function (row) {
|
|
return String(row.role_key || '').trim().toLowerCase() === DEFAULT_ROLE.key;
|
|
}) || null;
|
|
if (defaultRoleRow) {
|
|
if (!rolePermissionTargets.has(Number(defaultRoleRow.id))) {
|
|
rolePermissionTargets.set(Number(defaultRoleRow.id), new Set());
|
|
}
|
|
const defaultPermissions = rolePermissionTargets.get(Number(defaultRoleRow.id));
|
|
for (const permission of PERMISSIONS) {
|
|
defaultPermissions.add(permission.key);
|
|
}
|
|
}
|
|
|
|
const seedColumns = buildPermissionSeedColumns(permissionColumnNames);
|
|
if (!seedColumns.length) {
|
|
throw new Error('permissions table is missing required columns.');
|
|
}
|
|
|
|
for (const permission of PERMISSIONS) {
|
|
const seedValues = [];
|
|
if (permissionColumnNames.has('permission_key')) {
|
|
seedValues.push(permission.key);
|
|
}
|
|
if (permissionColumnNames.has('name')) {
|
|
seedValues.push(permission.name);
|
|
}
|
|
if (permissionColumnNames.has('section_name')) {
|
|
seedValues.push(permission.sectionName);
|
|
}
|
|
if (permissionColumnNames.has('description')) {
|
|
seedValues.push(permission.description || null);
|
|
}
|
|
if (permissionColumnNames.has('created_by')) {
|
|
seedValues.push(null);
|
|
}
|
|
if (permissionColumnNames.has('modified_by')) {
|
|
seedValues.push(null);
|
|
}
|
|
|
|
const updateAssignments = [];
|
|
if (permissionColumnNames.has('name')) {
|
|
updateAssignments.push('name = VALUES(name)');
|
|
}
|
|
if (permissionColumnNames.has('section_name')) {
|
|
updateAssignments.push('section_name = VALUES(section_name)');
|
|
}
|
|
if (permissionColumnNames.has('description')) {
|
|
updateAssignments.push('description = VALUES(description)');
|
|
}
|
|
if (permissionColumnNames.has('permission_key')) {
|
|
updateAssignments.push('permission_key = VALUES(permission_key)');
|
|
}
|
|
|
|
await pool.query(
|
|
`INSERT INTO permissions (${seedColumns.join(', ')})
|
|
VALUES (${seedColumns.map(function () { return '?'; }).join(', ')})
|
|
ON DUPLICATE KEY UPDATE ${updateAssignments.join(', ')}`,
|
|
seedValues
|
|
);
|
|
}
|
|
|
|
if (legacyPermissionRowIds.length) {
|
|
await pool.query('DELETE FROM permissions WHERE id IN (?)', [legacyPermissionRowIds]);
|
|
}
|
|
|
|
const [currentPermissionRows] = await pool.query('SELECT id, permission_key FROM permissions');
|
|
const permissionIdByKey = new Map();
|
|
for (const row of currentPermissionRows || []) {
|
|
const currentKey = getPermissionKey(row);
|
|
if (currentKey) {
|
|
permissionIdByKey.set(currentKey, Number(row.id));
|
|
}
|
|
}
|
|
|
|
await pool.query('DELETE FROM role_permissions');
|
|
for (const [roleId, permissionKeys] of rolePermissionTargets.entries()) {
|
|
const expandedPermissionKeys = normalizePermissionKeys(Array.from(permissionKeys.values()));
|
|
for (const permissionKey of expandedPermissionKeys) {
|
|
const permissionId = permissionIdByKey.get(permissionKey);
|
|
if (!permissionId) {
|
|
continue;
|
|
}
|
|
await pool.query(
|
|
'INSERT IGNORE INTO role_permissions (role_id, permission_id, created_by, modified_by) VALUES (?, ?, ?, ?)',
|
|
[Number(roleId), permissionId, null, null]
|
|
);
|
|
}
|
|
}
|
|
|
|
const [roleRowsAfter] = await pool.query('SELECT id, role_key, name FROM roles ORDER BY id ASC');
|
|
for (const row of roleRowsAfter || []) {
|
|
const currentKey = String(row.role_key || '').trim();
|
|
const isAdministratorsRole = String(row.name || '').trim().toLowerCase() === DEFAULT_ROLE.name.toLowerCase();
|
|
const expectedKey = isAdministratorsRole ? DEFAULT_ROLE.key : `role-${row.id}`;
|
|
if (!currentKey || currentKey !== expectedKey) {
|
|
await pool.query(
|
|
'UPDATE roles SET role_key = ?, name = ?, description = COALESCE(description, ?) WHERE id = ?',
|
|
[expectedKey, String(row.name || '').trim() || DEFAULT_ROLE.name, DEFAULT_ROLE.description || null, row.id]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 addUserAuditColumns(pool, 'canvas_sizes');
|
|
|
|
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 addUserAuditColumns(pool, 'playlists');
|
|
|
|
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,
|
|
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', 'background_color', 'VARCHAR(32) NULL');
|
|
await addColumnIfMissing(pool, 'slide_templates', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'slide_templates');
|
|
|
|
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 addUserAuditColumns(pool, 'slide_template_regions');
|
|
|
|
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 addUserAuditColumns(pool, 'slides');
|
|
|
|
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 addUserAuditColumns(pool, 'playlist_slides');
|
|
|
|
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 addUserAuditColumns(pool, 'screens');
|
|
|
|
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,
|
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
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 addColumnIfMissing(pool, 'player_onboarding_devices', 'client_name', 'VARCHAR(255) NULL');
|
|
await addColumnIfMissing(pool, 'player_onboarding_devices', 'screen_id', 'INT NULL');
|
|
await addColumnIfMissing(pool, 'player_onboarding_devices', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'player_onboarding_devices');
|
|
|
|
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 addUserAuditColumns(pool, 'users');
|
|
|
|
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,
|
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
`);
|
|
await addColumnIfMissing(pool, 'roles', 'role_key', 'VARCHAR(100) NULL');
|
|
await addColumnIfMissing(pool, 'roles', 'description', 'TEXT NULL');
|
|
await addColumnIfMissing(pool, 'roles', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'roles');
|
|
|
|
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,
|
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
`);
|
|
await addColumnIfMissing(pool, 'permissions', 'permission_key', 'VARCHAR(100) NULL');
|
|
await addColumnIfMissing(pool, 'permissions', 'name', 'VARCHAR(255) NULL');
|
|
await addColumnIfMissing(pool, 'permissions', 'section_name', 'VARCHAR(255) NULL');
|
|
await addColumnIfMissing(pool, 'permissions', 'description', 'TEXT NULL');
|
|
await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'permissions');
|
|
await dropColumnIfPresent(pool, 'permissions', 'perm_key');
|
|
await dedupePermissionRows(pool);
|
|
await addUniqueIndexIfMissing(pool, 'permissions', 'permission_key', 'uq_permissions_permission_key');
|
|
|
|
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,
|
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
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 addColumnIfMissing(pool, 'role_permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'role_permissions');
|
|
|
|
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,
|
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
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 addColumnIfMissing(pool, 'user_roles', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
await addUserAuditColumns(pool, 'user_roles');
|
|
|
|
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 addUserAuditColumns(pool, 'auth_sessions');
|
|
|
|
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 = ""');
|
|
|
|
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 backfillLegacyRbacSchema(pool);
|
|
|
|
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]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
createPool,
|
|
ensureSchema,
|
|
pruneStaleOnboardingDevices
|
|
};
|