Release v2.20.1: migrate authentication to Better Auth

This commit is contained in:
lzstealth committed 2026-10-25 18:19:55 +00:00
1 parent e9a53f577c
commit fba4f99ff7
92 files changed
+5085 -1943

No files matched your search

+137 -1
View File
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
require('../src/common');
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, buildAuditChanges, recordRequestAuditEvent } = require('../src/data/audit-log');
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, buildAuditChanges, buildSlideAuditChanges, recordRequestAuditEvent } = require('../src/data/audit-log');
const { getDefaultAppSettings } = require('../src/data/app-settings');
function createPool(settings) {
@@ -68,6 +68,20 @@ test('audit writer omits request metadata when disabled', async () => {
assert.equal(pool.inserts[0][7], null);
});
test('audit writer skips empty update diffs for content but preserves action events and other details', async () => {
const categories = ['slides', 'templates', 'playlists', 'screens', 'canvas-sizes', 'weather'];
const pool = createPool({ enabled: true, categories, includeMetadata: false });
for (const category of categories) {
await recordRequestAuditEvent(pool, request, { category, eventType: 'content.updated', details: { changes: {} } });
}
assert.equal(pool.inserts.length, 0);
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.created' });
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.deleted' });
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.updated', details: { changes: {}, promotedSlideId: 7 } });
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.updated', details: { changes: { 'regions.heading.x': { from: 1, to: 2 } } } });
assert.equal(pool.inserts.length, 4);
});
test('audit changes include only fields with different from and to values', () => {
assert.deepEqual(buildAuditChanges({
name: 'Old name',
@@ -81,4 +95,126 @@ test('audit changes include only fields with different from and to values', () =
name: { from: 'Old name', to: 'New name' },
roleIds: { from: [1, 2], to: [2, 3] }
});
});
test('slide audit stores only changed leaf fields without unchanged content', () => {
const previous = { title: 'Slide', content: { heading: { value: 'Old', font_size: 32 }, qr: { qr_svg: '<svg>' + 'x'.repeat(100000) + '</svg>' } } };
const next = structuredClone(previous);
next.content.heading.value = 'New';
const changes = buildSlideAuditChanges(previous, next);
assert.deepEqual(changes, { 'content.heading.value': { from: 'Old', to: 'New' } });
assert.ok(JSON.stringify(changes).length < 100);
});
test('slide audit ignores unchanged saves and object key order', () => {
assert.deepEqual(buildSlideAuditChanges({ content: { body: { value: 'Same', size: 32 } } }, { content: { body: { size: 32, value: 'Same' } } }), {});
});
test('slide audit preserves added and removed fields across JSON serialization', () => {
const changes = buildSlideAuditChanges({ content: { removed: { value: 'Old' }, body: { muted: false } } }, { content: { added: { value: 'New' }, body: { muted: true } } });
assert.deepEqual(JSON.parse(JSON.stringify(changes)), {
'content.removed.value': { from: 'Old', to: null },
'content.body.muted': { from: false, to: true },
'content.added.value': { from: null, to: 'New' }
});
});
test('slide audit fingerprints generated QR payloads including added and removed assets', () => {
const previous = { content: { qr: { qr_svg: '<svg>' + 'x'.repeat(100000) + '</svg>', qr_preview: 'data:image/png;base64,' + 'y'.repeat(100000), value: 'https://example.com/old' } } };
const next = { content: { qr: { qr_svg: '<svg>new</svg>', value: 'https://example.com/new' } } };
const changes = buildSlideAuditChanges(previous, next);
assert.match(changes['content.qr.qr_svg'].from, /^\[Generated asset: 100011 bytes; SHA-256 [a-f0-9]{64}\]$/);
assert.notEqual(changes['content.qr.qr_svg'].from, changes['content.qr.qr_svg'].to);
assert.equal(changes['content.qr.qr_preview'].to, null);
assert.deepEqual(changes['content.qr.value'], { from: 'https://example.com/old', to: 'https://example.com/new' });
assert.ok(JSON.stringify(changes).length < 1000);
const added = buildSlideAuditChanges({}, next);
assert.equal(added['content.qr.qr_svg'].from, null);
assert.match(added['content.qr.qr_svg'].to, /SHA-256/);
assert.equal(previous.content.qr.qr_svg.length, 100011);
});
test('slide save skips no-op audit events while retaining save side effects', async () => {
const registerContentRoutes = require('../src/web/routes/admin/content');
for (const changed of [false, true]) {
const handlers = {};
const calls = [];
const events = [];
const content = { heading: { type: 'text', value: 'Old' } };
const nextContent = { heading: { type: 'text', value: changed ? 'New' : 'Old' } };
const noop = async () => {};
const connection = {
beginTransaction: noop,
async commit() { calls.push('commit'); },
rollback: noop,
release() {},
async query(sql) {
if (sql.startsWith('SELECT id FROM c_slides')) return [[{ id: 7 }]];
if (sql.startsWith('UPDATE c_slides')) calls.push('update');
return [[]];
}
};
registerContentRoutes({
get() {},
post(route, ...actions) { handlers[route] = actions; }
}, {
pool: { async query() { return [[]]; }, async getConnection() { return connection; } },
common: {
fetchSlideById: async () => ({ id: 7, title: 'Slide', template_id: 2, content }),
buildSlidePayload: async () => ({ title: 'Slide', templateId: 2, contentJson: JSON.stringify(nextContent) }),
fetchDuplicateName: async () => null,
parseJsonSafe: JSON.parse
},
pages: {}, upload: { any: () => noop, single: () => noop },
fetchScreensBySlideId: async () => [], fetchScreensByTemplateId: async () => [],
collectUploadReferencesFromSlide: () => [], collectUploadReferencesFromTemplate: () => [],
collectUploadReferencesFromPayload: () => [], removeUnusedUploadFiles: noop,
syncPlaylistUploadsOnChange: async () => { calls.push('sync'); },
getAuditUserId: () => 9,
recordRequestAuditEvent: async (_pool, _req, event) => { events.push(event); },
redirectAfterSave: () => { calls.push('redirect'); },
notifyPlayerScreens: noop,
broadcastDashboardState: async () => { calls.push('broadcast'); },
backgroundTaskQueue: { enqueueTask: async () => { calls.push('thumbnail'); } },
getSlideDeleteBlockMessage: noop, getTemplateDeleteBlockMessage: noop, getCanvasSizeDeleteBlockMessage: noop,
hasAnyPermission: () => false, requirePermission: () => noop
});
await handlers['/slides/:id'].at(-1)({ params: { id: '7' }, files: [] }, {}, error => { throw error; });
assert.deepEqual(calls, ['update', 'commit', 'sync', 'broadcast', 'thumbnail', 'redirect']);
assert.equal(events.length, changed ? 1 : 0);
if (changed) {
assert.equal(events[0].eventType, 'slide.updated');
assert.deepEqual(events[0].details.changes, { 'content.heading.value': { from: 'Old', to: 'New' } });
}
}
});
test('shared audit changes compact nested content across categories', () => {
assert.deepEqual(buildAuditChanges({ settings: { text: 'Old', keep: 'Same' } }, { settings: { text: 'New', keep: 'Same' } }), {
'settings.text': { from: 'Old', to: 'New' }
});
});
test('template auditing matches regions by stable key and summarizes default QR assets', () => {
const heading = { region_key: 'heading', x: 10, default_content: { value: 'Same' } };
const qr = { region_key: 'qr', default_content: { qr_svg: '<svg>old</svg>' } };
assert.deepEqual(buildAuditChanges({ regions: [heading, qr] }, { regions: [qr, heading] }), {});
const changes = buildAuditChanges({ regions: [heading, qr] }, { regions: [qr, { ...heading, x: 20 }] });
assert.deepEqual(changes, { 'regions.heading.x': { from: 10, to: 20 } });
const assets = buildAuditChanges({ regions: [qr] }, { regions: [{ ...qr, default_content: { qr_svg: '<svg>new</svg>' } }] });
assert.match(assets['regions.qr.default_content.qr_svg'].from, /SHA-256/);
assert.match(assets['regions.qr.default_content.qr_svg'].to, /SHA-256/);
});
test('ordered content collections retain reordering and added or removed item differences', () => {
const first = { slideId: 1, duration: 10 };
const second = { slideId: 2, duration: 10 };
assert.deepEqual(buildAuditChanges({ items: [first, second] }, { items: [second, first] }), {
'items[0].slideId': { from: 1, to: 2 },
'items[1].slideId': { from: 2, to: 1 }
});
assert.deepEqual(buildAuditChanges({ items: [first, second] }, { items: [first] }), {
'items[1].slideId': { from: 2, to: null },
'items[1].duration': { from: 10, to: null }
});
});