feat: roles SPA foundations

This commit is contained in:
Florian Sylvain committed 2026-10-27 22:21:12 +01:00
1 parent 80f7037d3a
commit 907949aa12
180 files changed
+4308 -835

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
import type { Access, Grant, RoleRef, Scope } from '../lib/permissions'
export const CATALOGUE_KEYS = [
'content.read',
'content.create',
'content.update',
'content.delete',
'content.publish',
'schema.view',
'schema.edit',
'schema.delete',
'media.view',
'media.upload',
'media.delete',
'authors.view',
'authors.create',
'authors.edit',
'authors.delete',
'activity.view',
'activity.export',
'users.view',
'users.invite',
'users.change_role',
'users.remove',
'settings.view',
'settings.edit_general',
'settings.manage_sites',
'settings.delete_site',
'apikeys.view',
'apikeys.create',
'apikeys.revoke',
'roles.view',
'roles.manage',
'roles.edit_owner_only',
'system.apply_updates',
'system.edit_retention',
'system.transfer_ownership',
] as const
export const OWNER_ONLY_KEYS = [
'settings.delete_site',
'roles.edit_owner_only',
'system.apply_updates',
'system.edit_retention',
'system.transfer_ownership',
] as const
export const OWNER_ROLE: RoleRef = { id: 1, name: 'Owner', built_in: true, locked: true }
export const ADMIN_ROLE: RoleRef = { id: 2, name: 'Admin', built_in: true, locked: false }
export const WRITER_ROLE: RoleRef = { id: 3, name: 'Writer', built_in: true, locked: false }
const WRITER_GRANTS: Grant[] = [
{ key: 'content.read', scope: 'all' },
{ key: 'content.create', scope: 'all' },
{ key: 'content.update', scope: 'all' },
{ key: 'schema.view', scope: 'all' },
{ key: 'media.view', scope: 'all' },
{ key: 'media.upload', scope: 'all' },
{ key: 'authors.view', scope: 'all' },
{ key: 'authors.edit', scope: 'own' },
{ key: 'activity.view', scope: 'own' },
]
function everyKey(skip: readonly string[] = []): Grant[] {
return CATALOGUE_KEYS.filter((key) => !skip.includes(key)).map((key) => ({
key,
scope: 'all' as Scope,
}))
}
export function grantsFrom(spec: string): Grant[] {
return spec
.split(',')
.map((part) => part.trim())
.filter((part) => part !== '')
.map((part) => {
const [key, scope = 'all'] = part.split('=')
return { key: key!.trim(), scope: scope.trim() as Scope }
})
}
export function ownerAccess(): Access {
return { role: { ...OWNER_ROLE }, permissions: everyKey() }
}
export function adminAccess(): Access {
return { role: { ...ADMIN_ROLE }, permissions: everyKey(OWNER_ONLY_KEYS) }
}
export function writerAccess(): Access {
return { role: { ...WRITER_ROLE }, permissions: WRITER_GRANTS.map((grant) => ({ ...grant })) }
}
export function noAccess(name = 'Guest'): Access {
return { role: { id: 9, name, built_in: false, locked: false }, permissions: [] }
}
export function customAccess(grants: string | Grant[], name = 'Contributor', id = 7): Access {
return {
role: { id, name, built_in: false, locked: false },
permissions: typeof grants === 'string' ? grantsFrom(grants) : grants,
}
}
function withoutKeys(access: Access, drop: (key: string) => boolean): Access {
return {
role: { ...access.role, locked: false, name: 'Custom' },
permissions: access.permissions.filter((grant) => !drop(grant.key)),
}
}
export function accessFor(role?: string): Access | undefined {
switch (role) {
case 'owner':
return ownerAccess()
case 'admin':
return adminAccess()
case 'viewer':
return customAccess('content.read, settings.view', 'Viewer')
case 'keyholder':
return customAccess('content.read, apikeys.view=own, apikeys.create', 'Key holder')
case 'noschema':
return withoutKeys(ownerAccess(), (key) => key.startsWith('schema.'))
case 'member':
case 'editor':
case 'verified':
return writerAccess()
case 'user':
return noAccess()
default:
return undefined
}
}
export function meBody(role?: string, extra: Record<string, unknown> = {}) {
const access = accessFor(role)
return { ...(access ? { access } : {}), ...extra }
}
export function roleRefFor(role?: string | null): RoleRef | null {
switch (role) {
case 'owner':
return { ...OWNER_ROLE }
case 'admin':
return { ...ADMIN_ROLE }
case 'member':
case 'editor':
return { ...WRITER_ROLE }
default:
return null
}
}