Prepare v2.0.0 release

This commit is contained in:
2026-07-28 22:20:40 +01:00
parent de1469c29d
commit c643d2fb07
64 changed files with 809 additions and 1391 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ function registerBackgroundTaskHandlers(options) {
}
const [slides] = await pool.query(
'SELECT id, thumbnail_path FROM slides WHERE template_id = ? ORDER BY id ASC',
'SELECT id, thumbnail_path FROM c_slides WHERE template_id = ? ORDER BY id ASC',
[templateId]
);
+5 -5
View File
@@ -154,7 +154,7 @@ function createBackgroundTaskQueue(options) {
const record = buildTaskRecord(task);
const [result] = await pool.query(
`INSERT INTO background_tasks (
`INSERT INTO o_background_tasks (
task_key,
task_type,
title,
@@ -196,7 +196,7 @@ function createBackgroundTaskQueue(options) {
const record = buildTaskRecord(task);
await pool.query(
`UPDATE background_tasks
`UPDATE o_background_tasks
SET task_key = ?, task_type = ?, title = ?, category = ?, status = ?, payload_json = ?, metadata_json = ?, attempts = ?, started_at = ?, finished_at = ?, error_message = ?
WHERE id = ?`,
[
@@ -221,7 +221,7 @@ function createBackgroundTaskQueue(options) {
return;
}
await pool.query('DELETE FROM background_tasks WHERE id = ?', [taskId]);
await pool.query('DELETE FROM o_background_tasks WHERE id = ?', [taskId]);
}
async function initialize() {
@@ -235,7 +235,7 @@ function createBackgroundTaskQueue(options) {
}
const [rows] = await pool.query(
'SELECT id, task_key, task_type, title, category, status, payload_json, metadata_json, attempts, created_at, started_at, finished_at, error_message FROM background_tasks ORDER BY id ASC'
'SELECT id, task_key, task_type, title, category, status, payload_json, metadata_json, attempts, created_at, started_at, finished_at, error_message FROM o_background_tasks ORDER BY id ASC'
);
let highestTaskId = 0;
@@ -254,7 +254,7 @@ function createBackgroundTaskQueue(options) {
task.finishedAt = '';
task.errorMessage = '';
await pool.query(
'UPDATE background_tasks SET status = ?, started_at = NULL, finished_at = NULL, error_message = NULL WHERE id = ?',
'UPDATE o_background_tasks SET status = ?, started_at = NULL, finished_at = NULL, error_message = NULL WHERE id = ?',
['queued', task.id]
);
}
+5 -5
View File
@@ -8,10 +8,10 @@ function registerBackgroundTaskScheduling(options) {
const removeUnusedUploadFiles = options && options.removeUnusedUploadFiles;
const refreshApiSource = options && options.refreshApiSource;
const refreshRssFeed = options && options.refreshRssFeed;
const uploadsDir = String(options && options.uploadsDir || '').trim();
const mediaDir = String(options && options.mediaDir || '').trim();
const dataSourceStartupRefreshStaggerMs = Math.max(100, Number(options && options.dataSourceStartupRefreshStaggerMs || 250));
if (!pool || !common || !backgroundTaskQueue || !collectUploadPathsFromDirectory || !removeUnusedUploadFiles || !refreshApiSource || !refreshRssFeed || !uploadsDir) {
if (!pool || !common || !backgroundTaskQueue || !collectUploadPathsFromDirectory || !removeUnusedUploadFiles || !refreshApiSource || !refreshRssFeed || !mediaDir) {
throw new Error('registerBackgroundTaskScheduling requires the background task dependencies.');
}
@@ -55,12 +55,12 @@ function registerBackgroundTaskScheduling(options) {
async function registerRecurringMaintenanceTasks() {
async function runUnusedUploadSweep() {
const uploadPaths = await collectUploadPathsFromDirectory(uploadsDir);
const uploadPaths = await collectUploadPathsFromDirectory(mediaDir);
if (!uploadPaths.length) {
return;
}
await removeUnusedUploadFiles(pool, uploadsDir, uploadPaths);
await removeUnusedUploadFiles(pool, mediaDir, uploadPaths);
}
backgroundTaskQueue.registerRecurringTask({
@@ -69,7 +69,7 @@ function registerBackgroundTaskScheduling(options) {
category: 'media-sync',
intervalMs: 24 * 60 * 60 * 1000,
metadata: {
uploadDir: uploadsDir
mediaDir: mediaDir
},
run: runUnusedUploadSweep
});
+2 -2
View File
@@ -67,8 +67,8 @@ function createDashboardStateService(options) {
const [onboardingRows] = await pool.query(
`SELECT s.slug, pod.device_id, pod.client_name
FROM player_onboarding_devices pod
JOIN screens s ON s.id = pod.screen_id
FROM d_onboarding_devices pod
JOIN d_screens s ON s.id = pod.screen_id
WHERE pod.client_name IS NOT NULL
AND TRIM(pod.client_name) <> ''`
);
+1 -1
View File
@@ -12,7 +12,7 @@ async function refreshApiSource(pool, common, apiSourceId, apiUrl, actorId) {
await connection.beginTransaction();
await connection.query(
'UPDATE api_sources SET last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
'UPDATE i_api_sources SET last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
[new Date(), pullError || null, responseDetails ? responseDetails.responseStatus : null, responseDetails ? responseDetails.responseContentType : null, responseDetails ? responseDetails.responseJson : null, actorId, apiSourceId]
);
await connection.commit();
+11 -11
View File
@@ -91,10 +91,10 @@ function getCanvasSignature(width, height) {
async function fetchPlaylistCanvasSignature(pool, playlistId) {
const [rows] = await pool.query(
`SELECT DISTINCT cs.width AS canvas_width, cs.height AS canvas_height
FROM playlist_slides ps
JOIN slides sl ON sl.id = ps.slide_id
LEFT JOIN slide_templates st ON st.id = sl.template_id
LEFT JOIN canvas_sizes cs ON cs.id = st.canvas_size_id
FROM c_playlist_slides ps
JOIN c_slides sl ON sl.id = ps.slide_id
LEFT JOIN c_templates st ON st.id = sl.template_id
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
WHERE ps.playlist_id = ?
AND cs.width IS NOT NULL
AND cs.height IS NOT NULL`,
@@ -111,7 +111,7 @@ async function fetchPlaylistCanvasSignature(pool, playlistId) {
async function fetchScreensByPlaylistId(connection, playlistId) {
const [rows] = await connection.query(
'SELECT slug FROM screens WHERE playlist_id = ? AND slug IS NOT NULL',
'SELECT slug FROM d_screens WHERE playlist_id = ? AND slug IS NOT NULL',
[playlistId]
);
return rows.map(function (row) {
@@ -122,8 +122,8 @@ async function fetchScreensByPlaylistId(connection, playlistId) {
async function fetchScreensBySlideId(connection, slideId) {
const [rows] = await connection.query(
`SELECT DISTINCT s.slug
FROM screens s
JOIN playlist_slides ps ON ps.playlist_id = s.playlist_id
FROM d_screens s
JOIN c_playlist_slides ps ON ps.playlist_id = s.playlist_id
WHERE ps.slide_id = ?
AND s.slug IS NOT NULL`,
[slideId]
@@ -136,9 +136,9 @@ async function fetchScreensBySlideId(connection, slideId) {
async function fetchScreensByTemplateId(connection, templateId) {
const [rows] = await connection.query(
`SELECT DISTINCT s.slug
FROM screens s
JOIN playlist_slides ps ON ps.playlist_id = s.playlist_id
JOIN slides sl ON sl.id = ps.slide_id
FROM d_screens s
JOIN c_playlist_slides ps ON ps.playlist_id = s.playlist_id
JOIN c_slides sl ON sl.id = ps.slide_id
WHERE sl.template_id = ?
AND s.slug IS NOT NULL`,
[templateId]
@@ -150,7 +150,7 @@ async function fetchScreensByTemplateId(connection, templateId) {
async function fetchOrderedPlaylistSlides(connection, playlistId) {
const [rows] = await connection.query(
'SELECT id, position FROM playlist_slides WHERE playlist_id = ? ORDER BY position ASC, id ASC',
'SELECT id, position FROM c_playlist_slides WHERE playlist_id = ? ORDER BY position ASC, id ASC',
[playlistId]
);
return rows;
+5 -5
View File
@@ -68,7 +68,7 @@ function createPlayerActionService(options) {
}
async function getScreenDeleteBlockMessage(pool, screen, getScreenConnections) {
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM player_onboarding_devices WHERE screen_id = ?', [screen.id]);
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM d_onboarding_devices WHERE screen_id = ?', [screen.id]);
if (Number(rows[0] && rows[0].ref_count) > 0) {
return 'This screen is still linked to onboarding devices.';
}
@@ -89,22 +89,22 @@ function createPlayerActionService(options) {
}
async function getSlideDeleteBlockMessage(pool, slide) {
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM playlist_slides WHERE slide_id = ?', [slide.id]);
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM c_playlist_slides WHERE slide_id = ?', [slide.id]);
return Number(rows[0] && rows[0].ref_count) > 0 ? 'This slide is still used by one or more playlists.' : '';
}
async function getTemplateDeleteBlockMessage(pool, template) {
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM slides WHERE template_id = ?', [template.id]);
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM c_slides WHERE template_id = ?', [template.id]);
return Number(rows[0] && rows[0].ref_count) > 0 ? 'This template is still used by one or more slides.' : '';
}
async function getCanvasSizeDeleteBlockMessage(pool, canvasSize) {
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM slide_templates WHERE canvas_size_id = ?', [canvasSize.id]);
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM c_templates WHERE canvas_size_id = ?', [canvasSize.id]);
return Number(rows[0] && rows[0].ref_count) > 0 ? 'This canvas size is still used by one or more templates.' : '';
}
async function getPlaylistDeleteBlockMessage(pool, playlist) {
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM screens WHERE playlist_id = ?', [playlist.id]);
const [rows] = await pool.query('SELECT COUNT(*) AS ref_count FROM d_screens WHERE playlist_id = ?', [playlist.id]);
return Number(rows[0] && rows[0].ref_count) > 0 ? 'This playlist is still assigned to one or more screens.' : '';
}
+34 -34
View File
@@ -13,16 +13,16 @@ function parseCsvIds(value) {
}
async function fetchPermissions(pool) {
const [rows] = await pool.query('SELECT id, permission_key, name, section_name, description FROM permissions ORDER BY section_name ASC, name ASC');
const [rows] = await pool.query('SELECT id, permission_key, name, section_name, description FROM a_permissions ORDER BY section_name ASC, name ASC');
return rows || [];
}
async function fetchRoles(pool) {
const [rows] = await pool.query(
`SELECT r.id, r.role_key, r.name, r.description, r.created_at, r.modified_at,
(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM roles r
(SELECT COUNT(*) FROM a_user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM a_role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM a_roles r
ORDER BY r.name ASC`
);
return rows || [];
@@ -31,11 +31,11 @@ async function fetchRoles(pool) {
async function fetchRolesPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
const paged = await fetchPagedRows(pool, {
selectSql: `SELECT r.id, r.role_key, r.name, r.description, r.created_at, r.modified_at,
(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM roles r
(SELECT COUNT(*) FROM a_user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM a_role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM a_roles r
ORDER BY r.name ASC`,
countSql: 'SELECT COUNT(*) AS count FROM roles',
countSql: 'SELECT COUNT(*) AS count FROM a_roles',
searchColumns: ['r.role_key', 'r.name', 'r.description'],
searchTerm: searchTerm,
sortColumns: {
@@ -58,9 +58,9 @@ async function fetchRolesPage(pool, page, pageSize, searchTerm, sortKey, sortDir
async function fetchRoleById(pool, roleId) {
const [rows] = await pool.query(
`SELECT r.id, r.role_key, r.name, r.description, r.created_at, r.modified_at,
(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM roles r
(SELECT COUNT(*) FROM a_user_roles ur WHERE ur.role_id = r.id) AS user_count,
(SELECT COUNT(*) FROM a_role_permissions rp WHERE rp.role_id = r.id) AS permission_count
FROM a_roles r
WHERE r.id = ?
LIMIT 1`,
[roleId]
@@ -71,8 +71,8 @@ async function fetchRoleById(pool, roleId) {
async function fetchRolePermissionKeys(pool, roleId) {
const [rows] = await pool.query(
`SELECT p.permission_key
FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
FROM a_role_permissions rp
JOIN a_permissions p ON p.id = rp.permission_id
WHERE rp.role_id = ?
ORDER BY p.section_name ASC, p.name ASC`,
[roleId]
@@ -85,7 +85,7 @@ async function fetchRolePermissionKeys(pool, roleId) {
async function fetchRoleUserIds(pool, roleId) {
const [rows] = await pool.query(
`SELECT ur.user_id
FROM user_roles ur
FROM a_user_roles ur
WHERE ur.role_id = ?
ORDER BY ur.user_id ASC`,
[roleId]
@@ -100,8 +100,8 @@ async function fetchRoleUserIds(pool, roleId) {
async function fetchRolesForUser(pool, userId) {
const [rows] = await pool.query(
`SELECT r.id, r.role_key, r.name, r.description
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
FROM a_user_roles ur
JOIN a_roles r ON r.id = ur.role_id
WHERE ur.user_id = ?
ORDER BY r.name ASC`,
[userId]
@@ -114,13 +114,13 @@ async function fetchUsersWithRoles(pool) {
`SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM users u
FROM a_users u
LEFT JOIN (
SELECT ur.user_id,
GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', ') AS role_names,
GROUP_CONCAT(DISTINCT r.id ORDER BY r.name SEPARATOR ',') AS role_ids_csv
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
FROM a_user_roles ur
JOIN a_roles r ON r.id = ur.role_id
GROUP BY ur.user_id
) role_data ON role_data.user_id = u.id
ORDER BY u.id ASC`
@@ -143,18 +143,18 @@ async function fetchUsersWithRolesPage(pool, page, pageSize, searchTerm, sortKey
selectSql: `SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM users u
FROM a_users u
LEFT JOIN (
SELECT ur.user_id,
GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', ') AS role_names,
GROUP_CONCAT(DISTINCT r.id ORDER BY r.name SEPARATOR ',') AS role_ids_csv
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
FROM a_user_roles ur
JOIN a_roles r ON r.id = ur.role_id
GROUP BY ur.user_id
) role_data ON role_data.user_id = u.id
${whereSql}
ORDER BY u.id ASC`,
countSql: `SELECT COUNT(*) AS count FROM users u ${whereSql}`,
countSql: `SELECT COUNT(*) AS count FROM a_users u ${whereSql}`,
params: queryArgs,
searchColumns: ['u.name', 'u.username', 'role_data.role_names'],
searchTerm: searchTerm,
@@ -190,13 +190,13 @@ async function fetchUserWithRoles(pool, userId) {
`SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM users u
FROM a_users u
LEFT JOIN (
SELECT ur.user_id,
GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', ') AS role_names,
GROUP_CONCAT(DISTINCT r.id ORDER BY r.name SEPARATOR ',') AS role_ids_csv
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
FROM a_user_roles ur
JOIN a_roles r ON r.id = ur.role_id
GROUP BY ur.user_id
) role_data ON role_data.user_id = u.id
WHERE u.id = ?
@@ -221,9 +221,9 @@ async function syncUserRoles(pool, userId, roleIds) {
return Number.isInteger(roleId) && roleId > 0;
})));
await pool.query('DELETE FROM user_roles WHERE user_id = ?', [userId]);
await pool.query('DELETE FROM a_user_roles WHERE user_id = ?', [userId]);
for (const roleId of uniqueRoleIds) {
await pool.query('INSERT IGNORE INTO user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
await pool.query('INSERT IGNORE INTO a_user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
}
}
@@ -234,9 +234,9 @@ async function syncRoleUsers(pool, roleId, userIds) {
return Number.isInteger(userId) && userId > 0;
})));
await pool.query('DELETE FROM user_roles WHERE role_id = ?', [roleId]);
await pool.query('DELETE FROM a_user_roles WHERE role_id = ?', [roleId]);
for (const userId of uniqueUserIds) {
await pool.query('INSERT IGNORE INTO user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
await pool.query('INSERT IGNORE INTO a_user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
}
}
@@ -244,18 +244,18 @@ async function syncRolePermissions(pool, roleId, permissionKeys) {
const uniquePermissionKeys = normalizePermissionKeys(permissionKeys);
if (!uniquePermissionKeys.length) {
await pool.query('DELETE FROM role_permissions WHERE role_id = ?', [roleId]);
await pool.query('DELETE FROM a_role_permissions WHERE role_id = ?', [roleId]);
return;
}
const [permissionRows] = await pool.query('SELECT id, permission_key FROM permissions WHERE permission_key IN (?)', [uniquePermissionKeys]);
const [permissionRows] = await pool.query('SELECT id, permission_key FROM a_permissions WHERE permission_key IN (?)', [uniquePermissionKeys]);
if (permissionRows.length !== uniquePermissionKeys.length) {
throw new Error('One or more selected permissions are invalid.');
}
await pool.query('DELETE FROM role_permissions WHERE role_id = ?', [roleId]);
await pool.query('DELETE FROM a_role_permissions WHERE role_id = ?', [roleId]);
for (const permissionRow of permissionRows) {
await pool.query('INSERT IGNORE INTO role_permissions (role_id, permission_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [roleId, Number(permissionRow.id), null, null]);
await pool.query('INSERT IGNORE INTO a_role_permissions (role_id, permission_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [roleId, Number(permissionRow.id), null, null]);
}
}
+9 -9
View File
@@ -57,8 +57,8 @@ function createSessionService(options) {
const tokenHash = hashSessionToken(token);
const [rows] = await pool.query(
`SELECT s.user_id, u.id, u.name, u.username
FROM auth_sessions s
JOIN users u ON u.id = s.user_id
FROM a_sessions s
JOIN a_users u ON u.id = s.user_id
WHERE s.session_hash = ?
AND s.expires_at > NOW()
LIMIT 1`,
@@ -71,23 +71,23 @@ function createSessionService(options) {
const userId = Number(rows[0].id);
const [roleRows] = await pool.query(
`SELECT r.role_key
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
FROM a_user_roles ur
JOIN a_roles r ON r.id = ur.role_id
WHERE ur.user_id = ?
ORDER BY r.name ASC`,
[userId]
);
const [permissionRows] = await pool.query(
`SELECT p.permission_key
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
FROM a_user_roles ur
JOIN a_role_permissions rp ON rp.role_id = ur.role_id
JOIN a_permissions p ON p.id = rp.permission_id
WHERE ur.user_id = ?
ORDER BY p.section_name ASC, p.name ASC`,
[userId]
);
await pool.query('UPDATE auth_sessions SET last_used_at = CURRENT_TIMESTAMP, modified_by = ? WHERE session_hash = ?', [rows[0].user_id, tokenHash]);
await pool.query('UPDATE a_sessions SET last_used_at = CURRENT_TIMESTAMP, modified_by = ? WHERE session_hash = ?', [rows[0].user_id, tokenHash]);
return Object.assign({}, rows[0], {
roleKeys: roleRows.map(function (row) {
return String(row.role_key || '').trim();
@@ -103,7 +103,7 @@ function createSessionService(options) {
const tokenHash = hashSessionToken(token);
const expiresAt = new Date(Date.now() + sessionMaxAgeMs);
await pool.query(
'INSERT INTO auth_sessions (session_hash, user_id, expires_at, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
'INSERT INTO a_sessions (session_hash, user_id, expires_at, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[tokenHash, userId, expiresAt, userId, userId]
);
return token;
+2 -2
View File
@@ -276,13 +276,13 @@ async function captureSlideThumbnail(options) {
}
});
await pool.query('UPDATE slides SET thumbnail_path = ? WHERE id = ?', [thumbnailPath, slide.id]);
await pool.query('UPDATE c_slides SET thumbnail_path = ? WHERE id = ?', [thumbnailPath, slide.id]);
return {
slideId: slide.id,
thumbnailPath: thumbnailPath,
filePath: filePath,
fullSizePath: fullSizePath,
mediaKind: mediaKind(slide.media_path || '')
mediaKind: mediaKind('')
};
}
+17 -12
View File
@@ -76,11 +76,15 @@ function createUploadSyncService(options) {
return null;
}
const mediaRoot = path.basename(normalizedUploadDir) === 'uploads'
? path.dirname(normalizedUploadDir)
: normalizedUploadDir;
if (relativePath.startsWith('uploads/')) {
return path.join(normalizedUploadDir, relativePath.slice('uploads/'.length));
return path.join(mediaRoot, relativePath);
}
return path.join(path.dirname(normalizedUploadDir), relativePath);
return path.join(mediaRoot, relativePath);
}
function collectUploadReferencesFromValue(value, refs) {
@@ -117,7 +121,6 @@ function createUploadSyncService(options) {
if (!slide) {
return refs;
}
collectUploadReferencesFromValue(slide.media_path, refs);
collectUploadReferencesFromValue(common.parseJsonSafe(slide.content_json), refs);
return refs;
}
@@ -136,7 +139,6 @@ function createUploadSyncService(options) {
if (!payload) {
return refs;
}
collectUploadReferencesFromValue(payload.mediaPath, refs);
collectUploadReferencesFromValue(common.parseJsonSafe(payload.contentJson), refs);
collectUploadReferencesFromValue(payload.backgroundImagePath, refs);
return refs;
@@ -145,16 +147,19 @@ function createUploadSyncService(options) {
async function countUploadReferences(pool, uploadPath) {
const [slideRows] = await pool.query(
`SELECT COUNT(*) AS ref_count
FROM slides
WHERE media_path = ?
OR JSON_SEARCH(COALESCE(content_json, JSON_OBJECT()), 'one', ?) IS NOT NULL`,
[uploadPath, uploadPath]
);
const [templateRows] = await pool.query(
'SELECT COUNT(*) AS ref_count FROM slide_templates WHERE background_image_path = ?',
FROM c_slides
WHERE JSON_SEARCH(COALESCE(content_json, JSON_OBJECT()), 'one', ?) IS NOT NULL`,
[uploadPath]
);
return Number(slideRows[0].ref_count || 0) + Number(templateRows[0].ref_count || 0);
const [thumbnailRows] = await pool.query(
'SELECT COUNT(*) AS ref_count FROM c_slides WHERE thumbnail_path = ?',
[uploadPath]
);
const [templateRows] = await pool.query(
'SELECT COUNT(*) AS ref_count FROM c_templates WHERE background_image_path = ?',
[uploadPath]
);
return Number(slideRows[0].ref_count || 0) + Number(thumbnailRows[0].ref_count || 0) + Number(templateRows[0].ref_count || 0);
}
async function removeUnusedUploadFiles(pool, uploadDir, uploadPaths) {
@@ -232,7 +232,7 @@
var content = document.getElementById('slide-schedule-content');
var triggers = document.querySelectorAll('[data-schedule-config]');
if (!dialog || !content || !triggers.length) {
if (!dialog || !content) {
return;
}
@@ -297,20 +297,22 @@
window.openScheduleModal = openScheduleModal;
window.closeScheduleModal = closeScheduleModal;
Array.prototype.forEach.call(triggers, function (trigger) {
trigger.addEventListener('click', function () {
var url = trigger.getAttribute('data-schedule-config');
var rowKey = trigger.getAttribute('data-schedule-config-row') || '';
var row = trigger.closest('tr[data-playlist-slide-row]');
if (row) {
var params = collectScheduleParams(row, rowKey || row.getAttribute('data-row-key') || '');
url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString();
} else if (rowKey && url.indexOf('row_key=') === -1) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 'row_key=' + encodeURIComponent(rowKey);
}
openScheduleModal(url);
if (triggers.length) {
Array.prototype.forEach.call(triggers, function (trigger) {
trigger.addEventListener('click', function () {
var url = trigger.getAttribute('data-schedule-config');
var rowKey = trigger.getAttribute('data-schedule-config-row') || '';
var row = trigger.closest('tr[data-playlist-slide-row]');
if (row) {
var params = collectScheduleParams(row, rowKey || row.getAttribute('data-row-key') || '');
url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString();
} else if (rowKey && url.indexOf('row_key=') === -1) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 'row_key=' + encodeURIComponent(rowKey);
}
openScheduleModal(url);
});
});
});
}
}
@@ -727,8 +729,6 @@
showVideoDurationButton: slide.showVideoDurationButton,
videoSourcePath: slide.videoSourcePath,
videoDurationSeconds: slide.videoDurationSeconds,
media_type: slide.media_type,
media_path: slide.media_path,
title: slide.title || 'Slide',
duration_seconds: 10,
schedule_mode: 'always',
@@ -808,6 +808,34 @@
return 'Always visible';
}
function collectScheduleParams(row, rowKey) {
var params = new URLSearchParams();
var fields = getScheduleRowFields(row);
params.set('row_key', String(rowKey || ''));
if (fields.mode && fields.mode.value) {
params.set('schedule_mode', String(fields.mode.value || ''));
}
if (fields.startDatetime && fields.startDatetime.value) {
params.set('schedule_start_datetime', String(fields.startDatetime.value || ''));
}
if (fields.endDatetime && fields.endDatetime.value) {
params.set('schedule_end_datetime', String(fields.endDatetime.value || ''));
}
if (fields.startTime && fields.startTime.value) {
params.set('schedule_start_time', String(fields.startTime.value || ''));
}
if (fields.endTime && fields.endTime.value) {
params.set('schedule_end_time', String(fields.endTime.value || ''));
}
if (fields.daysJson && fields.daysJson.value) {
params.set('schedule_days_json', String(fields.daysJson.value || ''));
}
return params;
}
function syncAddSlideOptions() {
var activeSlideIds = {};
var activeCanvasSignatures = {};
@@ -951,14 +979,12 @@
row.setAttribute('data-row-key', rowKey);
row.setAttribute('data-slide-id', String(values.slide_id));
row.setAttribute('data-canvas-signature', String(values.canvas_signature || ''));
row.setAttribute('data-media-type', String(values.media_type || ''));
row.setAttribute('data-media-path', String(values.media_path || ''));
row.setAttribute('data-video-source-path', String(values.videoSourcePath || values.media_path || ''));
row.setAttribute('data-video-source-path', String(values.videoSourcePath || ''));
row.setAttribute('data-video-duration-seconds', String(values.videoDurationSeconds || ''));
row.setAttribute('data-use-video-duration', String(Boolean(values.useVideoDuration)));
row.setAttribute('data-use-video-duration-state', String(Boolean(values.useVideoDuration)));
var durationActionMarkup = buildVideoDurationButtonMarkup(
Boolean(values.useVideoDuration),
Boolean(values.showVideoDurationButton) || String(values.media_type || '').trim() === 'video'
Boolean(values.showVideoDurationButton)
);
row.innerHTML = '' +
buildPlaylistOrderCellMarkup() +
@@ -1071,7 +1097,7 @@
}
if (row) {
row.setAttribute('data-use-video-duration', isPressed ? 'true' : 'false');
row.setAttribute('data-use-video-duration-state', isPressed ? 'true' : 'false');
}
if (!row || !input) {
@@ -1096,10 +1122,9 @@
}
async function applyVideoDurationToRow(row, button) {
var mediaType = String(row && row.getAttribute('data-media-type') || '').trim().toLowerCase();
var mediaPath = String(row && (row.getAttribute('data-video-source-path') || row.getAttribute('data-media-path')) || '').trim();
var mediaPath = String(row && row.getAttribute('data-video-source-path') || '').trim();
var input = row ? row.querySelector('[name="duration_seconds[]"]') : null;
var isVideoRow = mediaType === 'video' || Boolean(mediaPath);
var isVideoRow = Boolean(mediaPath);
var storedDuration = Number(row && row.getAttribute('data-video-duration-seconds') || 0);
if (!isVideoRow || !input) {
@@ -1160,7 +1185,7 @@
var removeButton = event.target.closest('[data-playlist-remove-row]');
var scheduleButton = event.target.closest('[data-schedule-config]');
var durationButton = event.target.closest('[data-use-video-duration]');
var durationButton = event.target.closest('.playlist-use-video-duration');
var row = getPlaylistSlideRow(event.target);
if (!row) {
@@ -1177,7 +1202,11 @@
if (scheduleButton) {
event.preventDefault();
if (typeof window.openScheduleModal === 'function') {
window.openScheduleModal(scheduleButton.getAttribute('data-schedule-config') + '?row_key=' + encodeURIComponent(getPlaylistSlideRowKey(row)));
var rowKey = getPlaylistSlideRowKey(row);
var url = scheduleButton.getAttribute('data-schedule-config') || '';
var params = collectScheduleParams(row, rowKey);
url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString();
window.openScheduleModal(url);
}
return;
}
@@ -1252,7 +1281,7 @@
var useVideoDuration = Boolean(editedButton && editedButton.getAttribute('aria-pressed') === 'true');
if (editedRow) {
editedRow.setAttribute('data-use-video-duration', useVideoDuration ? 'true' : 'false');
editedRow.setAttribute('data-use-video-duration-state', useVideoDuration ? 'true' : 'false');
}
if (useVideoDuration && editedMirror) {
+6 -1
View File
@@ -68,7 +68,12 @@
if (typeof window.webHandleDashboardState === 'function') {
window.webHandleDashboardState(payload.state);
}
updateSidebarStatus(Boolean(payload.state && payload.state.playerServiceConnected), 'Live player feed');
var screenCount = Array.isArray(payload.state && payload.state.screens) ? payload.state.screens.length : 0;
if (!screenCount) {
updateSidebarStatus('unknown', 'No screens configured');
} else {
updateSidebarStatus(Boolean(payload.state && payload.state.playerServiceConnected), 'Live player feed');
}
}
} catch (_error) {
// Ignore malformed dashboard payloads.
+5 -5
View File
@@ -20,12 +20,12 @@ module.exports = function registerAdminAccountRoutes(app, deps) {
return res.status(400).send('Name is required.');
}
if (await common.fetchDuplicateName(pool, 'users', name, req.currentUser.id)) {
if (await common.fetchDuplicateName(pool, 'a_users', name, req.currentUser.id)) {
return res.redirect('/account?message=' + encodeURIComponent('That name already exists.'));
}
const actorId = getAuditUserId(req);
const [result] = await pool.query('UPDATE users SET name = ?, modified_by = ? WHERE id = ?', [name, actorId, req.currentUser.id]);
const [result] = await pool.query('UPDATE a_users SET name = ?, modified_by = ? WHERE id = ?', [name, actorId, req.currentUser.id]);
if (!result.affectedRows) {
return res.status(404).send('User not found.');
}
@@ -42,7 +42,7 @@ module.exports = function registerAdminAccountRoutes(app, deps) {
const newPassword = String(req.body.new_password || '');
const confirmPassword = String(req.body.confirm_password || '');
const [rows] = await pool.query('SELECT id, name, username, password_hash, password_salt, password_iterations FROM users WHERE id = ? LIMIT 1', [req.currentUser.id]);
const [rows] = await pool.query('SELECT id, name, username, password_hash, password_salt, password_iterations FROM a_users WHERE id = ? LIMIT 1', [req.currentUser.id]);
const user = rows[0] || null;
if (!user) {
return res.status(404).send('User not found.');
@@ -59,10 +59,10 @@ module.exports = function registerAdminAccountRoutes(app, deps) {
const passwordRecord = hashPassword(newPassword);
await pool.query(
'UPDATE users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?',
'UPDATE a_users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?',
[passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, getAuditUserId(req), user.id]
);
await pool.query('DELETE FROM auth_sessions WHERE user_id = ?', [user.id]);
await pool.query('DELETE FROM a_sessions WHERE user_id = ?', [user.id]);
const token = await createUserSession(pool, user.id);
setSessionCookie(res, token);
+14 -5
View File
@@ -4,6 +4,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
const getScreenConnections = deps.getScreenConnections;
const isClientNameAvailable = deps.isClientNameAvailable;
const withClientNameReservation = deps.withClientNameReservation;
const broadcastDashboardState = deps.broadcastDashboardState;
const requirePermission = deps.requirePermission;
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
@@ -22,7 +23,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
return res.status(400).json({ error: 'Command is required' });
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [slug]);
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug = ?', [slug]);
if (!screenRows.length) {
return res.status(404).json({ error: 'Screen not found' });
}
@@ -39,7 +40,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
const [currentRows] = await pool.query(
`SELECT client_name
FROM player_onboarding_devices
FROM d_onboarding_devices
WHERE device_id = ?
LIMIT 1`,
[deviceId]
@@ -65,7 +66,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
return withClientNameReservation(pool, clientName, async function () {
let liveConnections = [];
try {
const [screenSlugs] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
const [screenSlugs] = await pool.query('SELECT slug FROM d_screens ORDER BY slug ASC');
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
const screenSlug = String(row && row.slug ? row.slug : '').trim();
if (!screenSlug || typeof getScreenConnections !== 'function') {
@@ -96,6 +97,10 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
deviceId: deviceId || null
}, connectionId || deviceId || undefined);
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json({
screen: screenRows[0],
screenSlug: slug,
@@ -109,8 +114,8 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
}
const [updateResult] = await pool.query(
`UPDATE player_onboarding_devices pod
JOIN screens s ON s.id = pod.screen_id
`UPDATE d_onboarding_devices pod
JOIN d_screens s ON s.id = pod.screen_id
SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP
WHERE s.slug = ? AND pod.device_id = ?`,
[clientName, slug, deviceId]
@@ -142,6 +147,10 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
? await forwardPlayerCommand(slug, commandPayload, connectionId)
: await forwardPlayerCommand(slug, commandPayload);
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json(Object.assign({
screen: screenRows[0],
screenSlug: slug,
+46 -27
View File
@@ -33,6 +33,24 @@ module.exports = function registerAdminContentRoutes(app, deps) {
throw new Error('registerAdminContentRoutes requires the content route dependencies.');
}
async function removeMediaFile(uploadPath) {
const normalizedPath = String(uploadPath || '').trim();
if (!normalizedPath || !normalizedPath.startsWith('/media/')) {
return;
}
const mediaRoot = path.dirname(deps.uploadDir);
const filePath = path.join(mediaRoot, normalizedPath.replace(/^\/+media\//, ''));
try {
await fs.promises.unlink(filePath);
} catch (error) {
if (!error || error.code !== 'ENOENT') {
throw error;
}
}
}
function requireSlideUploadPermission(req, res, next) {
if (!req.currentUser) {
return res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.'));
@@ -105,12 +123,12 @@ module.exports = function registerAdminContentRoutes(app, deps) {
}
async function fetchTemplateSlideCount(templateId) {
const [rows] = await pool.query('SELECT COUNT(*) AS slide_count FROM slides WHERE template_id = ?', [templateId]);
const [rows] = await pool.query('SELECT COUNT(*) AS slide_count FROM c_slides WHERE template_id = ?', [templateId]);
return Number(rows[0] && rows[0].slide_count) || 0;
}
async function fetchTemplateRegionUsage(template) {
const [slides] = await pool.query('SELECT content_json FROM slides WHERE template_id = ?', [template.id]);
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE template_id = ?', [template.id]);
const usage = new Set();
const regionKeys = new Set((Array.isArray(template && template.regions) ? template.regions : [])
.map((region) => String(region && region.region_key || '').trim())
@@ -136,7 +154,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
}
async function fetchSlidesByTemplateId(templateId) {
const [slides] = await pool.query('SELECT id, thumbnail_path FROM slides WHERE template_id = ? ORDER BY id ASC', [templateId]);
const [slides] = await pool.query('SELECT id, thumbnail_path FROM c_slides WHERE template_id = ? ORDER BY id ASC', [templateId]);
return slides;
}
@@ -195,7 +213,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
async function canvasSizeExists(width, height, ignoreId) {
const params = [width, height];
let query = 'SELECT COUNT(*) AS count FROM canvas_sizes WHERE width = ? AND height = ?';
let query = 'SELECT COUNT(*) AS count FROM c_canvas_sizes WHERE width = ? AND height = ?';
if (ignoreId) {
query += ' AND id <> ?';
params.push(ignoreId);
@@ -214,8 +232,8 @@ module.exports = function registerAdminContentRoutes(app, deps) {
const [rows] = await pool.query(
`SELECT s.slug, COUNT(ps.id) AS slide_count
FROM screens s
LEFT JOIN playlist_slides ps ON ps.playlist_id = s.playlist_id
FROM d_screens s
LEFT JOIN c_playlist_slides ps ON ps.playlist_id = s.playlist_id
WHERE s.slug IN (?)
GROUP BY s.id, s.slug`,
[uniqueSlugs]
@@ -334,13 +352,13 @@ module.exports = function registerAdminContentRoutes(app, deps) {
try {
await validateUploadedFiles(req.files || []);
const payload = await common.buildSlidePayload(pool, req, null);
if (await common.fetchDuplicateName(pool, 'slides', payload.title, null, 'title')) {
if (await common.fetchDuplicateName(pool, 'c_slides', payload.title, null, 'title')) {
return res.status(400).send('A slide with that title already exists.');
}
const actorId = getAuditUserId(req);
const [result] = await pool.query(
'INSERT INTO slides (title, body, template_id, content_json, media_path, media_type, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[payload.title, payload.body, payload.templateId, payload.contentJson, payload.mediaPath, payload.mediaType, actorId, actorId]
'INSERT INTO c_slides (title, template_id, content_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[payload.title, payload.templateId, payload.contentJson, actorId, actorId]
);
await syncPlaylistUploadsOnChange({
key: 'slide:create:' + result.insertId,
@@ -372,14 +390,14 @@ module.exports = function registerAdminContentRoutes(app, deps) {
const screenSlideCounts = await fetchScreenSlideCountsBySlug(affectedScreens);
const existingUploadRefs = collectUploadReferencesFromSlide(slide);
const payload = await common.buildSlidePayload(pool, req, slide);
if (await common.fetchDuplicateName(pool, 'slides', payload.title, slide.id, 'title')) {
if (await common.fetchDuplicateName(pool, 'c_slides', payload.title, slide.id, 'title')) {
return res.status(400).send('A slide with that title already exists.');
}
const nextUploadRefs = collectUploadReferencesFromPayload(payload);
const actorId = getAuditUserId(req);
await pool.query(
'UPDATE slides SET title = ?, body = ?, template_id = ?, content_json = ?, media_path = ?, media_type = ?, modified_by = ? WHERE id = ?',
[payload.title, payload.body, payload.templateId, payload.contentJson, payload.mediaPath, payload.mediaType, actorId, slide.id]
'UPDATE c_slides SET title = ?, template_id = ?, content_json = ?, modified_by = ? WHERE id = ?',
[payload.title, payload.templateId, payload.contentJson, actorId, slide.id]
);
await syncPlaylistUploadsOnChange({
key: 'slide:update:' + slide.id,
@@ -418,7 +436,8 @@ module.exports = function registerAdminContentRoutes(app, deps) {
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
const screenSlideCounts = await fetchScreenSlideCountsBySlug(affectedScreens);
const uploadRefs = collectUploadReferencesFromSlide(slide);
await pool.query('DELETE FROM slides WHERE id = ?', [slide.id]);
await pool.query('DELETE FROM c_slides WHERE id = ?', [slide.id]);
await removeMediaFile(slide.thumbnail_path);
await syncPlaylistUploadsOnChange({
key: 'slide:delete:' + slide.id,
pool: pool,
@@ -463,18 +482,18 @@ module.exports = function registerAdminContentRoutes(app, deps) {
app.post('/templates', requirePermission('templates.create'), upload.any(), async function (req, res, next) {
try {
const payload = await common.buildTemplatePayload(pool, req, null);
if (await common.fetchDuplicateName(pool, 'slide_templates', payload.name)) {
if (await common.fetchDuplicateName(pool, 'c_templates', payload.name)) {
return res.redirect('/templates/new?message=' + encodeURIComponent('A template with that name already exists.'));
}
const actorId = getAuditUserId(req);
const [result] = await pool.query(
'INSERT INTO slide_templates (name, canvas_size_id, background_image_path, background_color, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)',
'INSERT INTO c_templates (name, canvas_size_id, background_image_path, background_color, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)',
[payload.name, payload.canvasSizeId, payload.backgroundImagePath, payload.backgroundColor, actorId, actorId]
);
for (let i = 0; i < payload.regions.length; i += 1) {
const region = payload.regions[i];
await pool.query(
'INSERT INTO slide_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[result.insertId, region.region_key, region.region_type, region.label, region.font_family, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
);
}
@@ -517,7 +536,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
template.region_usage = await fetchTemplateRegionUsage(template);
const existingUploadRefs = collectUploadReferencesFromTemplate(template);
const payload = await common.buildTemplatePayload(pool, req, template);
if (await common.fetchDuplicateName(pool, 'slide_templates', payload.name, template.id)) {
if (await common.fetchDuplicateName(pool, 'c_templates', payload.name, template.id)) {
return res.redirect('/templates/' + template.id + '/edit?message=' + encodeURIComponent('A template with that name already exists.'));
}
const regionDeleteBlockMessage = await getTemplateRegionDeleteBlockMessage(template, payload.regions);
@@ -528,14 +547,14 @@ module.exports = function registerAdminContentRoutes(app, deps) {
const affectedScreens = await fetchScreensByTemplateId(pool, template.id);
const actorId = getAuditUserId(req);
await pool.query(
'UPDATE slide_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, background_color = ?, modified_by = ? WHERE id = ?',
'UPDATE c_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, background_color = ?, modified_by = ? WHERE id = ?',
[payload.name, payload.canvasSizeId, payload.backgroundImagePath, payload.backgroundColor, actorId, template.id]
);
await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]);
await pool.query('DELETE FROM c_template_regions WHERE template_id = ?', [template.id]);
for (let i = 0; i < payload.regions.length; i += 1) {
const region = payload.regions[i];
await pool.query(
'INSERT INTO slide_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[template.id, region.region_key, region.region_type, region.label, region.font_family, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
);
}
@@ -575,9 +594,9 @@ module.exports = function registerAdminContentRoutes(app, deps) {
}
const uploadRefs = collectUploadReferencesFromTemplate(template);
const affectedScreens = await fetchScreensByTemplateId(pool, template.id);
await pool.query('UPDATE slides SET template_id = NULL, modified_by = ? WHERE template_id = ?', [getAuditUserId(req), template.id]);
await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]);
await pool.query('DELETE FROM slide_templates WHERE id = ?', [template.id]);
await pool.query('UPDATE c_slides SET template_id = NULL, modified_by = ? WHERE template_id = ?', [getAuditUserId(req), template.id]);
await pool.query('DELETE FROM c_template_regions WHERE template_id = ?', [template.id]);
await pool.query('DELETE FROM c_templates WHERE id = ?', [template.id]);
await syncPlaylistUploadsOnChange({
key: 'template:delete:' + template.id,
pool: pool,
@@ -621,7 +640,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
return res.redirect('/canvas-sizes/new?message=' + encodeURIComponent('That canvas size already exists.'));
}
const actorId = getAuditUserId(req);
const [result] = await pool.query('INSERT INTO canvas_sizes (name, width, height, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [payload.name, payload.width, payload.height, actorId, actorId]);
const [result] = await pool.query('INSERT INTO c_canvas_sizes (name, width, height, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [payload.name, payload.width, payload.height, actorId, actorId]);
redirectAfterSave(req, res, '/canvas-sizes/' + result.insertId + '/edit', {
closeUrl: '/canvas-sizes',
newUrl: '/canvas-sizes/new',
@@ -657,7 +676,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
if (await canvasSizeExists(payload.width, payload.height, canvasSize.id)) {
return res.redirect('/canvas-sizes/' + canvasSize.id + '/edit?message=' + encodeURIComponent('That canvas size already exists.'));
}
await pool.query('UPDATE canvas_sizes SET name = ?, width = ?, height = ?, modified_by = ? WHERE id = ?', [payload.name, payload.width, payload.height, getAuditUserId(req), canvasSize.id]);
await pool.query('UPDATE c_canvas_sizes SET name = ?, width = ?, height = ?, modified_by = ? WHERE id = ?', [payload.name, payload.width, payload.height, getAuditUserId(req), canvasSize.id]);
redirectAfterSave(req, res, '/canvas-sizes', {
closeUrl: '/canvas-sizes',
newUrl: '/canvas-sizes/new',
@@ -678,8 +697,8 @@ module.exports = function registerAdminContentRoutes(app, deps) {
if (blockMessage) {
return res.redirect('/canvas-sizes?message=' + encodeURIComponent(blockMessage));
}
await pool.query('UPDATE slide_templates SET canvas_size_id = NULL, modified_by = ? WHERE canvas_size_id = ?', [getAuditUserId(req), canvasSize.id]);
await pool.query('DELETE FROM canvas_sizes WHERE id = ?', [canvasSize.id]);
await pool.query('UPDATE c_templates SET canvas_size_id = NULL, modified_by = ? WHERE canvas_size_id = ?', [getAuditUserId(req), canvasSize.id]);
await pool.query('DELETE FROM c_canvas_sizes WHERE id = ?', [canvasSize.id]);
res.redirect('/canvas-sizes?message=' + encodeURIComponent('Canvas size deleted.'));
} catch (error) {
next(error);
+9 -9
View File
@@ -40,7 +40,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
await connection.beginTransaction();
await connection.query(
'UPDATE api_sources SET last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
'UPDATE i_api_sources SET last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
[new Date(), pullError || null, responseDetails ? responseDetails.responseStatus : null, responseDetails ? responseDetails.responseContentType : null, responseDetails ? responseDetails.responseJson : null, actorId, apiSourceId]
);
await connection.commit();
@@ -70,7 +70,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
await connection.beginTransaction();
await connection.query(
'UPDATE rss_feeds SET modified_by = ? WHERE id = ?',
'UPDATE i_rss_feeds SET modified_by = ? WHERE id = ?',
[actorId, rssFeedId]
);
if (replaceRssFeedItems) {
@@ -229,13 +229,13 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const connection = await pool.getConnection();
try {
const payload = common.buildApiSourcePayload(req, null);
if (await common.fetchDuplicateName(pool, 'api_sources', payload.name)) {
if (await common.fetchDuplicateName(pool, 'i_api_sources', payload.name)) {
return res.redirect('/data-sources/api-sources/new?message=' + encodeURIComponent('An API source with that name already exists.'));
}
const actorId = getAuditUserId(req);
await connection.beginTransaction();
const [result] = await connection.query(
'INSERT INTO api_sources (name, api_url, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO i_api_sources (name, api_url, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[payload.name, payload.apiUrl, payload.updateIntervalValue, payload.updateIntervalUnit, null, null, null, null, null, actorId, actorId]
);
await connection.commit();
@@ -318,7 +318,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
await connection.query(
'UPDATE api_sources SET name = ?, api_url = ?, update_interval_value = ?, update_interval_unit = ?, last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
'UPDATE i_api_sources SET name = ?, api_url = ?, update_interval_value = ?, update_interval_unit = ?, last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
[payload.name, payload.apiUrl, payload.updateIntervalValue, payload.updateIntervalUnit, null, null, null, null, null, actorId, apiSource.id]
);
await connection.commit();
@@ -366,7 +366,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM api_sources WHERE id = ?', [apiSource.id]);
await connection.query('DELETE FROM i_api_sources WHERE id = ?', [apiSource.id]);
await connection.commit();
removeRecurringRefresh('api-source', apiSource.id);
} catch (error) {
@@ -411,7 +411,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
const [result] = await connection.query(
'INSERT INTO rss_feeds (name, feed_url, update_interval_value, update_interval_unit, item_limit, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO i_rss_feeds (name, feed_url, update_interval_value, update_interval_unit, item_limit, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
[payload.name, payload.feedUrl, payload.updateIntervalValue, payload.updateIntervalUnit, payload.itemLimit, actorId, actorId]
);
await connection.commit();
@@ -493,7 +493,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
await connection.query(
'UPDATE rss_feeds SET name = ?, feed_url = ?, update_interval_value = ?, update_interval_unit = ?, item_limit = ?, modified_by = ? WHERE id = ?',
'UPDATE i_rss_feeds SET name = ?, feed_url = ?, update_interval_value = ?, update_interval_unit = ?, item_limit = ?, modified_by = ? WHERE id = ?',
[payload.name, payload.feedUrl, payload.updateIntervalValue, payload.updateIntervalUnit, payload.itemLimit, actorId, rssFeed.id]
);
await connection.commit();
@@ -541,7 +541,7 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM rss_feeds WHERE id = ?', [rssFeed.id]);
await connection.query('DELETE FROM i_rss_feeds WHERE id = ?', [rssFeed.id]);
await connection.commit();
removeRecurringRefresh('rss-feed', rssFeed.id);
} catch (error) {
+23 -23
View File
@@ -22,7 +22,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const requirePermission = deps.requirePermission;
async function fetchAllScreenSlugs() {
const [rows] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
const [rows] = await pool.query('SELECT slug FROM d_screens ORDER BY slug ASC');
return (rows || [])
.map(function (row) {
return String(row && row.slug ? row.slug : '').trim();
@@ -89,11 +89,11 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (!name) {
return res.status(400).send('Playlist name is required.');
}
if (await common.fetchDuplicateName(pool, 'playlists', name)) {
if (await common.fetchDuplicateName(pool, 'c_playlists', name)) {
return res.status(400).send('A playlist with that name already exists.');
}
const actorId = getAuditUserId(req);
const [result] = await pool.query('INSERT INTO playlists (name, fade_between_slides, skip_unavailable_rtmp, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, actorId]);
const [result] = await pool.query('INSERT INTO c_playlists (name, fade_between_slides, skip_unavailable_rtmp, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, actorId]);
redirectAfterSave(req, res, '/playlists?edit=' + result.insertId, {
closeUrl: '/playlists',
newUrl: '/playlists/new',
@@ -117,7 +117,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (!playlist) {
return res.status(404).send('Playlist not found');
}
if (await common.fetchDuplicateName(pool, 'playlists', name, playlist.id)) {
if (await common.fetchDuplicateName(pool, 'c_playlists', name, playlist.id)) {
return res.status(400).send('A playlist with that name already exists.');
}
@@ -215,9 +215,9 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (normalizedSlides.length) {
const [slides] = await connection.query(
`SELECT sl.id, cs.width AS canvas_width, cs.height AS canvas_height
FROM slides sl
LEFT JOIN slide_templates st ON st.id = sl.template_id
LEFT JOIN canvas_sizes cs ON cs.id = st.canvas_size_id
FROM c_slides sl
LEFT JOIN c_templates st ON st.id = sl.template_id
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
WHERE sl.id IN (?)`,
[normalizedSlides.map(function (item) { return item.slideId; })]
);
@@ -237,12 +237,12 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
await connection.query('UPDATE playlists SET name = ?, fade_between_slides = ?, skip_unavailable_rtmp = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, playlist.id]);
await connection.query('DELETE FROM playlist_slides WHERE playlist_id = ?', [playlist.id]);
await connection.query('UPDATE c_playlists SET name = ?, fade_between_slides = ?, skip_unavailable_rtmp = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, playlist.id]);
await connection.query('DELETE FROM c_playlist_slides WHERE playlist_id = ?', [playlist.id]);
for (let i = 0; i < normalizedSlides.length; i += 1) {
const item = normalizedSlides[i];
await connection.query(
'INSERT INTO playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, schedule_mode, schedule_start_datetime, schedule_end_datetime, schedule_start_time, schedule_end_time, schedule_days_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO c_playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, schedule_mode, schedule_start_datetime, schedule_end_datetime, schedule_start_time, schedule_end_time, schedule_days_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
playlist.id,
item.slideId,
@@ -290,7 +290,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (blockMessage) {
return res.redirect('/playlists?message=' + encodeURIComponent(blockMessage));
}
await pool.query('DELETE FROM playlists WHERE id = ?', [playlist.id]);
await pool.query('DELETE FROM c_playlists WHERE id = ?', [playlist.id]);
res.redirect('/playlists?message=' + encodeURIComponent('Playlist deleted.'));
} catch (error) {
next(error);
@@ -321,10 +321,10 @@ module.exports = function registerAdminManageRoutes(app, deps) {
}
const durationValue = Number(req.body.duration_seconds || 10);
const durationSeconds = Number.isFinite(durationValue) ? Math.max(0.001, Math.round(durationValue * 1000) / 1000) : 10;
const [positionRows] = await pool.query('SELECT COALESCE(MAX(position), -1) AS max_position FROM playlist_slides WHERE playlist_id = ?', [playlist.id]);
const [positionRows] = await pool.query('SELECT COALESCE(MAX(position), -1) AS max_position FROM c_playlist_slides WHERE playlist_id = ?', [playlist.id]);
const nextPosition = Number(positionRows[0].max_position) + 1;
const actorId = getAuditUserId(req);
await pool.query('INSERT INTO playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)', [playlist.id, slideId, nextPosition, durationSeconds, 0, actorId, actorId]);
await pool.query('INSERT INTO c_playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)', [playlist.id, slideId, nextPosition, durationSeconds, 0, actorId, actorId]);
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
await broadcastDashboardState();
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide added to playlist.'));
@@ -344,7 +344,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const durationSeconds = Number.isFinite(durationValue) ? Math.max(0.001, Math.round(durationValue * 1000) / 1000) : 10;
const actorId = getAuditUserId(req);
const [result] = await pool.query(
'UPDATE playlist_slides SET duration_seconds = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
'UPDATE c_playlist_slides SET duration_seconds = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
[durationSeconds, actorId, playlistSlideId, playlist.id]
);
if (!result.affectedRows) {
@@ -391,8 +391,8 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const swapSlide = orderedSlides[swapIndex];
const actorId = getAuditUserId(req);
await connection.query('UPDATE playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [swapSlide.position, actorId, currentSlide.id, playlist.id]);
await connection.query('UPDATE playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [currentSlide.position, actorId, swapSlide.id, playlist.id]);
await connection.query('UPDATE c_playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [swapSlide.position, actorId, currentSlide.id, playlist.id]);
await connection.query('UPDATE c_playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [currentSlide.position, actorId, swapSlide.id, playlist.id]);
await connection.commit();
await notifyPlayerScreens(await fetchScreensByPlaylistId(connection, playlist.id), 'refresh');
await broadcastDashboardState();
@@ -508,7 +508,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const actorId = getAuditUserId(req);
const [result] = await pool.query(
'UPDATE playlist_slides SET schedule_mode = ?, schedule_start_datetime = ?, schedule_end_datetime = ?, schedule_start_time = ?, schedule_end_time = ?, schedule_days_json = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
'UPDATE c_playlist_slides SET schedule_mode = ?, schedule_start_datetime = ?, schedule_end_datetime = ?, schedule_start_time = ?, schedule_end_time = ?, schedule_days_json = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
[scheduleMode, scheduleStartDatetime, scheduleEndDatetime, scheduleStartTime, scheduleEndTime, scheduleDaysJson, actorId, playlistSlideId, playlist.id]
);
if (!result.affectedRows) {
@@ -528,7 +528,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (!playlist) {
return res.status(404).send('Playlist not found');
}
await pool.query('DELETE FROM playlist_slides WHERE id = ? AND playlist_id = ?', [Number(req.params.playlistSlideId), playlist.id]);
await pool.query('DELETE FROM c_playlist_slides WHERE id = ? AND playlist_id = ?', [Number(req.params.playlistSlideId), playlist.id]);
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
await broadcastDashboardState();
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide removed.'));
@@ -552,14 +552,14 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (!name) {
return res.status(400).send('Screen name is required.');
}
if (await common.fetchDuplicateName(pool, 'screens', name)) {
if (await common.fetchDuplicateName(pool, 'd_screens', name)) {
return res.status(400).send('A screen with that name already exists.');
}
const slugInput = String(req.body.slug || '').trim();
const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null;
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name));
const actorId = getAuditUserId(req);
const [result] = await pool.query('INSERT INTO screens (name, slug, playlist_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, slug, playlistId, actorId, actorId]);
const [result] = await pool.query('INSERT INTO d_screens (name, slug, playlist_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, slug, playlistId, actorId, actorId]);
redirectAfterSave(req, res, '/screens?edit=' + result.insertId, {
closeUrl: '/screens',
newUrl: '/screens/new',
@@ -580,7 +580,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (!screen) {
return res.status(404).send('Screen not found');
}
if (await common.fetchDuplicateName(pool, 'screens', name, screen.id)) {
if (await common.fetchDuplicateName(pool, 'd_screens', name, screen.id)) {
return res.status(400).send('A screen with that name already exists.');
}
const slugInput = String(req.body.slug || '').trim();
@@ -588,7 +588,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const previousPlaylistId = screen.playlist_id;
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name), screen.id);
const previousSlug = String(screen.slug || '').trim();
await pool.query('UPDATE screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, getAuditUserId(req), screen.id]);
await pool.query('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, getAuditUserId(req), screen.id]);
if (previousPlaylistId !== playlistId && previousSlug) {
await notifyPlayerScreens([previousSlug], 'refresh');
}
@@ -618,7 +618,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
if (blockMessage) {
return res.redirect('/screens?message=' + encodeURIComponent(blockMessage));
}
await pool.query('DELETE FROM screens WHERE id = ?', [screen.id]);
await pool.query('DELETE FROM d_screens WHERE id = ?', [screen.id]);
res.redirect('/screens?message=' + encodeURIComponent('Screen deleted.'));
} catch (error) {
next(error);
+5 -5
View File
@@ -46,7 +46,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
let suffix = 2;
while (true) {
const [rows] = await pool.query('SELECT id FROM roles WHERE role_key = ? LIMIT 1', [candidate]);
const [rows] = await pool.query('SELECT id FROM a_roles WHERE role_key = ? LIMIT 1', [candidate]);
if (!rows.length) {
return candidate;
}
@@ -201,7 +201,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
if (!name) {
return res.status(400).send(pages.renderRbacAddPage('Role name is required.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, 'warning'));
}
if (await common.fetchDuplicateName(pool, 'roles', name)) {
if (await common.fetchDuplicateName(pool, 'a_roles', name)) {
return res.status(400).send(pages.renderRbacAddPage('A role with that name already exists.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, 'warning'));
}
@@ -218,7 +218,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
try {
await connection.beginTransaction();
const [result] = await connection.query(
'INSERT INTO roles (role_key, name, description, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
'INSERT INTO a_roles (role_key, name, description, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[roleKey, name, description || null, actorId, actorId]
);
insertedRoleId = Number(result.insertId);
@@ -325,7 +325,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
try {
await connection.beginTransaction();
await connection.query(
'UPDATE roles SET name = ?, description = ?, modified_by = ? WHERE id = ?',
'UPDATE a_roles SET name = ?, description = ?, modified_by = ? WHERE id = ?',
[name, description || null, getAuditUserId(req), roleId]
);
if (shouldSyncPermissions) {
@@ -445,7 +445,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
return res.redirect('/rbac?message=' + encodeURIComponent('Remove all users from this role before deleting it.'));
}
await pool.query('DELETE FROM roles WHERE id = ?', [roleId]);
await pool.query('DELETE FROM a_roles WHERE id = ?', [roleId]);
res.redirect('/rbac?message=' + encodeURIComponent('Role deleted.'));
} catch (error) {
next(error);
+13 -13
View File
@@ -147,11 +147,11 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
if (!roleCheck.ok) {
return renderValidationError(roleCheck.message);
}
if (await common.fetchDuplicateName(pool, 'users', name)) {
if (await common.fetchDuplicateName(pool, 'a_users', name)) {
return renderValidationError('That name already exists.');
}
const [existingRows] = await connection.query('SELECT id FROM users WHERE username = ? LIMIT 1', [username]);
const [existingRows] = await connection.query('SELECT id FROM a_users WHERE username = ? LIMIT 1', [username]);
if (existingRows.length) {
return renderValidationError('That username already exists.');
}
@@ -160,7 +160,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
const [result] = await connection.query(
'INSERT INTO users (name, username, password_hash, password_salt, password_iterations, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO a_users (name, username, password_hash, password_salt, password_iterations, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
[name, username, passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, actorId, actorId]
);
await rbacData.syncUserRoles(connection, result.insertId, roleCheck.roleIds);
@@ -188,7 +188,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
return res.redirect('/account?message=' + encodeURIComponent('Use My Account to update your own login details.'));
}
const [rows] = await pool.query('SELECT id FROM users WHERE id = ? LIMIT 1', [userId]);
const [rows] = await pool.query('SELECT id FROM a_users WHERE id = ? LIMIT 1', [userId]);
if (!rows.length) {
return res.status(404).send('User not found.');
}
@@ -221,7 +221,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
return res.redirect('/account?message=' + encodeURIComponent('Use My Account to update your own username or password.'));
}
const [userRows] = await pool.query('SELECT id, username FROM users WHERE id = ? LIMIT 1', [userId]);
const [userRows] = await pool.query('SELECT id, username FROM a_users WHERE id = ? LIMIT 1', [userId]);
if (!userRows.length) {
return res.status(404).send('User not found.');
}
@@ -231,16 +231,16 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
return res.redirect('/users/' + userId + '/edit?message=' + encodeURIComponent('Username is required.'));
}
if (await common.fetchDuplicateName(pool, 'users', name, userId)) {
if (await common.fetchDuplicateName(pool, 'a_users', name, userId)) {
return res.redirect('/users/' + userId + '/edit?message=' + encodeURIComponent('That name already exists.'));
}
const [existingRows] = await pool.query('SELECT id FROM users WHERE username = ? AND id <> ? LIMIT 1', [username, userId]);
const [existingRows] = await pool.query('SELECT id FROM a_users WHERE username = ? AND id <> ? LIMIT 1', [username, userId]);
if (existingRows.length) {
return res.redirect('/users/' + userId + '/edit?message=' + encodeURIComponent('That username already exists.'));
}
const [result] = await pool.query('UPDATE users SET name = ?, username = ?, modified_by = ? WHERE id = ?', [name, username, getAuditUserId(req), userId]);
const [result] = await pool.query('UPDATE a_users SET name = ?, username = ?, modified_by = ? WHERE id = ?', [name, username, getAuditUserId(req), userId]);
if (!result.affectedRows) {
return res.status(404).send('User not found.');
}
@@ -269,17 +269,17 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
return res.redirect('/users?message=' + encodeURIComponent('Passwords do not match.'));
}
const [rows] = await pool.query('SELECT id FROM users WHERE id = ? LIMIT 1', [userId]);
const [rows] = await pool.query('SELECT id FROM a_users WHERE id = ? LIMIT 1', [userId]);
if (!rows.length) {
return res.status(404).send('User not found.');
}
const passwordRecord = hashPassword(password);
await pool.query(
'UPDATE users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?',
'UPDATE a_users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?',
[passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, getAuditUserId(req), userId]
);
await pool.query('DELETE FROM auth_sessions WHERE user_id = ?', [userId]);
await pool.query('DELETE FROM a_sessions WHERE user_id = ?', [userId]);
res.redirect('/users?message=' + encodeURIComponent('Password updated.'));
} catch (error) {
next(error);
@@ -296,12 +296,12 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
return res.redirect('/users?message=' + encodeURIComponent('You cannot delete your own account from the users page.'));
}
const [countRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users');
const [countRows] = await pool.query('SELECT COUNT(*) AS user_count FROM a_users');
if (!countRows.length || Number(countRows[0].user_count) <= 1) {
return res.redirect('/users?message=' + encodeURIComponent('At least one user must remain.'));
}
const [result] = await pool.query('DELETE FROM users WHERE id = ?', [userId]);
const [result] = await pool.query('DELETE FROM a_users WHERE id = ?', [userId]);
if (!result.affectedRows) {
return res.status(404).send('User not found.');
}
+2 -2
View File
@@ -28,7 +28,7 @@ module.exports = function registerAuthRoutes(app, deps) {
return res.status(400).send('Username and password are required.');
}
const [rows] = await pool.query('SELECT id, name, username, password_hash, password_salt, password_iterations FROM users WHERE username = ? LIMIT 1', [username]);
const [rows] = await pool.query('SELECT id, name, username, password_hash, password_salt, password_iterations FROM a_users WHERE username = ? LIMIT 1', [username]);
const user = rows[0] || null;
if (!user || !verifyPassword(password, user)) {
return res.redirect('/login?message=' + encodeURIComponent('Invalid username or password.'));
@@ -47,7 +47,7 @@ module.exports = function registerAuthRoutes(app, deps) {
const cookies = parseCookies(req.headers.cookie || '');
const token = cookies[sessionCookieName];
if (token) {
await pool.query('DELETE FROM auth_sessions WHERE session_hash = ?', [hashSessionToken(token)]);
await pool.query('DELETE FROM a_sessions WHERE session_hash = ?', [hashSessionToken(token)]);
}
clearSessionCookie(res);
res.redirect('/login?message=' + encodeURIComponent('You have been signed out.'));
+6 -11
View File
@@ -103,29 +103,24 @@ function getVideoSourcePath(slide) {
return '';
}
const directMediaPath = String(slide.media_path || '').trim();
if (String(slide.media_type || '').trim().toLowerCase() === 'video' && directMediaPath) {
return directMediaPath;
}
const contentJson = slide.content_json;
if (!contentJson) {
return directMediaPath;
return '';
}
try {
const parsed = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
if (!parsed || typeof parsed !== 'object') {
return directMediaPath;
return '';
}
const videoRegion = Object.keys(parsed).map((key) => parsed[key]).find((region) => {
return region && typeof region === 'object' && String(region.type || '').trim().toLowerCase() === 'video' && String(region.value || '').trim();
});
return videoRegion ? String(videoRegion.value || '').trim() : directMediaPath;
return videoRegion ? String(videoRegion.value || '').trim() : '';
} catch (_error) {
return directMediaPath;
return '';
}
}
@@ -176,7 +171,7 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
isFirst: index === 0,
isLast: index === items.length - 1,
canvasSignature: getCanvasSignature(item.canvas_width, item.canvas_height) || '',
showVideoDurationButton: String(item.media_type || '').trim().toLowerCase() === 'video' || hasVideoRegion(item.content_json),
showVideoDurationButton: hasVideoRegion(item.content_json),
videoSourcePath: getVideoSourcePath(item),
videoDurationSeconds: getVideoDurationSeconds(item),
useVideoDuration: Boolean(item.use_video_duration),
@@ -199,7 +194,7 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
}).map((slide) => Object.assign({}, slide, {
canvasSignature: getCanvasSignature(slide.canvas_width, slide.canvas_height) || '',
isAssigned: assignedSlideIds.has(slide.id),
showVideoDurationButton: String(slide.media_type || '').trim().toLowerCase() === 'video' || hasVideoRegion(slide.content_json),
showVideoDurationButton: hasVideoRegion(slide.content_json),
videoSourcePath: getVideoSourcePath(slide),
videoDurationSeconds: getVideoDurationSeconds(slide),
useVideoDuration: Boolean(slide.use_video_duration),
+2 -2
View File
@@ -83,7 +83,7 @@
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}">
{{#if playlistSlides.length}}
{{#each playlistSlides}}
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}" data-media-type="{{media_type}}" data-media-path="{{media_path}}" data-video-source-path="{{videoSourcePath}}" data-video-duration-seconds="{{videoDurationSeconds}}" data-use-video-duration="{{#if useVideoDuration}}true{{else}}false{{/if}}">
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}" data-media-type="{{media_type}}" data-media-path="{{media_path}}" data-video-source-path="{{videoSourcePath}}" data-video-duration-seconds="{{videoDurationSeconds}}" data-use-video-duration-state="{{#if useVideoDuration}}true{{else}}false{{/if}}">
<td class="playlist-order-cell" data-label="Order">
<div class="playlist-order-cell-inner">
<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder">
@@ -130,7 +130,7 @@
<input type="hidden" name="duration_seconds[]" value="{{durationSeconds}}" form="playlist-edit-form" data-video-duration-mirror />
{{/if}}
{{#if showVideoDurationButton}}
<button type="button" class="btn btn-outline-secondary btn-sm playlist-use-video-duration{{#if useVideoDuration}} active{{/if}}" data-use-video-duration aria-pressed="{{#if useVideoDuration}}true{{else}}false{{/if}}">{{#if useVideoDuration}}Use Video Duration{{else}}Use video duration{{/if}}</button>
<button type="button" class="btn btn-outline-secondary btn-sm playlist-use-video-duration{{#if useVideoDuration}} active{{/if}}" data-use-video-duration-button aria-pressed="{{#if useVideoDuration}}true{{else}}false{{/if}}">{{#if useVideoDuration}}Use Video Duration{{else}}Use video duration{{/if}}</button>
{{/if}}
</div>
</td>
+3 -1
View File
@@ -25,6 +25,7 @@
<th data-sortable="false">Thumbnail</th>
<th data-table-sort-key="title">Title</th>
<th data-table-sort-key="template">Template</th>
<th>Playlists</th>
<th>Actions</th>
</tr>
</thead>
@@ -43,6 +44,7 @@
</td>
<td data-label="Title">{{title}}</td>
<td data-label="Template">{{template_name}}</td>
<td data-label="Playlists">{{playlist_count}}</td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'slides.update' 'slides.delete')}}
<div class="actions">
@@ -62,7 +64,7 @@
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default><td colspan="4" class="empty">No slides yet.</td></tr>
<tr data-table-search-empty-default><td colspan="5" class="empty">No slides yet.</td></tr>
{{/if}}
</tbody>
</table>