Release v2.17.5
This commit is contained in:
1 parent
46636939de
commit
86b9609ea7
11 files changed
+285
-56
No files matched your search
+15
-8
@@ -64,7 +64,8 @@ async function ensureSchema(pool, options) {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL
|
||||
modified_by INT NULL,
|
||||
INDEX idx_templates_canvas_size (canvas_size_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -86,7 +87,8 @@ async function ensureSchema(pool, options) {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL
|
||||
modified_by INT NULL,
|
||||
INDEX idx_template_regions_template_z (template_id, z_index)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -100,7 +102,8 @@ async function ensureSchema(pool, options) {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL
|
||||
modified_by INT NULL,
|
||||
INDEX idx_slides_template (template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -143,7 +146,8 @@ async function ensureSchema(pool, options) {
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL,
|
||||
CONSTRAINT fk_playlist_slides_playlist FOREIGN KEY (playlist_id) REFERENCES c_playlists(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_playlist_slides_slide FOREIGN KEY (slide_id) REFERENCES c_slides(id) ON DELETE CASCADE
|
||||
CONSTRAINT fk_playlist_slides_slide FOREIGN KEY (slide_id) REFERENCES c_slides(id) ON DELETE CASCADE,
|
||||
INDEX idx_playlist_slides_playlist_position (playlist_id, position)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -240,7 +244,8 @@ async function ensureSchema(pool, options) {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL
|
||||
modified_by INT NULL,
|
||||
INDEX idx_rss_feeds_modified_at (modified_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -281,7 +286,8 @@ async function ensureSchema(pool, options) {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL
|
||||
modified_by INT NULL,
|
||||
INDEX idx_api_source_groups_modified_at (modified_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
await pool.query(`
|
||||
@@ -520,7 +526,8 @@ async function ensureSchema(pool, options) {
|
||||
created_by INT NULL,
|
||||
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL,
|
||||
CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES a_users(id) ON DELETE CASCADE
|
||||
CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES a_users(id) ON DELETE CASCADE,
|
||||
INDEX idx_sessions_expires_at (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
@@ -551,7 +558,7 @@ async function ensureSchema(pool, options) {
|
||||
started_at TIMESTAMP NULL,
|
||||
finished_at TIMESTAMP NULL,
|
||||
error_message MEDIUMTEXT NULL,
|
||||
INDEX idx_background_tasks_status (status),
|
||||
INDEX idx_background_tasks_status_created_at (status, created_at),
|
||||
INDEX idx_background_tasks_key (task_key),
|
||||
INDEX idx_background_tasks_type (task_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
|
||||
@@ -746,6 +746,21 @@ const VERSIONED_MIGRATIONS = [
|
||||
);
|
||||
await pool.query('DELETE FROM a_permissions WHERE permission_key = ?', ['dashboard.allow']);
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '2.17.5',
|
||||
label: 'v2.17.5 query indexes',
|
||||
run: async function (pool) {
|
||||
await ensureIndex(pool, 'c_playlist_slides', 'idx_playlist_slides_playlist_position', 'playlist_id, position');
|
||||
await ensureIndex(pool, 'c_template_regions', 'idx_template_regions_template_z', 'template_id, z_index');
|
||||
await ensureIndex(pool, 'c_slides', 'idx_slides_template', 'template_id');
|
||||
await ensureIndex(pool, 'c_templates', 'idx_templates_canvas_size', 'canvas_size_id');
|
||||
await ensureIndex(pool, 'a_sessions', 'idx_sessions_expires_at', 'expires_at');
|
||||
await ensureIndex(pool, 'o_background_tasks', 'idx_background_tasks_status_created_at', 'status, created_at');
|
||||
await ensureIndex(pool, 'i_rss_feeds', 'idx_rss_feeds_modified_at', 'modified_at');
|
||||
await ensureIndex(pool, 'i_api_source_groups', 'idx_api_source_groups_modified_at', 'modified_at');
|
||||
await dropIndexIfExists(pool, 'o_background_tasks', 'idx_background_tasks_status');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -896,6 +911,38 @@ async function ensureForeignKey(pool, tableName, constraintName, columnName, ref
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureIndex(pool, tableName, indexName, columns) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS index_count
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND INDEX_NAME = ?`,
|
||||
[tableName, indexName]
|
||||
);
|
||||
|
||||
if (Number(rows && rows[0] && rows[0].index_count) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pool.query('ALTER TABLE ' + tableName + ' ADD INDEX ' + indexName + ' (' + columns + ')');
|
||||
}
|
||||
|
||||
async function dropIndexIfExists(pool, tableName, indexName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS index_count
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND INDEX_NAME = ?`,
|
||||
[tableName, indexName]
|
||||
);
|
||||
|
||||
if (Number(rows && rows[0] && rows[0].index_count) > 0) {
|
||||
await pool.query('ALTER TABLE ' + tableName + ' DROP INDEX ' + indexName);
|
||||
}
|
||||
}
|
||||
|
||||
async function dropForeignKeyIfExists(pool, tableName, columnName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT CONSTRAINT_NAME AS constraint_name
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { hasPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
|
||||
const { AUDIT_CATEGORY_KEYS, formatUserAgentLabel } = require('#src/data/audit-log');
|
||||
const { buildSearchFilter } = require('#src/data/utils');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
|
||||
const { getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber } = require('#src/web/lib/list-query');
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const AUDIT_FROM_SQL = `FROM o_audit_events e
|
||||
LEFT JOIN a_users u ON u.id = e.actor_user_id`;
|
||||
const AUDIT_TARGET_LABEL_SQL = "CASE WHEN COALESCE(e.target_label, '') <> '' THEN e.target_label WHEN e.target_type IS NOT NULL AND e.target_id IS NOT NULL THEN CONCAT(e.target_type, ' #', e.target_id) ELSE '' END";
|
||||
const AUDIT_SEARCH_COLUMNS = [
|
||||
'e.category',
|
||||
'e.event_type',
|
||||
"COALESCE(NULLIF(u.name, ''), u.username, 'System')",
|
||||
AUDIT_TARGET_LABEL_SQL,
|
||||
'e.ip_address',
|
||||
'e.user_agent'
|
||||
];
|
||||
const AUDIT_SORT_COLUMNS = {
|
||||
time: 'e.occurred_at',
|
||||
event: "REPLACE(REPLACE(REPLACE(e.event_type, '.', ' '), '_', ' '), '-', ' ')",
|
||||
actor: "COALESCE(NULLIF(u.name, ''), u.username, 'System')",
|
||||
target: AUDIT_TARGET_LABEL_SQL,
|
||||
category: 'e.category'
|
||||
};
|
||||
|
||||
const SORTED_AUDIT_CATEGORY_KEYS = AUDIT_CATEGORY_KEYS.slice().sort(function (left, right) {
|
||||
return String(left).localeCompare(String(right));
|
||||
@@ -168,35 +187,78 @@ function mapAuditRow(row, formatDashboardDate) {
|
||||
});
|
||||
}
|
||||
|
||||
function filterAuditRows(rows, req, formatDashboardDate) {
|
||||
function buildAuditFilters(req) {
|
||||
const category = String(req.query.category || '').trim().toLowerCase();
|
||||
const eventType = String(req.query.event_type || '').trim().toLowerCase();
|
||||
const search = getSearchQuery(req);
|
||||
const requestedSort = getSortQuery(req);
|
||||
const sort = requestedSort || 'time';
|
||||
const sort = Object.prototype.hasOwnProperty.call(AUDIT_SORT_COLUMNS, requestedSort) ? requestedSort : 'time';
|
||||
const direction = requestedSort ? getSortDirectionQuery(req) : 'desc';
|
||||
const mappedRows = (rows || []).map(function (row) { return mapAuditRow(row, formatDashboardDate); });
|
||||
const filteredRows = sortRows(mappedRows.filter(function (event) {
|
||||
return (!category || event.category === category) && (!eventType || event.event_type === eventType);
|
||||
}).filter(createSearchMatcher(search, ['category', 'event_type', 'actorLabel', 'targetLabelDisplay', 'ip_address', 'user_agent'])), function (event) {
|
||||
if (sort === 'event') return event.eventLabel;
|
||||
if (sort === 'actor') return event.actorLabel;
|
||||
if (sort === 'target') return event.targetLabelDisplay;
|
||||
if (sort === 'category') return event.category;
|
||||
return event.occurred_at;
|
||||
}, direction);
|
||||
return { rows: filteredRows, category: category, eventType: eventType, search: search, sort: sort, direction: direction };
|
||||
const whereClauses = [];
|
||||
const params = [];
|
||||
|
||||
if (category) {
|
||||
whereClauses.push('e.category = ?');
|
||||
params.push(category);
|
||||
}
|
||||
if (eventType) {
|
||||
whereClauses.push('e.event_type = ?');
|
||||
params.push(eventType);
|
||||
}
|
||||
|
||||
const searchFilter = buildSearchFilter(AUDIT_SEARCH_COLUMNS, search);
|
||||
if (searchFilter.clause) {
|
||||
whereClauses.push(searchFilter.clause.replace(/^\s*WHERE\s+/i, ''));
|
||||
params.push(...searchFilter.params);
|
||||
}
|
||||
|
||||
return {
|
||||
category: category,
|
||||
eventType: eventType,
|
||||
search: search,
|
||||
sort: sort,
|
||||
direction: direction,
|
||||
whereSql: whereClauses.length ? ' WHERE ' + whereClauses.join(' AND ') : '',
|
||||
params: params
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAuditRows(pool) {
|
||||
function buildAuditOrderBy(filters) {
|
||||
const sortExpression = AUDIT_SORT_COLUMNS[filters.sort] || AUDIT_SORT_COLUMNS.time;
|
||||
const tieBreakers = filters.sort === 'time' ? 'e.id DESC' : 'e.occurred_at DESC, e.id DESC';
|
||||
return ' ORDER BY ' + sortExpression + ' ' + filters.direction.toUpperCase() + ', ' + tieBreakers;
|
||||
}
|
||||
|
||||
async function fetchAuditCount(pool, filters) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id,
|
||||
'SELECT COUNT(*) AS count ' + AUDIT_FROM_SQL + filters.whereSql,
|
||||
filters.params
|
||||
);
|
||||
return Number(rows && rows[0] && rows[0].count) || 0;
|
||||
}
|
||||
|
||||
async function fetchAuditEventTypes(pool, filters) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT DISTINCT e.event_type AS event_type ' + AUDIT_FROM_SQL + filters.whereSql + (filters.whereSql ? ' AND ' : ' WHERE ') + "e.event_type IS NOT NULL AND e.event_type <> '' ORDER BY e.event_type ASC",
|
||||
filters.params
|
||||
);
|
||||
return (rows || []).map(function (row) {
|
||||
return String(row.event_type || '').trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
async function fetchAuditRows(pool, filters, page) {
|
||||
let sql = `SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id,
|
||||
e.target_label, e.ip_address, e.user_agent, e.details_json,
|
||||
u.name AS actor_name, u.username AS actor_username
|
||||
FROM o_audit_events e
|
||||
LEFT JOIN a_users u ON u.id = e.actor_user_id
|
||||
ORDER BY e.occurred_at DESC, e.id DESC`, []
|
||||
);
|
||||
${AUDIT_FROM_SQL}${filters.whereSql}${buildAuditOrderBy(filters)}`;
|
||||
const params = filters.params.slice();
|
||||
if (page) {
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
params.push(page.limit, page.offset);
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(sql, params);
|
||||
return rows || [];
|
||||
}
|
||||
|
||||
@@ -214,27 +276,28 @@ module.exports = function registerAuditLogRoutes(app, deps) {
|
||||
const search = getSearchQuery(req);
|
||||
const sort = getSortQuery(req);
|
||||
const direction = getSortDirectionQuery(req);
|
||||
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
|
||||
const filteredEvents = filtered.rows;
|
||||
const eventTypes = Array.from(new Set(filtered.rows.map(function (event) {
|
||||
return String(event.event_type || '').trim();
|
||||
}).filter(Boolean))).sort();
|
||||
const exportQuery = new URLSearchParams({
|
||||
category: filtered.category,
|
||||
event_type: filtered.eventType,
|
||||
search: filtered.search,
|
||||
sort: filtered.sort,
|
||||
direction: filtered.direction
|
||||
}).toString();
|
||||
const pagination = buildPagination(filteredEvents.length, parsePageNumber(req.query.page), 'page', {
|
||||
const filters = buildAuditFilters(req);
|
||||
const totalEvents = await fetchAuditCount(pool, filters);
|
||||
const pagination = buildPagination(totalEvents, parsePageNumber(req.query.page), 'page', {
|
||||
search: search,
|
||||
sort: sort,
|
||||
direction: direction,
|
||||
category: category,
|
||||
event_type: eventType
|
||||
}, PAGE_SIZE, 'events', 'Audit event pages');
|
||||
const startIndex = (pagination.currentPage - 1) * PAGE_SIZE;
|
||||
const events = filteredEvents.slice(startIndex, startIndex + PAGE_SIZE);
|
||||
const rawEvents = await fetchAuditRows(pool, filters, {
|
||||
limit: PAGE_SIZE,
|
||||
offset: (pagination.currentPage - 1) * PAGE_SIZE
|
||||
});
|
||||
const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); });
|
||||
const eventTypes = await fetchAuditEventTypes(pool, filters);
|
||||
const exportQuery = new URLSearchParams({
|
||||
category: filters.category,
|
||||
event_type: filters.eventType,
|
||||
search: filters.search,
|
||||
sort: filters.sort,
|
||||
direction: filters.direction
|
||||
}).toString();
|
||||
res.send(renderView('settings/audit-log/index', {
|
||||
title: 'Audit log',
|
||||
active: 'audit-log',
|
||||
@@ -257,11 +320,13 @@ module.exports = function registerAuditLogRoutes(app, deps) {
|
||||
|
||||
app.get('/settings/audit-log/export', requireAuditExportAccess(deps.setAuthMessageCookie), async function (req, res, next) {
|
||||
try {
|
||||
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
|
||||
const filters = buildAuditFilters(req);
|
||||
const rawEvents = await fetchAuditRows(pool, filters);
|
||||
const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); });
|
||||
const lines = [
|
||||
['Occurred At (UTC)', 'Category', 'Event Type', 'Actor', 'Target Type', 'Target ID', 'Target', 'IP Address', 'User Agent', 'Details'].map(csvCell).join(',')
|
||||
];
|
||||
filtered.rows.forEach(function (event) {
|
||||
events.forEach(function (event) {
|
||||
lines.push([
|
||||
event.occurredAtValue,
|
||||
event.category,
|
||||
|
||||
Reference in new issue
Block a user