Roles and Permissions Guide
This guide covers everything you need to know when adding new permissions or modifying the roles system in Lumio.
Concepts
Permission Format
Permissions follow the resource:action format:
events:read,events:createchat:ban,chat:timeoutautomations:execute
Rules:
- Use granular scopes. Every resource needs at minimum
readplus granularcreate/edit/delete. - Add domain-specific actions where the CRUD model is insufficient (e.g.,
automations:execute,chat:ban). - Never use wildcard permissions like
resource:*or*:*for account-level permissions. A:managesuffix is allowed only when it is an explicit, registry-listed action.
Role Properties
| Property | Type | Description |
|---|---|---|
slug | string | Machine-readable identifier (lowercase, stable — used in code checks) |
name | string | Human-readable display name (user-editable on custom roles) |
is_system | bool | Cannot be edited or deleted. Only the Owner role is a system role. |
is_default | bool | Cannot be deleted. All four default roles have this set. |
color | string | Hex color string, e.g. "#f59e0b" |
Default Roles
Lumio creates four default roles for every new account:
| Role | Slug | Color | Notes |
|---|---|---|---|
| Owner | owner | #f59e0b | System role — all 124 registry permissions, cannot be deleted or edited |
| Administrator | administrator | #ef4444 | 116 permissions — everything except account:delete, plan:read, plan:edit, extension-dev:payouts |
| Moderator | moderator | #22c55e | 46 permissions — chat moderation, event monitoring, read access |
| Viewer | viewer | #6b7280 | 11 permissions — read-only, plus extension-store:review and own-session management |
The definitions live in ROLE_OWNER / ROLE_ADMIN / ROLE_MODERATOR / ROLE_VIEWER in crates/lo-auth/src/rbac.rs and are inserted by create_default_roles() (apps/api/src/db/roles.rs), which sets is_default = true on all four.
Adding New Permissions
Step 1 — Define constants in rbac.rs
All account-level permission constants live in crates/lo-auth/src/rbac.rs inside the pub mod account {} block.
// crates/lo-auth/src/rbac.rs
pub mod account {
// ... existing permissions ...
/// Read resource data.
pub const RESOURCE_READ: &str = "resource:read";
/// Create new resource entries.
pub const RESOURCE_CREATE: &str = "resource:create";
/// Edit existing resource entries.
pub const RESOURCE_EDIT: &str = "resource:edit";
/// Delete resource entries.
pub const RESOURCE_DELETE: &str = "resource:delete";
}
Use descriptive doc comments — they serve as inline documentation for the permission's purpose.
Step 2 — Add to default roles in rbac.rs
Update the ROLE_OWNER, ROLE_ADMIN, ROLE_MODERATOR, and ROLE_VIEWER constants in the same file.
Decision guide:
| Role | Gets the permission? |
|---|---|
| Owner | Always — Owner gets ALL permissions |
| Administrator | Yes, unless it is owner-only (account:delete, plan:read, plan:edit, extension-dev:payouts) |
| Moderator | Read permissions + feature-specific actions relevant to moderation |
| Viewer | Read-only permissions only |
pub const ROLE_OWNER: DefaultRole = DefaultRole {
// ...
permissions: &[
// ... existing ...
account::RESOURCE_READ,
account::RESOURCE_CREATE,
account::RESOURCE_EDIT,
account::RESOURCE_DELETE,
],
// ...
};
pub const ROLE_ADMIN: DefaultRole = DefaultRole {
// ...
permissions: &[
// ... existing ...
account::RESOURCE_READ,
account::RESOURCE_CREATE,
account::RESOURCE_EDIT,
account::RESOURCE_DELETE,
],
// ...
};
pub const ROLE_MODERATOR: DefaultRole = DefaultRole {
// ...
permissions: &[
// ... existing ...
account::RESOURCE_READ,
],
// ...
};
Step 3 — Add GraphQL guards
Every GraphQL resolver that touches the new resource needs a PermissionGuard:
// apps/api/src/graphql/resource.rs
impl ResourceQuery {
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"resource:read\")")]
async fn resources(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<Resource>> {
// ...
}
}
impl ResourceMutation {
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"resource:create\")")]
async fn create_resource(
&self,
ctx: &Context<'_>,
input: CreateResourceInput,
) -> async_graphql::Result<Resource> {
// ...
}
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"resource:edit\")")]
async fn update_resource(
&self,
ctx: &Context<'_>,
id: Uuid,
input: UpdateResourceInput,
) -> async_graphql::Result<Resource> {
// ...
}
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"resource:delete\")")]
async fn delete_resource(
&self,
ctx: &Context<'_>,
id: Uuid,
) -> async_graphql::Result<bool> {
// ...
}
}
Step 4 — Add REST guards
Every Actix route handler that touches the new resource needs a permission check. Use the constant from rbac.rs rather than a string literal:
// apps/api/src/routes/resource.rs
use lo_auth::rbac::account;
pub async fn list_resources(
auth: Auth,
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
auth.require_permission(account::RESOURCE_READ)
.map_err(from_auth_error)?;
// ...
}
pub async fn create_resource(
auth: Auth,
state: web::Data<AppState>,
body: web::Json<CreateResourceRequest>,
) -> Result<HttpResponse, ApiError> {
auth.require_permission(account::RESOURCE_CREATE)
.map_err(from_auth_error)?;
// ...
}
Step 5 — Register the permission in the registry
There is exactly one list to edit: account::get_all_account_permission_infos() in crates/lo-auth/src/rbac.rs. It returns every account permission with the label and category the role editor renders.
// crates/lo-auth/src/rbac.rs — inside get_all_account_permission_infos()
// Resource
p!(RESOURCE_READ, "Read Resource", "Resource"),
p!(RESOURCE_CREATE, "Create Resource", "Resource"),
p!(RESOURCE_EDIT, "Edit Resource", "Resource"),
p!(RESOURCE_DELETE, "Delete Resource", "Resource"),
Everything downstream derives from it and needs no edit:
account::get_all_account_permissions()— the validation registry consulted by role create/update on both protocols (apps/api/src/{graphql,routes}/roles.rs). It maps the info list's keys, so a key the editor hides can never be accepted by validation and vice versa (ZAF-231).- The web role editor pickers: REST
GET /v1/roles/permissionsand GraphQLavailablePermissions, both guarded onroles:read. - The admin override pickers via
adminAssignablePermissions(ZAF-430).
Unit tests in both roles.rs files assert the picker set equals the registry set, so a divergence fails CI rather than shipping.
Step 6 — Write a DB migration
Existing accounts already have their roles in the database. The migration must backfill the new permissions for default roles. New migrations use the reversible format with .up.sql and .down.sql suffixes (YYYYMMDD######_description.up.sql / .down.sql); see apps/api/migrations/ for examples.
-- apps/api/migrations/20260401000001_add_resource_permissions.up.sql
--
-- Add resource permissions to existing default roles.
-- Owner: all permissions
INSERT INTO account_role_permissions (role_id, permission)
SELECT ar.id, perm.permission
FROM account_roles ar
CROSS JOIN (
VALUES
('resource:read'),
('resource:create'),
('resource:edit'),
('resource:delete')
) AS perm(permission)
WHERE ar.slug = 'owner'
ON CONFLICT DO NOTHING;
-- Administrator: all permissions
INSERT INTO account_role_permissions (role_id, permission)
SELECT ar.id, perm.permission
FROM account_roles ar
CROSS JOIN (
VALUES
('resource:read'),
('resource:create'),
('resource:edit'),
('resource:delete')
) AS perm(permission)
WHERE ar.slug = 'administrator'
ON CONFLICT DO NOTHING;
-- Moderator: read only
INSERT INTO account_role_permissions (role_id, permission)
SELECT ar.id, perm.permission
FROM account_roles ar
CROSS JOIN (
VALUES
('resource:read')
) AS perm(permission)
WHERE ar.slug = 'moderator'
ON CONFLICT DO NOTHING;
Note: ON CONFLICT DO NOTHING makes migrations safe to re-run.
Frontend Translations (Critical)
Translations must be updated in both apps/web/messages/en.json and apps/web/messages/de.json.
Permission labels
Each permission key must have a translation in the "permissions" namespace:
{
"permissions": {
"resource:read": "Read Resource",
"resource:create": "Create Resource",
"resource:edit": "Edit Resource",
"resource:delete": "Delete Resource"
}
}
Permission category
Each distinct category value used in get_all_account_permissions() must have a translation under "permissions.categories":
{
"permissions": {
"categories": {
"Resource": "Resource"
}
}
}
Default role names
Default role display names are translated via "roles.labels" keyed by slug:
{
"roles": {
"labels": {
"owner": "Owner",
"administrator": "Administrator",
"moderator": "Moderator",
"viewer": "Viewer"
}
}
}
Use the useTranslateRole() hook (from apps/web/src/lib/translate-role.ts) when displaying role names — it returns a (slug, name) => string function. Default role slugs are translated; custom roles fall back to their name field unchanged.
Frontend Permission Checks
Sidebar navigation
Add a permission-gated nav entry in apps/web/src/app/(main)/(app)/shell.tsx:
// apps/web/src/app/(main)/(app)/shell.tsx
{
label: "manage",
items: [
// ...
{
href: "/dashboard/resource",
icon: Box,
translationKey: "resource",
permission: "resource:read",
},
],
},
The shell filters out items where the user lacks the required permission.
Page-level boundary
Wrap the main page content in <PermissionErrorBoundary> to show a "No Access" fallback for users without the permission:
// apps/web/src/app/(main)/(app)/dashboard/resource/resource-list.tsx
import { PermissionErrorBoundary } from "@/components/permission-error-boundary";
import { Gate } from "@/components/gate";
export function ResourceList() {
return (
<PermissionErrorBoundary permission="resource:read">
<div>
<Gate permission="resource:create">
<Button>Create Resource</Button>
</Gate>
{/* list content */}
</div>
</PermissionErrorBoundary>
);
}
Component-level checks
For ad-hoc imperative checks (e.g., inside hooks or event handlers):
const canEdit = useHasPerm("resource:edit");
if (canEdit) {
// show edit button
}
useHasPerm resolves as perms.includes("*") || perms.includes(permission). An empty permission array grants nothing — it means the identity has no account-scope grants (for example a user whose active context is their personal profile), so every gate closes. Never hand-roll a permissions.length === 0 escape hatch.
Use <Gate permission="..."> for declarative rendering and useHasPerm("...") for boolean checks. See Frontend Permission System for the full API reference.
Next.js proxy routes
The frontend never calls the Rust GraphQL endpoint directly. Follow the established proxy pattern:
Browser → Next.js API route → serverGql() → Rust GraphQL
// apps/web/src/app/api/resource/route.ts
import { NextRequest, NextResponse } from "next/server";
import { serverGql } from "@/lib/server-gql";
import { transformKeys } from "@/lib/transform";
const RESOURCES_QUERY = `
query Resources {
resources {
id accountId name createdAt updatedAt
}
}
`;
export async function GET(request: NextRequest) {
try {
const { data } = await serverGql<{ resources: unknown[] }>(
request,
RESOURCES_QUERY,
);
return NextResponse.json({
data: (data.resources || []).map(transformKeys),
});
} catch (e) {
const status = (e as Error & { status?: number }).status ?? 500;
return NextResponse.json(
{ error: (e as Error).message },
{ status },
);
}
}
Complete Permissions Reference
Every account-scope permission in account::get_all_account_permission_infos(), with its role-editor label, picker category, and the default roles that carry it. 124 entries — Owner holds every one of them.
| Permission | Label | Category | Owner | Admin | Moderator | Viewer |
|---|---|---|---|---|---|---|
events:read | Read Events | Events | ✓ | ✓ | ✓ | ✓ |
events:create | Create Events | Events | ✓ | ✓ | ✓ | |
events:userinfo | Event User Info | Events | ✓ | ✓ | ✓ | ✓ |
overlays:read | Read Overlays | Overlays | ✓ | ✓ | ✓ | ✓ |
overlays:create | Create Overlays | Overlays | ✓ | ✓ | ||
overlays:edit | Edit Overlays | Overlays | ✓ | ✓ | ||
overlays:delete | Delete Overlays | Overlays | ✓ | ✓ | ||
overlays:access-read | Read Overlay Access | Overlays | ✓ | ✓ | ||
overlays:access-grant | Grant Overlay Access | Overlays | ✓ | ✓ | ||
overlays:access-revoke | Revoke Overlay Access | Overlays | ✓ | ✓ | ||
spotify:read | Read Spotify | Spotify | ✓ | ✓ | ✓ | |
spotify:playback | Spotify Playback | Spotify | ✓ | ✓ | ✓ | |
spotify:volume | Spotify Volume | Spotify | ✓ | ✓ | ||
spotify:queue | Spotify Queue | Spotify | ✓ | ✓ | ✓ | |
spotify:playlist | Spotify Playlists | Spotify | ✓ | ✓ | ✓ | |
spotify:device | Spotify Devices | Spotify | ✓ | ✓ | ✓ | |
spotify:worker | Spotify Worker | Spotify | ✓ | ✓ | ||
chat:read | Read Chat | Chat | ✓ | ✓ | ✓ | |
chat:write | Write Chat | Chat | ✓ | ✓ | ✓ | |
chat:userinfo | Chat User Info | Chat | ✓ | ✓ | ✓ | |
chat:delete | Delete Chat Messages | Chat | ✓ | ✓ | ✓ | |
chat:ban | Ban Chat Users | Chat | ✓ | ✓ | ✓ | |
chat:timeout | Timeout Chat Users | Chat | ✓ | ✓ | ✓ | |
chat:notes | Chat User Notes | Chat | ✓ | ✓ | ✓ | |
chat:raid | Cancel Raids | Chat | ✓ | ✓ | ✓ | |
chat:refresh_user | Refresh User Profile | Chat | ✓ | ✓ | ✓ | |
chat:poll | End Polls | Chat | ✓ | ✓ | ✓ | |
chat:prediction | End Predictions | Chat | ✓ | ✓ | ✓ | |
connections:read | Read Connections | Connections | ✓ | ✓ | ✓ | |
connections:create | Create Connections | Connections | ✓ | ✓ | ||
connections:edit | Edit Connections | Connections | ✓ | ✓ | ||
connections:delete | Delete Connections | Connections | ✓ | ✓ | ||
settings:read | Read Settings | Settings | ✓ | ✓ | ||
settings:edit | Edit Settings | Settings | ✓ | ✓ | ||
account:read | Read Account Settings | Account | ✓ | ✓ | ||
account:edit | Edit Account Settings | Account | ✓ | ✓ | ||
account:delete | Delete Account | Account | ✓ | |||
plan:read | View Plan & Billing | Plan | ✓ | |||
plan:edit | Change Plan | Plan | ✓ | |||
members:read | Read Members | Members | ✓ | ✓ | ✓ | |
members:create | Create Invites | Members | ✓ | ✓ | ||
members:edit | Edit Members | Members | ✓ | ✓ | ||
members:delete | Delete Members | Members | ✓ | ✓ | ||
roles:read | Read Roles | Roles | ✓ | ✓ | ✓ | |
roles:edit | Edit Roles | Roles | ✓ | ✓ | ||
roles:delete | Delete Roles | Roles | ✓ | ✓ | ||
login-assignments:read | View Login Assignments | Login Assignments | ✓ | ✓ | ||
login-assignments:create | Assign Login Connections | Login Assignments | ✓ | ✓ | ||
login-assignments:delete | Remove Login Assignments | Login Assignments | ✓ | ✓ | ||
uploads:read | Read Uploads | Uploads | ✓ | ✓ | ✓ | |
uploads:create | Create Uploads | Uploads | ✓ | ✓ | ||
uploads:delete | Delete Uploads | Uploads | ✓ | ✓ | ||
rewards:read | Read Rewards | Rewards | ✓ | ✓ | ✓ | |
rewards:create | Create Rewards | Rewards | ✓ | ✓ | ||
rewards:edit | Edit Rewards | Rewards | ✓ | ✓ | ||
rewards:delete | Delete Rewards | Rewards | ✓ | ✓ | ||
tokens:read | Read Tokens | Tokens | ✓ | ✓ | ||
tokens:create | Create Tokens | Tokens | ✓ | ✓ | ||
tokens:edit | Edit Tokens | Tokens | ✓ | ✓ | ||
tokens:delete | Delete Tokens | Tokens | ✓ | ✓ | ||
automations:read | Read Automations | Automations | ✓ | ✓ | ✓ | |
automations:create | Create Automations | Automations | ✓ | ✓ | ||
automations:edit | Edit Automations | Automations | ✓ | ✓ | ||
automations:delete | Delete Automations | Automations | ✓ | ✓ | ||
automations:execute | Execute Automations | Automations | ✓ | ✓ | ✓ | |
obs:read | View OBS Config | OBS | ✓ | ✓ | ✓ | |
obs:edit | Edit OBS Config | OBS | ✓ | ✓ | ||
obs:delete | Delete OBS Config | OBS | ✓ | ✓ | ||
copyright:read | View Copyright | Copyright | ✓ | ✓ | ||
copyright:edit | Edit Copyright Lists | Copyright | ✓ | ✓ | ||
copyright:delete | Delete Copyright Entries | Copyright | ✓ | ✓ | ||
copyright:vote | Vote on Songs | Copyright | ✓ | ✓ | ✓ | |
copyright:report | Create Copyright Reports | Copyright | ✓ | ✓ | ✓ | |
copyright:recommend | Recommend Songs | Copyright | ✓ | ✓ | ✓ | |
copyright:moderate | Moderate Global Copyright List | Copyright | ✓ | ✓ | ||
bot-modules:read | View Bot Modules | Bot Modules | ✓ | ✓ | ✓ | |
bot-modules:edit | Edit Bot Modules | Bot Modules | ✓ | ✓ | ||
bot-commands:read | View Bot Commands | Bot Commands | ✓ | ✓ | ✓ | ✓ |
bot-commands:create | Create Bot Commands | Bot Commands | ✓ | ✓ | ||
bot-commands:edit | Edit Bot Commands | Bot Commands | ✓ | ✓ | ||
bot-commands:delete | Delete Bot Commands | Bot Commands | ✓ | ✓ | ||
bot-connections:read | View Bot Connections | Bot Connections | ✓ | ✓ | ✓ | |
bot-connections:create | Create Bot Connections | Bot Connections | ✓ | ✓ | ||
bot-connections:delete | Delete Bot Connections | Bot Connections | ✓ | ✓ | ||
se-tokens:read | View SE Tokens | StreamElements | ✓ | ✓ | ||
se-tokens:create | Create SE Tokens | StreamElements | ✓ | ✓ | ||
se-tokens:delete | Delete SE Tokens | StreamElements | ✓ | ✓ | ||
extension-dev:read | View Developer Dashboard | Extension Developer | ✓ | ✓ | ||
extension-dev:create | Create Extensions | Extension Developer | ✓ | ✓ | ||
extension-dev:edit | Edit Extensions | Extension Developer | ✓ | ✓ | ||
extension-dev:delete | Delete Extensions | Extension Developer | ✓ | ✓ | ||
extension-dev:publish | Publish Extensions | Extension Developer | ✓ | ✓ | ||
extension-dev:analytics | View Extension Analytics | Extension Developer | ✓ | ✓ | ||
extension-dev:payouts | Manage Extension Payouts | Extension Developer | ✓ | |||
extension-store:read | Browse Extension Store | Extension Store | ✓ | ✓ | ✓ | ✓ |
extension-store:install | Install Extensions | Extension Store | ✓ | ✓ | ||
extension-store:uninstall | Uninstall Extensions | Extension Store | ✓ | ✓ | ||
extension-store:configure | Configure Extensions | Extension Store | ✓ | ✓ | ||
extension-store:review | Review Extensions | Extension Store | ✓ | ✓ | ✓ | ✓ |
extension-data:read | Read Extension Data | Extension Data | ✓ | ✓ | ✓ | ✓ |
extension-data:edit | Edit Extension Data | Extension Data | ✓ | ✓ | ||
widgets:read | View Widgets | Widgets | ✓ | ✓ | ✓ | ✓ |
widgets:create | Create Widgets | Widgets | ✓ | ✓ | ||
widgets:edit | Edit Widgets | Widgets | ✓ | ✓ | ||
widgets:delete | Delete Widgets | Widgets | ✓ | ✓ | ||
widgets:access-read | Read Widget Access | Widgets | ✓ | ✓ | ||
widgets:access-grant | Grant Widget Access | Widgets | ✓ | ✓ | ||
widgets:access-revoke | Revoke Widget Access | Widgets | ✓ | ✓ | ||
sounds:read | View Sounds | Sounds | ✓ | ✓ | ✓ | ✓ |
sounds:create | Upload Sounds | Sounds | ✓ | ✓ | ||
sounds:edit | Edit Sounds | Sounds | ✓ | ✓ | ||
sounds:delete | Delete Sounds | Sounds | ✓ | ✓ | ||
sounds:play | Play Sounds | Sounds | ✓ | ✓ | ✓ | |
history:read | Read Stream History | Stream History | ✓ | ✓ | ✓ | |
history:share | Share History Reports | Stream History | ✓ | ✓ | ||
history:export | Export History Reports | Stream History | ✓ | ✓ | ✓ | |
history:delete | Delete Stream History | Stream History | ✓ | ✓ | ||
public-stats:read | Read Public Stats Settings | Public Stats | ✓ | ✓ | ✓ | |
public-stats:edit | Edit Public Stats Settings | Public Stats | ✓ | ✓ | ||
audit-log:read | Read Audit Log | Audit Log | ✓ | ✓ |
User-scope (ideas:*) and admin-scope permissions are separate registries — see User Roles and RBAC & Permissions.
Checklist for Adding a New Feature with Permissions
Use this checklist whenever you add a new feature that requires access control:
- 1. Define constants in
crates/lo-auth/src/rbac.rs(pub mod account {})RESOURCE_READ,RESOURCE_CREATE,RESOURCE_EDIT,RESOURCE_DELETE- Add domain-specific actions if needed (e.g.,
RESOURCE_EXECUTE)
- 2. Add to default roles in
rbac.rs- Owner: all new permissions
- Administrator: all except
account:deleteandplan:edit - Moderator: read + relevant domain actions
- Viewer: read only (if appropriate)
- 3. Add GraphQL guards to all resolvers in
apps/api/src/graphql/#[graphql(guard = "lo_graphql::PermissionGuard::new(\"resource:read\")")]
- 4. Add REST guards to all route handlers in
apps/api/src/routes/auth.require_permission(account::RESOURCE_READ).map_err(from_auth_error)?;
- 5. Register the permission once in
crates/lo-auth/src/rbac.rs- Add a
p!(KEY, "Label", "Category")entry toaccount::get_all_account_permission_infos() - All pickers derive from it automatically — no per-picker edits:
- Web role editor: REST
GET /v1/roles/permissions+ GraphQLavailablePermissions(apps/api/src/{routes,graphql}/roles.rs) map the registry - Admin override pickers (ZAF-430): the per-user (
/admin/users/[id]) and per-account (/admin/accounts/[id]) override pickers fetchGET /api/assignable-permissions(→adminAssignablePermissionsGraphQL, mapping the same registry). No hardcoded arrays — do not reintroduce the oldALL_USER_PERMISSIONS/ALL_ACCOUNT_PERMISSIONSshadow lists that once drifted.
- Web role editor: REST
- Add a
- 6. Write DB migration in
apps/api/migrations/- Backfill permissions for all affected default roles
- Use
ON CONFLICT DO NOTHINGfor idempotency
- 7. Add translations in
apps/web/messages/en.jsonandapps/web/messages/de.json- Permission labels under
"permissions":"resource:read": "Read Resource" - Category under
"permissions.categories":"Resource": "Resource"
- Permission labels under
- 8. Add sidebar nav entry in
apps/web/src/app/(main)/(app)/shell.tsx{ href: "/dashboard/resource", permission: "resource:read", ... }
- 9. Wrap page with
<PermissionErrorBoundary permission="resource:read"> - 10. Gate write controls with
<Gate permission="resource:create">etc. - 11. Create Next.js proxy routes in
apps/web/src/app/api/resource/GET→serverGql()→ Rust GraphQL queryPOST/PATCH/DELETE→serverGql()→ Rust GraphQL mutation
- 12. Run lint —
just lint-all(clippy + ESLint, zero warnings)
Key Files Reference
| File | Purpose |
|---|---|
crates/lo-auth/src/rbac.rs | Permission constants, get_all_account_permission_infos() registry, default role definitions |
apps/api/src/graphql/roles.rs | GraphQL availablePermissions picker + role create/update validation |
apps/api/src/routes/roles.rs | REST GET /v1/roles/permissions picker + validation |
apps/api/src/db/roles.rs | create_default_roles() — inserts the four default roles per account |
apps/api/src/graphql/admin.rs | GraphQL adminAssignablePermissions (admin override pickers, ZAF-430) |
apps/admin/src/app/api/assignable-permissions/route.ts | Admin proxy serving the registry to the override pickers |
apps/api/migrations/ | DB migrations for backfilling permissions |
apps/web/messages/en.json | English translations for permissions + role names |
apps/web/messages/de.json | German translations for permissions + role names |
apps/web/src/app/(main)/(app)/shell.tsx | Sidebar nav with permission filtering |
apps/web/src/lib/translate-role.ts | useTranslateRole() hook for default role names |
apps/web/src/contexts/permission-context.tsx | PermissionProvider, usePermissions(), useHasPerm() |
apps/web/src/components/gate.tsx | <Gate> declarative permission component |
apps/web/src/components/permission-error-boundary.tsx | Page-level access denial component |