feat: roles/permissions system and Nostr profile display on admin users
Introduce granular role-based permissions with SuperAdmin env override, admin roles UI, and permission-gated API routes. Fix admin user Nostr metadata by batching relay profile fetches, normalizing npub pubkeys to hex, and adding reusable NostrAvatar/useNostrProfile components. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// Central permission registry. This is the single source of truth for every
|
||||
// granular permission key in the dashboard. Adding a key here automatically
|
||||
// surfaces it in the GET /admin/permissions endpoint and the Roles matrix UI.
|
||||
// To add a new permission: add an entry to PERMISSIONS below, then (optionally)
|
||||
// grant it to roles by default in DEFAULT_ROLE_PERMISSIONS and the migration.
|
||||
|
||||
export type PermissionKey = string;
|
||||
|
||||
export interface PermissionDef {
|
||||
key: PermissionKey;
|
||||
label: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
// Ordered list of every permission, grouped by feature area for the matrix UI.
|
||||
export const PERMISSIONS: PermissionDef[] = [
|
||||
{ key: 'events.create', label: 'Create events', group: 'Events' },
|
||||
{ key: 'events.edit', label: 'Edit events', group: 'Events' },
|
||||
{ key: 'events.delete', label: 'Delete events', group: 'Events' },
|
||||
|
||||
{ key: 'organizers.manage', label: 'Manage organizers', group: 'Organizers' },
|
||||
|
||||
{ key: 'gallery.upload', label: 'Upload media', group: 'Gallery' },
|
||||
{ key: 'gallery.delete', label: 'Delete media', group: 'Gallery' },
|
||||
|
||||
{ key: 'blog.draft', label: 'Draft and edit posts', group: 'Blog' },
|
||||
{ key: 'blog.publish', label: 'Publish posts', group: 'Blog' },
|
||||
{ key: 'blog.delete', label: 'Delete posts', group: 'Blog' },
|
||||
|
||||
{ key: 'faq.manage', label: 'Manage FAQ', group: 'FAQ' },
|
||||
|
||||
{ key: 'submissions.review', label: 'Review submissions', group: 'Submissions' },
|
||||
|
||||
{ key: 'board.manage', label: 'Moderate the message board', group: 'Board' },
|
||||
|
||||
{ key: 'moderation.act', label: 'Hide content and block pubkeys', group: 'Moderation' },
|
||||
|
||||
{ key: 'categories.manage', label: 'Manage categories', group: 'Categories' },
|
||||
|
||||
{ key: 'users.assign_role', label: 'Assign user roles', group: 'Users' },
|
||||
{ key: 'nip05.assign', label: 'Assign NIP-05 usernames', group: 'Users' },
|
||||
|
||||
{ key: 'relays.manage', label: 'Manage relays', group: 'Relays' },
|
||||
|
||||
{ key: 'settings.edit', label: 'Edit site settings', group: 'Settings' },
|
||||
|
||||
{ key: 'roles.edit_permissions', label: 'Edit roles and permissions', group: 'Roles' },
|
||||
|
||||
{ key: 'nostr_tools.use', label: 'Use Nostr tools', group: 'Nostr Tools' },
|
||||
];
|
||||
|
||||
// Assignable roles, highest to lowest. SuperAdmin is env-sourced and never stored
|
||||
// or assignable, so it is not part of this list. Guest is the absence of a role.
|
||||
export const ASSIGNABLE_ROLES = ['admin', 'moderator', 'writer'] as const;
|
||||
export type AssignableRole = (typeof ASSIGNABLE_ROLES)[number];
|
||||
|
||||
export type EffectiveRole = 'superadmin' | AssignableRole | 'guest';
|
||||
|
||||
// Hierarchy ranks used for safeguard comparisons.
|
||||
export const ROLE_RANK: Record<EffectiveRole, number> = {
|
||||
superadmin: 4,
|
||||
admin: 3,
|
||||
moderator: 2,
|
||||
writer: 1,
|
||||
guest: 0,
|
||||
};
|
||||
|
||||
export const ALL_PERMISSION_KEYS: ReadonlySet<PermissionKey> = new Set(
|
||||
PERMISSIONS.map((p) => p.key)
|
||||
);
|
||||
|
||||
export function isValidPermissionKey(key: string): boolean {
|
||||
return ALL_PERMISSION_KEYS.has(key);
|
||||
}
|
||||
|
||||
export function isAssignableRole(role: unknown): role is AssignableRole {
|
||||
return typeof role === 'string' && (ASSIGNABLE_ROLES as readonly string[]).includes(role);
|
||||
}
|
||||
|
||||
// Default permission sets seeded per role. Admin gets everything except the
|
||||
// roles editor, which stays SuperAdmin-only by default. Guest gets nothing and
|
||||
// is therefore not represented here.
|
||||
export const DEFAULT_ROLE_PERMISSIONS: Record<AssignableRole, PermissionKey[]> = {
|
||||
admin: PERMISSIONS.map((p) => p.key).filter((k) => k !== 'roles.edit_permissions'),
|
||||
moderator: [
|
||||
'events.create',
|
||||
'events.edit',
|
||||
'events.delete',
|
||||
'submissions.review',
|
||||
'moderation.act',
|
||||
'gallery.upload',
|
||||
'board.manage',
|
||||
'categories.manage',
|
||||
'faq.manage',
|
||||
],
|
||||
writer: ['blog.draft', 'gallery.upload', 'events.create'],
|
||||
};
|
||||
Reference in New Issue
Block a user