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
+339 -181
View File
@@ -1,7 +1,14 @@
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, getRegistrySearch, getRegistrySort } from '$lib/server/pagination';
import {
getPagination,
getRegistrySearch,
getRegistrySort,
} from '$lib/server/pagination';
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
@@ -9,206 +16,357 @@ import { recordIsacAudit } from '$lib/server/audit';
const filters = ['isac', 'data', 'users', 'audit', 'system'];
const seededRoleIds = new Set([
'role-data-editor',
'role-user-operator',
'role-access-manager',
'role-admin',
'role-moderator',
'role-data-viewer',
'role-data-contributor',
'role-data-approver',
'role-auditor'
'role-data-editor',
'role-user-operator',
'role-access-manager',
'role-admin',
'role-moderator',
'role-data-viewer',
'role-data-contributor',
'role-data-approver',
'role-auditor',
]);
export const load: PageServerLoad = async ({ cookies, url }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'users.roles');
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const database = await getDatabase();
const filter = filters.includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
const { search, pattern } = getRegistrySearch(url);
const sort = getRegistrySort(url, {
name: { asc: 'roles.name COLLATE NOCASE ASC', desc: 'roles.name COLLATE NOCASE DESC' },
users: { asc: 'user_count ASC', desc: 'user_count DESC', defaultDirection: 'desc' },
permissions: {
asc: '(SELECT COUNT(*) FROM role_permissions WHERE role_id=roles.id) ASC',
desc: '(SELECT COUNT(*) FROM role_permissions WHERE role_id=roles.id) DESC',
defaultDirection: 'desc'
},
state: {
asc: "CASE WHEN roles.id='role-admin' THEN 1 ELSE 0 END ASC",
desc: "CASE WHEN roles.id='role-admin' THEN 1 ELSE 0 END DESC"
}
}, 'name');
const permissionPrefix = filter === 'isac' ? 'isac.%' : filter === 'data' ? 'data.%' : filter === 'users' ? 'users.%' : filter === 'audit' ? 'audit.%' : filter === 'system' ? 'system.%' : '';
const relatedPermissionFilter = filter === 'data'
? " OR p.name LIKE 'calculation.%' OR p.name LIKE 'ai.%'"
: filter === 'system'
? " OR p.name LIKE 'site.%' OR p.name LIKE 'metrics.%' OR p.name LIKE 'images.%'"
: '';
const conditions = [
...(permissionPrefix ? [`EXISTS (
const operator = await requirePermission(
await getSessionUser(cookies),
'users.roles',
);
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const database = await getDatabase();
const filter = filters.includes(url.searchParams.get('filter') ?? '')
? String(url.searchParams.get('filter'))
: '';
const { search, pattern } = getRegistrySearch(url);
const sort = getRegistrySort(
url,
{
name: {
asc: 'roles.name COLLATE NOCASE ASC',
desc: 'roles.name COLLATE NOCASE DESC',
},
users: {
asc: 'user_count ASC',
desc: 'user_count DESC',
defaultDirection: 'desc',
},
permissions: {
asc: '(SELECT COUNT(*) FROM role_permissions WHERE role_id=roles.id) ASC',
desc: '(SELECT COUNT(*) FROM role_permissions WHERE role_id=roles.id) DESC',
defaultDirection: 'desc',
},
state: {
asc: "CASE WHEN roles.id='role-admin' THEN 1 ELSE 0 END ASC",
desc: "CASE WHEN roles.id='role-admin' THEN 1 ELSE 0 END DESC",
},
},
'name',
);
const permissionPrefix =
filter === 'isac'
? 'isac.%'
: filter === 'data'
? 'data.%'
: filter === 'users'
? 'users.%'
: filter === 'audit'
? 'audit.%'
: filter === 'system'
? 'system.%'
: '';
const relatedPermissionFilter =
filter === 'data'
? " OR p.name LIKE 'calculation.%' OR p.name LIKE 'ai.%'"
: filter === 'system'
? " OR p.name LIKE 'site.%' OR p.name LIKE 'metrics.%' OR p.name LIKE 'images.%'"
: '';
const conditions = [
...(permissionPrefix
? [
`EXISTS (
SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id
WHERE rp.role_id=roles.id AND (p.name LIKE ?${relatedPermissionFilter})
)`] : []),
...(search ? ["roles.name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
];
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const filterArgs = [...(permissionPrefix ? [permissionPrefix] : []), ...(search ? [pattern] : [])];
const [summary, filtered, permissions] = await Promise.all([
(await database.prepare(`SELECT COUNT(*) AS total,
)`,
]
: []),
...(search ? ["roles.name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : []),
];
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const filterArgs = [
...(permissionPrefix ? [permissionPrefix] : []),
...(search ? [pattern] : []),
];
const [summary, filtered, permissions] = await Promise.all([
(
await database.prepare(`SELECT COUNT(*) AS total,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'isac.%') THEN 1 ELSE 0 END) AS isac,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND (p.name LIKE 'data.%' OR p.name LIKE 'calculation.%' OR p.name LIKE 'ai.%')) THEN 1 ELSE 0 END) AS data,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'users.%') THEN 1 ELSE 0 END) AS users,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'audit.%') THEN 1 ELSE 0 END) AS audit
,SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND (p.name LIKE 'system.%' OR p.name LIKE 'site.%' OR p.name LIKE 'metrics.%' OR p.name LIKE 'images.%')) THEN 1 ELSE 0 END) AS system
FROM roles`)).get(),
(await database.prepare(`SELECT COUNT(*) AS total FROM roles ${where}`)).get(...filterArgs),
(await database.prepare(`SELECT id,name,description FROM permissions
${canManageSystemAdmin ? '' : "WHERE name<>'system.admin'"} ORDER BY name COLLATE NOCASE`)).all()
]);
const pagination = getPagination(url, Number(filtered?.total ?? 0));
const roles = await (await database.prepare(`SELECT roles.*,
FROM roles`)
).get(),
(
await database.prepare(`SELECT COUNT(*) AS total FROM roles ${where}`)
).get(...filterArgs),
(
await database.prepare(`SELECT id,name,description FROM permissions
${canManageSystemAdmin ? '' : "WHERE name<>'system.admin'"} ORDER BY name COLLATE NOCASE`)
).all(),
]);
const pagination = getPagination(url, Number(filtered?.total ?? 0));
const roles = await (
await database.prepare(`SELECT roles.*,
(SELECT COUNT(*) FROM user_roles WHERE role_id=roles.id) AS user_count
FROM roles ${where} ORDER BY ${sort.orderBy},roles.name COLLATE NOCASE,roles.id
LIMIT ? OFFSET ?`)).all(...filterArgs, pagination.perPage, pagination.offset);
const roleIds = roles.map((role) => String(role.id));
const links = roleIds.length ? await (await database.prepare(`SELECT role_id,permission_id FROM role_permissions
WHERE role_id IN (${roleIds.map(() => '?').join(',')})`)).all(...roleIds) : [];
return {
roles: roles.map((role) => ({
...role,
permissionIds: links.filter((link) => link.role_id === role.id).map((link) => String(link.permission_id)),
systemRole: seededRoleIds.has(String(role.id)),
protected: role.id === 'role-admin',
canEdit: role.id !== 'role-admin' || canManageSystemAdmin,
canDelete: role.id !== 'role-admin'
&& (canManageSystemAdmin || !seededRoleIds.has(String(role.id)))
})),
permissions,
summary,
filter,
search,
sort,
pagination,
canManageSystemAdmin
};
LIMIT ? OFFSET ?`)
).all(...filterArgs, pagination.perPage, pagination.offset);
const roleIds = roles.map((role) => String(role.id));
const links = roleIds.length
? await (
await database.prepare(`SELECT role_id,permission_id FROM role_permissions
WHERE role_id IN (${roleIds.map(() => '?').join(',')})`)
).all(...roleIds)
: [];
return {
roles: roles.map((role) => ({
...role,
permissionIds: links
.filter((link) => link.role_id === role.id)
.map((link) => String(link.permission_id)),
systemRole: seededRoleIds.has(String(role.id)),
protected: role.id === 'role-admin',
canEdit: role.id !== 'role-admin' || canManageSystemAdmin,
canDelete:
role.id !== 'role-admin' &&
(canManageSystemAdmin || !seededRoleIds.has(String(role.id))),
})),
permissions,
summary,
filter,
search,
sort,
pagination,
canManageSystemAdmin,
};
};
export const actions: Actions = {
save: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'users.roles');
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const form = await request.formData();
const id = String(form.get('id') ?? '');
const name = String(form.get('name') ?? '').normalize('NFKC').trim();
const description = String(form.get('description') ?? '').normalize('NFKC').trim();
const permissionIds = [...new Set(form.getAll('permissionIds').map(String))];
if (name.length < 2 || name.length > 60) return fail(400, { roleError: 'Role names must be 2–60 characters.' });
if (description.length > 240) return fail(400, { roleError: 'Descriptions must be 240 characters or fewer.' });
const database = await getDatabase();
const existing = id ? await (await database.prepare('SELECT id,name,description FROM roles WHERE id=?')).get(id) : null;
if (id && !existing) return fail(404, { roleError: 'Role not found.' });
if (id === 'role-admin' && !canManageSystemAdmin) {
return fail(403, { roleError: 'Only a full administrator can modify the core Admin role.' });
}
if (await (await database.prepare('SELECT id FROM roles WHERE name=? COLLATE NOCASE AND id<>?')).get(name, id || 'new')) {
return fail(409, { roleError: 'A role with that name already exists.' });
}
const validPermissions = permissionIds.length ? await (await database.prepare(`SELECT id,name FROM permissions WHERE id IN (${permissionIds.map(() => '?').join(',')})`)).all(...permissionIds) : [];
if (validPermissions.length !== permissionIds.length) return fail(400, { roleError: 'One or more permissions are invalid.' });
if (!canManageSystemAdmin && validPermissions.some((permission) => permission.name === 'system.admin')) {
return fail(403, { roleError: 'Only a full administrator can grant unrestricted system access.' });
}
const roleId = id || randomUUID();
const beforeLinks = id ? 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(id) : [];
if (id) await (await database.prepare('UPDATE roles SET name=?,description=? WHERE id=?')).run(name, description || null, id);
else await (await database.prepare('INSERT INTO roles(id,name,description) VALUES(?,?,?)')).run(roleId, name, description || null);
await (await database.prepare('DELETE FROM role_permissions WHERE role_id=?')).run(roleId);
for (const permissionId of permissionIds) {
await (await database.prepare('INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)')).run(roleId, permissionId);
}
await recordIsacAudit({
actorId: operator.id, category: 'role_management', action: id ? 'role.updated' : 'role.created', entityId: roleId,
before: id ? { name: existing?.name, description: existing?.description, permissions: beforeLinks.map((row) => row.name) } : null,
after: { name, description: description || null, permissions: validPermissions.map((row) => row.name) }
});
return { roleSaved: true, roleMode: id ? 'edit' : 'add', resultToken: randomUUID() };
},
delete: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'users.roles');
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const confirmation = String(form.get('confirmation') ?? '').normalize('NFKC').trim();
if (!id) return fail(400, { roleDeleteError: 'Choose a role to delete.' });
if (id === 'role-admin') {
return fail(403, { roleDeleteError: 'The core Admin role cannot be deleted.' });
}
if (seededRoleIds.has(id) && !canManageSystemAdmin) {
return fail(403, { roleDeleteError: 'Only a full administrator can delete a seeded system role.' });
}
const database = await getDatabase();
const role = await (await database.prepare(
'SELECT id,name,description FROM roles WHERE id=?'
)).get(id);
if (!role) return fail(404, { roleDeleteError: 'Role not found.' });
if (confirmation !== String(role.name)) {
return fail(400, { roleDeleteError: 'Enter the exact role name to confirm deletion.' });
}
const [permissions, assignedUsers] = await Promise.all([
(await database.prepare(`SELECT permissions.name FROM role_permissions
save: async ({ cookies, request }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'users.roles',
);
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const form = await request.formData();
const id = String(form.get('id') ?? '');
const name = String(form.get('name') ?? '')
.normalize('NFKC')
.trim();
const description = String(form.get('description') ?? '')
.normalize('NFKC')
.trim();
const permissionIds = [
...new Set(form.getAll('permissionIds').map(String)),
];
if (name.length < 2 || name.length > 60)
return fail(400, { roleError: 'Role names must be 2–60 characters.' });
if (description.length > 240)
return fail(400, {
roleError: 'Descriptions must be 240 characters or fewer.',
});
const database = await getDatabase();
const existing = id
? await (
await database.prepare(
'SELECT id,name,description FROM roles WHERE id=?',
)
).get(id)
: null;
if (id && !existing) return fail(404, { roleError: 'Role not found.' });
if (id === 'role-admin' && !canManageSystemAdmin) {
return fail(403, {
roleError: 'Only a full administrator can modify the core Admin role.',
});
}
if (
await (
await database.prepare(
'SELECT id FROM roles WHERE name=? COLLATE NOCASE AND id<>?',
)
).get(name, id || 'new')
) {
return fail(409, { roleError: 'A role with that name already exists.' });
}
const validPermissions = permissionIds.length
? await (
await database.prepare(
`SELECT id,name FROM permissions WHERE id IN (${permissionIds.map(() => '?').join(',')})`,
)
).all(...permissionIds)
: [];
if (validPermissions.length !== permissionIds.length)
return fail(400, { roleError: 'One or more permissions are invalid.' });
if (
!canManageSystemAdmin &&
validPermissions.some((permission) => permission.name === 'system.admin')
) {
return fail(403, {
roleError:
'Only a full administrator can grant unrestricted system access.',
});
}
const roleId = id || randomUUID();
const beforeLinks = id
? 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(id)
: [];
if (id)
await (
await database.prepare(
'UPDATE roles SET name=?,description=? WHERE id=?',
)
).run(name, description || null, id);
else
await (
await database.prepare(
'INSERT INTO roles(id,name,description) VALUES(?,?,?)',
)
).run(roleId, name, description || null);
await (
await database.prepare('DELETE FROM role_permissions WHERE role_id=?')
).run(roleId);
for (const permissionId of permissionIds) {
await (
await database.prepare(
'INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)',
)
).run(roleId, permissionId);
}
await recordIsacAudit({
actorId: operator.id,
category: 'role_management',
action: id ? 'role.updated' : 'role.created',
entityId: roleId,
before: id
? {
name: existing?.name,
description: existing?.description,
permissions: beforeLinks.map((row) => row.name),
}
: null,
after: {
name,
description: description || null,
permissions: validPermissions.map((row) => row.name),
},
});
return {
roleSaved: true,
roleMode: id ? 'edit' : 'add',
resultToken: randomUUID(),
};
},
delete: async ({ cookies, request }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'users.roles',
);
const operatorPermissions = await getPermissions(operator);
const canManageSystemAdmin = operatorPermissions.includes('*');
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const confirmation = String(form.get('confirmation') ?? '')
.normalize('NFKC')
.trim();
if (!id) return fail(400, { roleDeleteError: 'Choose a role to delete.' });
if (id === 'role-admin') {
return fail(403, {
roleDeleteError: 'The core Admin role cannot be deleted.',
});
}
if (seededRoleIds.has(id) && !canManageSystemAdmin) {
return fail(403, {
roleDeleteError:
'Only a full administrator can delete a seeded system role.',
});
}
const database = await getDatabase();
const role = await (
await database.prepare('SELECT id,name,description FROM roles WHERE id=?')
).get(id);
if (!role) return fail(404, { roleDeleteError: 'Role not found.' });
if (confirmation !== String(role.name)) {
return fail(400, {
roleDeleteError: 'Enter the exact role name to confirm deletion.',
});
}
const [permissions, assignedUsers] = await Promise.all([
(
await database.prepare(`SELECT permissions.name FROM role_permissions
JOIN permissions ON permissions.id=role_permissions.permission_id
WHERE role_permissions.role_id=? ORDER BY permissions.name COLLATE NOCASE`)).all(id),
(await database.prepare(`SELECT users.id,
WHERE role_permissions.role_id=? ORDER BY permissions.name COLLATE NOCASE`)
).all(id),
(
await database.prepare(`SELECT users.id,
COALESCE(user_profiles.display_name,users.username) AS display_name
FROM user_roles JOIN users ON users.id=user_roles.user_id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
WHERE user_roles.role_id=?
ORDER BY display_name COLLATE NOCASE,users.id`)).all(id)
]);
await database.exec('BEGIN IMMEDIATE');
try {
if (seededRoleIds.has(id)) {
await (await database.prepare(`INSERT INTO deleted_roles(role_id,deleted_at,deleted_by)
ORDER BY display_name COLLATE NOCASE,users.id`)
).all(id),
]);
await database.exec('BEGIN IMMEDIATE');
try {
if (seededRoleIds.has(id)) {
await (
await database.prepare(`INSERT INTO deleted_roles(role_id,deleted_at,deleted_by)
VALUES(?,?,?)
ON CONFLICT(role_id) DO UPDATE SET
deleted_at=excluded.deleted_at,
deleted_by=excluded.deleted_by`))
.run(id, new Date().toISOString(), operator.id);
}
await (await database.prepare('DELETE FROM user_roles WHERE role_id=?')).run(id);
await (await database.prepare('DELETE FROM role_permissions WHERE role_id=?')).run(id);
const deleted = await (await database.prepare('DELETE FROM roles WHERE id=?')).run(id);
if (!deleted.changes) throw new Error('The role changed before it could be deleted.');
await recordIsacAudit({
actorId: operator.id,
category: 'role_management',
action: 'role.deleted',
entityId: id,
before: {
name: String(role.name),
description: role.description ? String(role.description) : null,
permissions: permissions.map((row) => String(row.name)),
assignedUsers: assignedUsers.map((row) => ({
id: String(row.id),
displayName: String(row.display_name)
}))
},
after: {
deleted: true,
systemRole: seededRoleIds.has(id),
removedAssignmentCount: assignedUsers.length
}
});
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
return { roleDeleted: true, resultToken: randomUUID() };
}
deleted_by=excluded.deleted_by`)
).run(id, new Date().toISOString(), operator.id);
}
await (
await database.prepare('DELETE FROM user_roles WHERE role_id=?')
).run(id);
await (
await database.prepare('DELETE FROM role_permissions WHERE role_id=?')
).run(id);
const deleted = await (
await database.prepare('DELETE FROM roles WHERE id=?')
).run(id);
if (!deleted.changes)
throw new Error('The role changed before it could be deleted.');
await recordIsacAudit({
actorId: operator.id,
category: 'role_management',
action: 'role.deleted',
entityId: id,
before: {
name: String(role.name),
description: role.description ? String(role.description) : null,
permissions: permissions.map((row) => String(row.name)),
assignedUsers: assignedUsers.map((row) => ({
id: String(row.id),
displayName: String(row.display_name),
})),
},
after: {
deleted: true,
systemRole: seededRoleIds.has(id),
removedAssignmentCount: assignedUsers.length,
},
});
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
return { roleDeleted: true, resultToken: randomUUID() };
},
};