feat: add community publishing and news syndication
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s

This commit is contained in:
jb
2026-08-01 21:42:09 -04:00
parent b47e2588e9
commit dd84dff055
288 changed files with 83254 additions and 38351 deletions
+424 -147
View File
@@ -1,4 +1,7 @@
import { getPermissions, requirePermission } from '$lib/server/auth/authorization';
import {
getPermissions,
requirePermission,
} from '$lib/server/auth/authorization';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySort } from '$lib/server/pagination';
@@ -8,83 +11,137 @@ import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
const reversibleActions = new Set([
'attribute.updated', 'attribute.archived',
'brand_set.archived', 'gear.archived', 'weapon.archived',
'role.updated', 'access.updated'
'attribute.updated',
'attribute.archived',
'brand_set.archived',
'gear.archived',
'weapon.archived',
'role.updated',
'access.updated',
]);
const parseSnapshot = (value: unknown): Record<string, any> | null => {
if (typeof value !== 'string' || !value) return null;
try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' ? parsed : null; } catch { return null; }
if (typeof value !== 'string' || !value) return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' ? parsed : null;
} catch {
return null;
}
};
const normalize = (value: unknown): unknown => {
if (Array.isArray(value)) {
const normalized = value.map(normalize);
return normalized.every((item) => ['string', 'number', 'boolean'].includes(typeof item))
? normalized.sort((left, right) => String(left).localeCompare(String(right)))
: normalized;
}
if (!value || typeof value !== 'object') return value;
return Object.fromEntries(Object.entries(value as Record<string, unknown>)
.filter(([key]) => !['created_at', 'updated_at'].includes(key))
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, normalize(item)]));
if (Array.isArray(value)) {
const normalized = value.map(normalize);
return normalized.every((item) =>
['string', 'number', 'boolean'].includes(typeof item),
)
? normalized.sort((left, right) =>
String(left).localeCompare(String(right)),
)
: normalized;
}
if (!value || typeof value !== 'object') return value;
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.filter(([key]) => !['created_at', 'updated_at'].includes(key))
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, normalize(item)]),
);
};
const comparable = (value: unknown): string => JSON.stringify(normalize(value));
const sameRecord = (current: Record<string, unknown> | null, expected: Record<string, unknown> | null) => {
if (!current || !expected) return false;
return Object.entries(expected).every(([key, value]) =>
!['created_at', 'updated_at'].includes(key) && comparable(current[key]) === comparable(value)
);
const sameRecord = (
current: Record<string, unknown> | null,
expected: Record<string, unknown> | null,
) => {
if (!current || !expected) return false;
return Object.entries(expected).every(
([key, value]) =>
!['created_at', 'updated_at'].includes(key) &&
comparable(current[key]) === comparable(value),
);
};
export const load: PageServerLoad = async ({ cookies, url }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'audit.view');
const canRevert = (await getPermissions(operator)).includes('*') || (await getPermissions(operator)).includes('audit.revert');
const database = await getDatabase();
const query = url.searchParams.get('q')?.trim().slice(0, 120) ?? '';
const category = url.searchParams.get('category')?.trim() ?? '';
const action = url.searchParams.get('action')?.trim() ?? '';
const sort = getRegistrySort(url, {
event: {
asc: 'audit_log.action COLLATE NOCASE ASC,audit_log.entity_type COLLATE NOCASE ASC',
desc: 'audit_log.action COLLATE NOCASE DESC,audit_log.entity_type COLLATE NOCASE DESC'
},
operator: {
asc: 'COALESCE(user_profiles.display_name,users.username) COLLATE NOCASE ASC',
desc: 'COALESCE(user_profiles.display_name,users.username) COLLATE NOCASE DESC'
},
target: { asc: 'audit_log.entity_id COLLATE NOCASE ASC', desc: 'audit_log.entity_id COLLATE NOCASE DESC' },
time: {
asc: 'audit_log.created_at ASC',
desc: 'audit_log.created_at DESC',
defaultDirection: 'desc'
}
}, 'time', 'desc');
const conditions: string[] = [];
const args: unknown[] = [];
if (query) {
conditions.push(`(audit_log.entity_id LIKE ? ESCAPE '\\' COLLATE NOCASE
const operator = await requirePermission(
await getSessionUser(cookies),
'audit.view',
);
const canRevert =
(await getPermissions(operator)).includes('*') ||
(await getPermissions(operator)).includes('audit.revert');
const database = await getDatabase();
const query = url.searchParams.get('q')?.trim().slice(0, 120) ?? '';
const category = url.searchParams.get('category')?.trim() ?? '';
const action = url.searchParams.get('action')?.trim() ?? '';
const sort = getRegistrySort(
url,
{
event: {
asc: 'audit_log.action COLLATE NOCASE ASC,audit_log.entity_type COLLATE NOCASE ASC',
desc: 'audit_log.action COLLATE NOCASE DESC,audit_log.entity_type COLLATE NOCASE DESC',
},
operator: {
asc: 'COALESCE(user_profiles.display_name,users.username) COLLATE NOCASE ASC',
desc: 'COALESCE(user_profiles.display_name,users.username) COLLATE NOCASE DESC',
},
target: {
asc: 'audit_log.entity_id COLLATE NOCASE ASC',
desc: 'audit_log.entity_id COLLATE NOCASE DESC',
},
time: {
asc: 'audit_log.created_at ASC',
desc: 'audit_log.created_at DESC',
defaultDirection: 'desc',
},
},
'time',
'desc',
);
const conditions: string[] = [];
const args: unknown[] = [];
if (query) {
conditions.push(`(audit_log.entity_id LIKE ? ESCAPE '\\' COLLATE NOCASE
OR audit_log.before_json LIKE ? ESCAPE '\\' COLLATE NOCASE
OR audit_log.after_json LIKE ? ESCAPE '\\' COLLATE NOCASE
OR users.username LIKE ? ESCAPE '\\' COLLATE NOCASE
OR user_profiles.display_name LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
const pattern = `%${query.replaceAll('%', '\\%').replaceAll('_', '\\_')}%`;
args.push(pattern, pattern, pattern, pattern, pattern);
}
if (category === 'user') conditions.push("audit_log.entity_type IN ('user','user_access','user_moderation','identity')");
else if (category === 'access_policy') conditions.push("audit_log.entity_type IN ('role','role_management')");
else if (category) { conditions.push('audit_log.entity_type=?'); args.push(category); }
if (action) { conditions.push('audit_log.action=?'); args.push(action); }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const [total, categories, actions, summary] = await Promise.all([
(await database.prepare(`SELECT COUNT(*) AS total FROM audit_log
JOIN users ON users.id=audit_log.actor_id LEFT JOIN user_profiles ON user_profiles.user_id=users.id ${where}`)).get(...args),
(await database.prepare('SELECT entity_type,COUNT(*) AS count FROM audit_log GROUP BY entity_type ORDER BY entity_type')).all(),
(await database.prepare('SELECT action,COUNT(*) AS count FROM audit_log GROUP BY action ORDER BY action')).all(),
(await database.prepare(`SELECT COUNT(*) AS total,
const pattern = `%${query.replaceAll('%', '\\%').replaceAll('_', '\\_')}%`;
args.push(pattern, pattern, pattern, pattern, pattern);
}
if (category === 'user')
conditions.push(
"audit_log.entity_type IN ('user','user_access','user_moderation','identity')",
);
else if (category === 'access_policy')
conditions.push("audit_log.entity_type IN ('role','role_management')");
else if (category) {
conditions.push('audit_log.entity_type=?');
args.push(category);
}
if (action) {
conditions.push('audit_log.action=?');
args.push(action);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const [total, categories, actions, summary] = await Promise.all([
(
await database.prepare(`SELECT COUNT(*) AS total FROM audit_log
JOIN users ON users.id=audit_log.actor_id LEFT JOIN user_profiles ON user_profiles.user_id=users.id ${where}`)
).get(...args),
(
await database.prepare(
'SELECT entity_type,COUNT(*) AS count FROM audit_log GROUP BY entity_type ORDER BY entity_type',
)
).all(),
(
await database.prepare(
'SELECT action,COUNT(*) AS count FROM audit_log GROUP BY action ORDER BY action',
)
).all(),
(
await database.prepare(`SELECT COUNT(*) AS total,
SUM(CASE WHEN entity_type='game_data' THEN 1 ELSE 0 END) AS game_data,
SUM(CASE WHEN entity_type='data_moderation' THEN 1 ELSE 0 END) AS data_moderation,
SUM(CASE WHEN entity_type='community_moderation' THEN 1 ELSE 0 END) AS community_moderation,
@@ -93,95 +150,315 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
SUM(CASE WHEN entity_type='system_configuration' THEN 1 ELSE 0 END) AS system_configuration,
SUM(CASE WHEN entity_type='calculation_configuration' THEN 1 ELSE 0 END) AS calculation_configuration,
SUM(CASE WHEN entity_type='ai_assistance' THEN 1 ELSE 0 END) AS ai_assistance
FROM audit_log`)).get()
]);
const pagination = getPagination(url, Number(total?.total ?? 0));
const entries = await (await database.prepare(`SELECT audit_log.*,
FROM audit_log`)
).get(),
]);
const pagination = getPagination(url, Number(total?.total ?? 0));
const entries = await (
await database.prepare(`SELECT audit_log.*,
COALESCE(user_profiles.display_name,users.username) AS actor_name,users.login_username AS actor_login
FROM audit_log JOIN users ON users.id=audit_log.actor_id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
${where} ORDER BY ${sort.orderBy},audit_log.id LIMIT ? OFFSET ?`)).all(...args, pagination.perPage, pagination.offset);
const revertedIds = entries.length ? await (await database.prepare(`SELECT entity_id FROM audit_log
WHERE entity_type='audit_control' AND action='audit.reverted' AND entity_id IN (${entries.map(() => '?').join(',')})`)).all(...entries.map((entry) => entry.id)) : [];
const reverted = new Set(revertedIds.map((entry) => String(entry.entity_id)));
return {
entries: entries.map((entry) => ({ ...entry, reversible: canRevert && reversibleActions.has(String(entry.action)) && Boolean(entry.before_json) && !reverted.has(String(entry.id)), reverted: reverted.has(String(entry.id)) })),
categories, actions, summary, pagination, filters: { query, category, action }, canRevert, sort
};
${where} ORDER BY ${sort.orderBy},audit_log.id LIMIT ? OFFSET ?`)
).all(...args, pagination.perPage, pagination.offset);
const revertedIds = entries.length
? await (
await database.prepare(`SELECT entity_id FROM audit_log
WHERE entity_type='audit_control' AND action='audit.reverted' AND entity_id IN (${entries.map(() => '?').join(',')})`)
).all(...entries.map((entry) => entry.id))
: [];
const reverted = new Set(revertedIds.map((entry) => String(entry.entity_id)));
return {
entries: entries.map((entry) => ({
...entry,
reversible:
canRevert &&
reversibleActions.has(String(entry.action)) &&
Boolean(entry.before_json) &&
!reverted.has(String(entry.id)),
reverted: reverted.has(String(entry.id)),
})),
categories,
actions,
summary,
pagination,
filters: { query, category, action },
canRevert,
sort,
};
};
export const actions: Actions = {
revert: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'audit.revert');
const operatorPermissions = await getPermissions(operator);
const isFullAdmin = operatorPermissions.includes('*');
const eventId = String((await request.formData()).get('eventId') ?? '');
const database = await getDatabase();
const event = await (await database.prepare('SELECT * FROM audit_log WHERE id=?')).get(eventId);
if (!event) return fail(404, { revertError: 'Audit event not found.' });
if (!reversibleActions.has(String(event.action)) || !event.before_json) return fail(400, { revertError: 'This event does not contain a supported reversible change.' });
if (await (await database.prepare("SELECT id FROM audit_log WHERE entity_type='audit_control' AND action='audit.reverted' AND entity_id=?")).get(eventId)) {
return fail(409, { revertError: 'This audit event has already been reverted.' });
}
const before = parseSnapshot(event.before_json), after = parseSnapshot(event.after_json);
if (!before || !after) return fail(400, { revertError: 'The stored snapshots cannot be safely interpreted.' });
const action = String(event.action), targetId = String(event.entity_id);
let current: Record<string, unknown> | null = null;
let restored: Record<string, unknown> | null = null;
try {
if (action.startsWith('attribute.')) {
current = await (await database.prepare('SELECT * FROM stat_attributes WHERE id=?')).get(targetId) ?? null;
if (!sameRecord(current, after)) return fail(409, { revertError: 'The attribute changed after this event. Reversion was blocked to preserve newer work.' });
await (await database.prepare(`UPDATE stat_attributes SET name=?,description=?,type=?,category=?,calculation_type=?,unit=?,gear_min=?,gear_max=?,
weapon_min=?,weapon_max=?,skill_chip_min=?,skill_chip_max=?,cap_value=?,active=?,updated_at=? WHERE id=?`))
.run(before.name,before.description ?? null,before.type,before.category,before.calculation_type,before.unit,before.gear_min,before.gear_max,before.weapon_min,before.weapon_max,before.skill_chip_min,before.skill_chip_max,before.cap_value,before.active,new Date().toISOString(),targetId);
restored = await (await database.prepare('SELECT * FROM stat_attributes WHERE id=?')).get(targetId) ?? null;
} else if (action.endsWith('.archived')) {
const table = action.startsWith('brand_set.') ? 'brand_sets' : action.startsWith('gear.') ? 'gear' : action.startsWith('weapon.') ? 'weapons' : '';
if (!table) return fail(400, { revertError: 'Unsupported archive category.' });
current = await (await database.prepare(`SELECT * FROM ${table} WHERE id=?`)).get(targetId) ?? null;
if (!sameRecord(current, after)) return fail(409, { revertError: 'The record changed after it was archived. Reversion was blocked.' });
await (await database.prepare(`UPDATE ${table} SET active=?,updated_at=? WHERE id=?`)).run(before.active, new Date().toISOString(), targetId);
restored = await (await database.prepare(`SELECT * FROM ${table} WHERE id=?`)).get(targetId) ?? null;
} else if (action === 'role.updated') {
const currentRole = await (await database.prepare('SELECT name,description FROM roles WHERE id=?')).get(targetId);
const currentPermissions = await (await database.prepare(`SELECT permissions.name FROM role_permissions JOIN permissions ON permissions.id=role_permissions.permission_id WHERE role_id=? ORDER BY permissions.name`)).all(targetId);
current = { ...currentRole, permissions: currentPermissions.map((row) => row.name) };
if (comparable(current) !== comparable(after)) return fail(409, { revertError: 'The role changed after this event. Reversion was blocked.' });
const permissionNames = Array.isArray(before.permissions) ? before.permissions.map(String) : [];
if (permissionNames.includes('system.admin') && !isFullAdmin) return fail(403, { revertError: 'Only a full administrator can restore unrestricted system access.' });
const permissionRows = permissionNames.length ? await (await database.prepare(`SELECT id,name FROM permissions WHERE name IN (${permissionNames.map(() => '?').join(',')})`)).all(...permissionNames) : [];
if (permissionRows.length !== permissionNames.length) return fail(409, { revertError: 'A previous permission no longer exists.' });
await (await database.prepare('UPDATE roles SET name=?,description=? WHERE id=?')).run(before.name, before.description ?? null, targetId);
await (await database.prepare('DELETE FROM role_permissions WHERE role_id=?')).run(targetId);
for (const permission of permissionRows) await (await database.prepare('INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)')).run(targetId, permission.id);
restored = { name: before.name, description: before.description ?? null, permissions: permissionNames };
} else if (action === 'access.updated') {
const currentRoles = await (await database.prepare('SELECT role_id FROM user_roles WHERE user_id=? ORDER BY role_id')).all(targetId);
const currentPermissions = await (await database.prepare('SELECT permission_id FROM user_permissions WHERE user_id=? ORDER BY permission_id')).all(targetId);
current = { roleIds: currentRoles.map((row) => row.role_id), permissionIds: currentPermissions.map((row) => row.permission_id) };
if (comparable(current) !== comparable(after)) return fail(409, { revertError: 'The user’s access changed after this event. Reversion was blocked.' });
const roleIds = Array.isArray(before.roleIds) ? before.roleIds.map(String) : [];
const permissionIds = Array.isArray(before.permissionIds) ? before.permissionIds.map(String) : [];
const restoresAdminRole = roleIds.includes('role-admin');
const restoresSystemPermission = permissionIds.includes('permission-system-admin');
if ((restoresAdminRole || restoresSystemPermission) && !isFullAdmin) return fail(403, { revertError: 'Only a full administrator can restore administrator access.' });
const roleCount = roleIds.length ? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM roles WHERE id IN (${roleIds.map(() => '?').join(',')})`)).get(...roleIds))?.count) : 0;
const permissionCount = permissionIds.length ? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM permissions WHERE id IN (${permissionIds.map(() => '?').join(',')})`)).get(...permissionIds))?.count) : 0;
if (roleCount !== roleIds.length || permissionCount !== permissionIds.length) return fail(409, { revertError: 'A previous role or permission no longer exists.' });
await (await database.prepare('DELETE FROM user_roles WHERE user_id=?')).run(targetId);
await (await database.prepare('DELETE FROM user_permissions WHERE user_id=?')).run(targetId);
for (const roleId of roleIds) await (await database.prepare('INSERT INTO user_roles(user_id,role_id) VALUES(?,?)')).run(targetId, roleId);
for (const permissionId of permissionIds) await (await database.prepare('INSERT INTO user_permissions(user_id,permission_id) VALUES(?,?)')).run(targetId, permissionId);
restored = { roleIds, permissionIds };
} else {
return fail(400, { revertError: 'This change requires a specialized recovery workflow that is not available yet.' });
}
} catch (error) {
if (error && typeof error === 'object' && 'status' in error) throw error;
return fail(409, { revertError: 'The previous state could not be restored because related data has changed.' });
}
await recordIsacAudit({ actorId: operator.id, category: 'audit_control', action: 'audit.reverted', entityId: eventId, before: current, after: { originalEventId: eventId, originalAction: action, targetId, restored } });
return { reverted: true, resultToken: randomUUID() };
}
revert: async ({ cookies, request }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'audit.revert',
);
const operatorPermissions = await getPermissions(operator);
const isFullAdmin = operatorPermissions.includes('*');
const eventId = String((await request.formData()).get('eventId') ?? '');
const database = await getDatabase();
const event = await (
await database.prepare('SELECT * FROM audit_log WHERE id=?')
).get(eventId);
if (!event) return fail(404, { revertError: 'Audit event not found.' });
if (!reversibleActions.has(String(event.action)) || !event.before_json)
return fail(400, {
revertError:
'This event does not contain a supported reversible change.',
});
if (
await (
await database.prepare(
"SELECT id FROM audit_log WHERE entity_type='audit_control' AND action='audit.reverted' AND entity_id=?",
)
).get(eventId)
) {
return fail(409, {
revertError: 'This audit event has already been reverted.',
});
}
const before = parseSnapshot(event.before_json),
after = parseSnapshot(event.after_json);
if (!before || !after)
return fail(400, {
revertError: 'The stored snapshots cannot be safely interpreted.',
});
const action = String(event.action),
targetId = String(event.entity_id);
let current: Record<string, unknown> | null = null;
let restored: Record<string, unknown> | null = null;
try {
if (action.startsWith('attribute.')) {
current =
(await (
await database.prepare('SELECT * FROM stat_attributes WHERE id=?')
).get(targetId)) ?? null;
if (!sameRecord(current, after))
return fail(409, {
revertError:
'The attribute changed after this event. Reversion was blocked to preserve newer work.',
});
await (
await database.prepare(`UPDATE stat_attributes SET name=?,description=?,type=?,category=?,calculation_type=?,unit=?,gear_min=?,gear_max=?,
weapon_min=?,weapon_max=?,skill_chip_min=?,skill_chip_max=?,cap_value=?,active=?,updated_at=? WHERE id=?`)
).run(
before.name,
before.description ?? null,
before.type,
before.category,
before.calculation_type,
before.unit,
before.gear_min,
before.gear_max,
before.weapon_min,
before.weapon_max,
before.skill_chip_min,
before.skill_chip_max,
before.cap_value,
before.active,
new Date().toISOString(),
targetId,
);
restored =
(await (
await database.prepare('SELECT * FROM stat_attributes WHERE id=?')
).get(targetId)) ?? null;
} else if (action.endsWith('.archived')) {
const table = action.startsWith('brand_set.')
? 'brand_sets'
: action.startsWith('gear.')
? 'gear'
: action.startsWith('weapon.')
? 'weapons'
: '';
if (!table)
return fail(400, { revertError: 'Unsupported archive category.' });
current =
(await (
await database.prepare(`SELECT * FROM ${table} WHERE id=?`)
).get(targetId)) ?? null;
if (!sameRecord(current, after))
return fail(409, {
revertError:
'The record changed after it was archived. Reversion was blocked.',
});
await (
await database.prepare(
`UPDATE ${table} SET active=?,updated_at=? WHERE id=?`,
)
).run(before.active, new Date().toISOString(), targetId);
restored =
(await (
await database.prepare(`SELECT * FROM ${table} WHERE id=?`)
).get(targetId)) ?? null;
} else if (action === 'role.updated') {
const currentRole = await (
await database.prepare(
'SELECT name,description FROM roles WHERE id=?',
)
).get(targetId);
const currentPermissions = await (
await database.prepare(
`SELECT permissions.name FROM role_permissions JOIN permissions ON permissions.id=role_permissions.permission_id WHERE role_id=? ORDER BY permissions.name`,
)
).all(targetId);
current = {
...currentRole,
permissions: currentPermissions.map((row) => row.name),
};
if (comparable(current) !== comparable(after))
return fail(409, {
revertError:
'The role changed after this event. Reversion was blocked.',
});
const permissionNames = Array.isArray(before.permissions)
? before.permissions.map(String)
: [];
if (permissionNames.includes('system.admin') && !isFullAdmin)
return fail(403, {
revertError:
'Only a full administrator can restore unrestricted system access.',
});
const permissionRows = permissionNames.length
? await (
await database.prepare(
`SELECT id,name FROM permissions WHERE name IN (${permissionNames.map(() => '?').join(',')})`,
)
).all(...permissionNames)
: [];
if (permissionRows.length !== permissionNames.length)
return fail(409, {
revertError: 'A previous permission no longer exists.',
});
await (
await database.prepare(
'UPDATE roles SET name=?,description=? WHERE id=?',
)
).run(before.name, before.description ?? null, targetId);
await (
await database.prepare('DELETE FROM role_permissions WHERE role_id=?')
).run(targetId);
for (const permission of permissionRows)
await (
await database.prepare(
'INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)',
)
).run(targetId, permission.id);
restored = {
name: before.name,
description: before.description ?? null,
permissions: permissionNames,
};
} else if (action === 'access.updated') {
const currentRoles = await (
await database.prepare(
'SELECT role_id FROM user_roles WHERE user_id=? ORDER BY role_id',
)
).all(targetId);
const currentPermissions = await (
await database.prepare(
'SELECT permission_id FROM user_permissions WHERE user_id=? ORDER BY permission_id',
)
).all(targetId);
current = {
roleIds: currentRoles.map((row) => row.role_id),
permissionIds: currentPermissions.map((row) => row.permission_id),
};
if (comparable(current) !== comparable(after))
return fail(409, {
revertError:
'The user’s access changed after this event. Reversion was blocked.',
});
const roleIds = Array.isArray(before.roleIds)
? before.roleIds.map(String)
: [];
const permissionIds = Array.isArray(before.permissionIds)
? before.permissionIds.map(String)
: [];
const restoresAdminRole = roleIds.includes('role-admin');
const restoresSystemPermission = permissionIds.includes(
'permission-system-admin',
);
if ((restoresAdminRole || restoresSystemPermission) && !isFullAdmin)
return fail(403, {
revertError:
'Only a full administrator can restore administrator access.',
});
const roleCount = roleIds.length
? Number(
(
await (
await database.prepare(
`SELECT COUNT(*) AS count FROM roles WHERE id IN (${roleIds.map(() => '?').join(',')})`,
)
).get(...roleIds)
)?.count,
)
: 0;
const permissionCount = permissionIds.length
? Number(
(
await (
await database.prepare(
`SELECT COUNT(*) AS count FROM permissions WHERE id IN (${permissionIds.map(() => '?').join(',')})`,
)
).get(...permissionIds)
)?.count,
)
: 0;
if (
roleCount !== roleIds.length ||
permissionCount !== permissionIds.length
)
return fail(409, {
revertError: 'A previous role or permission no longer exists.',
});
await (
await database.prepare('DELETE FROM user_roles WHERE user_id=?')
).run(targetId);
await (
await database.prepare('DELETE FROM user_permissions WHERE user_id=?')
).run(targetId);
for (const roleId of roleIds)
await (
await database.prepare(
'INSERT INTO user_roles(user_id,role_id) VALUES(?,?)',
)
).run(targetId, roleId);
for (const permissionId of permissionIds)
await (
await database.prepare(
'INSERT INTO user_permissions(user_id,permission_id) VALUES(?,?)',
)
).run(targetId, permissionId);
restored = { roleIds, permissionIds };
} else {
return fail(400, {
revertError:
'This change requires a specialized recovery workflow that is not available yet.',
});
}
} catch (error) {
if (error && typeof error === 'object' && 'status' in error) throw error;
return fail(409, {
revertError:
'The previous state could not be restored because related data has changed.',
});
}
await recordIsacAudit({
actorId: operator.id,
category: 'audit_control',
action: 'audit.reverted',
entityId: eventId,
before: current,
after: {
originalEventId: eventId,
originalAction: action,
targetId,
restored,
},
});
return { reverted: true, resultToken: randomUUID() };
},
};