Appearance
Permissions & Role-Based Access ​
Overview ​
- Four DAM roles —
admin,manager,viewer, andbrand-portal-user— map workspace module membership to granular feature access across every DAM surface. Note: there is noowneroreditorrole; the old docs were inaccurate. - DAM_CAPABILITIES matrix — a static record defined in
useHelpers.tswith 14 capability groups and explicit role allowlists. Every UI gate derives from this single source of truth. canPerformDamOperation— the core runtime checker that reads the current user's resolved role for a workspace and matches it against the capability allowlist.- ~50 named
can*helpers wrapcanPerformDamOperationand are exported fromuseHelpers. Components call them directly for UI-level gating (show/hide buttons, tabs, and menu items). - Route middleware enforces access at the navigation layer before any component renders. The Laravel backend enforces the same rules on every API call independently.
- External user access is handled via a separate OTP-based auth flow with its own middleware chain, isolated from the main RBAC system.
Architecture ​
Access control flows from two directions. Route middleware gates navigation: can-access-dam-module.ts verifies the user has a valid DAM role for the requested workspace and redirects or returns a fatal error before any component mounts. can-access-dam-settings.ts and can-access-general-settings.ts narrow access further for settings-only sections. At the component level, the useHelpers composable exposes the full set of can* helper functions. All helpers resolve through the same DAM_CAPABILITIES matrix, so a role change takes effect on both navigation and UI simultaneously without any cache invalidation.
Role resolution is performed by getUserModulesAndRoles in useHelpers.ts. It reads a Workspace object from user.accessibleWorkspaces, finds the matching WorkspaceModule entry, and maps the role_slug to a structured UserData object containing booleans (isAdmin, isManager, isViewer) and flags (hasDamAccess, hasGeneralSettingsAccess).
File Structure ​
TypeScript Files ​
app/composables/core/common/useHelpers.ts— Primary permissions composable; containsDAM_CAPABILITIES,getUserModulesAndRoles,canPerformDamOperation, and all namedcan*helpersapp/composables/auth/useAuth.ts— Auth state management; provides theuserobject consumed by all permission helpersapp/types/auth.ts—UserData,User,LoginCredentials, andLoginResponseinterfacesapp/types/layout.ts—WorkspaceandWorkspaceModuleinterfaces used in role resolutionapp/constants/routes.ts— Route name constants referenced by middleware guards
Middleware Files ​
app/middleware/auth-hydrate.global.ts— Runs before all named middleware; hydrates user state from token on every hard refresh, setsisExternalSessionflagapp/middleware/auth-check.ts— Validates session, enforces workspace membership, redirects external users to their upload pageapp/middleware/can-access.ts— Legacy general-purpose access guard; validates workspace membership and checks module access for DAM, lead, and settings pathsapp/middleware/can-access-dam-module.ts— Guards all DAM routes; resolves the user's DAM role, checks for a configured instance, and enforces per-route operation gatesapp/middleware/can-access-dam-settings.ts— Guards portal/branding settings pages; blocks users without DAM accessapp/middleware/can-access-general-settings.ts— Guards workspace settings routes; checkshasGeneralSettingsAccess(admin-only)app/middleware/check-workspace-access.ts— Validates workspace membership fromuser.accessibleWorkspacesapp/middleware/check-if-suspended.ts— Redirects non-admins away from all DAM pages when a workspace is suspended; admins are confined to the dashboardapp/middleware/check-dam-instance.ts— Guards the add-instance setup page; redirects to the dashboard if an instance already existsapp/middleware/check-external-access.ts— Validates active external user access for/externalroutes via thecheck-external-accessAPIapp/middleware/external-upload-auth.ts— Guards the external upload page; redirects unauthenticated guests to the OTP verify pageapp/middleware/external-otp-verify.ts— Guards the OTP page; validates a 10-minuteverifyEmailcookie before allowing OTP submissionapp/middleware/external-guest-redirect.ts— Redirects already-active external users from request-access directly to the upload pageapp/middleware/guest-check.ts— Redirects authenticated internal users away from the login page to their workspace
Role Resolution ​
getUserModulesAndRoles ​
Defined in app/composables/core/common/useHelpers.ts. Takes a Workspace object from user.accessibleWorkspaces and returns a structured UserData object with booleans and the resolved role slug.
javascript
// Returned UserData shape
{
hasDamAccess: boolean, // workspace has a DAM module with a valid role
hasGeneralSettingsAccess: boolean, // admin-only workspace settings access
redirectPathName: string | null, // first accessible route name for post-login redirect
dam: {
role: string | null, // 'admin' | 'manager' | 'viewer' | 'brand-portal-user'
isAdmin: boolean,
isViewer: boolean,
isManager: boolean,
},
lead: {
role: string | null,
isSuperAdmin: boolean,
isRSM: boolean,
isDealerGroupAdmin: boolean,
isDealerAdmin: boolean,
isSalesPerson: boolean,
}
}DAM Roles ​
| Role slug | Access level |
|---|---|
admin | Full DAM access: account settings, user management, empty trash, all content operations |
manager | Content operations (upload, edit, delete, share, portal management); no account settings |
viewer | Read-only: browse, search, share/download own content, view versions and insights |
brand-portal-user | Portal access only; cannot enter the main DAM |
DAM Capabilities Matrix ​
Defined in app/composables/core/common/useHelpers.ts (lines 527–614). Each key maps to the roles allowed to perform that operation.
javascript
const DAM_CAPABILITIES = {
// Basic access
brand_portal_access: ['brand-portal-user', 'viewer', 'manager', 'admin'],
// Content upload
content_upload: ['manager', 'admin'],
uncategorized_assets_bar: ['manager', 'admin'],
uncategorized_assets_folder: ['manager', 'admin'],
// Folder management
creating_folders: ['manager', 'admin'],
editing_folder_name_description: ['manager', 'admin'],
folder_permission: ['manager', 'admin'],
moving_content: ['manager', 'admin'],
deleting_content: ['manager', 'admin'],
sharing_downloading: ['viewer', 'manager', 'admin'],
// Collage management
creating_collages: ['manager', 'admin'],
removing_collage: ['manager', 'admin'],
editing_collage_name_description: ['manager', 'admin'],
collage_permission: ['manager', 'admin'],
deleting_collage: ['manager', 'admin'],
removing_asset_from_collage: ['manager', 'admin'],
// Asset details
editing_asset_name: ['manager', 'admin'],
duplicating_content: ['manager', 'admin'],
asset_permission: ['manager', 'admin'],
editing_asset_description: ['manager', 'admin'],
viewing_insights: ['viewer', 'manager', 'admin'],
// Tag management
adding_tags: ['manager', 'admin'],
removing_tags: ['manager', 'admin'],
viewing_tags: ['manager', 'admin'],
// Custom fields
managing_custom_fields_settings: ['admin'],
creating_new_custom_fields: ['admin'],
managing_custom_fields: ['manager', 'admin'],
removing_custom_fields: ['manager', 'admin'],
// Asset versioning
adding_new_versions: ['manager', 'admin'],
viewing_versions: ['viewer', 'manager', 'admin'],
restoring_versions: ['manager', 'admin'],
deleting_versions: ['manager', 'admin'],
downloading_versions: ['viewer', 'manager', 'admin'],
// Trash management
restoring_trash: ['manager', 'admin'],
managing_trash: ['admin'], // empty trash — admin only
// Search
search_bar: ['viewer', 'manager', 'admin'],
inner_search: ['viewer', 'manager', 'admin'],
// Help & support — all roles
help_support: ['brand-portal-user', 'viewer', 'manager', 'admin'],
// Account settings — admin only
account_settings: ['admin'],
account_admins: ['admin'],
adding_users: ['admin'],
manage_organization_notifications: ['admin'],
viewing_announcements: ['admin'],
guest_upload_manager: ['admin'],
// Brand portal settings
creating_brand_portal: ['manager', 'admin'],
controlling_brand_portal_settings: ['admin'],
managing_brand_portal_users: ['manager', 'admin'],
managing_brand_portal_banners: ['manager', 'admin'],
managing_brand_portal_tiles: ['manager', 'admin'],
// Share/embed management
embedded_delete: ['viewer', 'manager', 'admin'],
revoke_share: ['viewer', 'manager', 'admin'],
advanced_settings: ['viewer', 'manager', 'admin'],
manage_sharing: ['manager', 'admin'],
}Permission Helpers ​
canPerformDamOperation ​
The core permission checker. Takes a workspace object and an operation key, resolves the user's role via getUserModulesAndRoles, and returns whether that role appears in the capability's allowlist. Returns false on any invalid or missing input.
javascript
// Signature
canPerformDamOperation(workspace: Workspace | undefined, operation: string): boolean
// Direct usage
const { canPerformDamOperation } = useHelpers()
const canUpload = canPerformDamOperation(currentWorkspace, 'content_upload')Named Helpers ​
All of the following are returned from useHelpers() and are auto-imported in components.
javascript
// Content
canUploadContent(workspace) // content_upload
canDeleteContent(workspace) // deleting_content
canShareDownload(workspace) // sharing_downloading
canMoveContent(workspace) // moving_content
canDuplicateContent(workspace) // duplicating_content
// Folders
canCreateFolders(workspace)
canEditFolderNameDescription(workspace)
canManageFolderPermission(workspace)
// Collages
canCreateCollages(workspace)
canRemoveCollage(workspace)
canEditCollageNameDescription(workspace)
canManageCollagePermission(workspace)
canDeleteCollage(workspace)
canRemoveAssetFromCollage(workspace)
// Assets
canEditAssetName(workspace)
canEditAssetDescription(workspace)
canManageAssetPermission(workspace)
canViewInsights(workspace)
// Tags
canAddTags(workspace)
canRemoveTags(workspace)
canViewTags(workspace)
// Custom fields
canManageCustomFields(workspace)
canCreateNewCustomFields(workspace) // admin only
canManageCustomFieldsSettings(workspace) // admin only
canRemoveCustomFields(workspace)
// Versions
canAddNewVersions(workspace)
canViewVersions(workspace)
canRestoreVersions(workspace)
canDeleteVersions(workspace)
canDownloadVersions(workspace)
// Trash
canRestoreTrash(workspace)
canManageTrash(workspace) // admin only (empty trash)
// Uncategorized assets bar/folder — manager, admin only
canViewUncategorizedAssetsBar(workspace)
canViewUncategorizedAssetsFolder(workspace)
// Search
canUseSearchBar(workspace)
canUseInnerSearch(workspace)
canUseAdvancedSearch(workspace) // RBAC + subscription_features.advance_search_functionality.enable
// Help & support — all roles
canAccessHelpSupport(workspace)
// Share/embed
canDeleteEmbed(workspace)
canRevokeShare(workspace)
canAccessAdvancedSettings(workspace) // viewer, manager, admin (advanced share settings panel)
canManageSharing(workspace) // manager, admin (any share)
canManageShare(workspace, share, currentUserName) // viewer = own shares only
canSelectShare(workspace, share, currentUserName)
// Account settings — admin only
canAddUsers(workspace)
canAccessAccountSettings(workspace)
canManageAccountAdmins(workspace)
canManageGuestUpload(workspace)
canManageNotifications(workspace)
canViewAnnouncements(workspace)
// Brand portal
canCreateBrandPortal(workspace)
canManageBrandPortalSettings(workspace) // admin only
canManageBrandPortalUsers(workspace)
canManageBrandPortalBanners(workspace)
canManageBrandPortalTiles(workspace)Component Integration ​
Permissions are cross-cutting — there is no dedicated "permissions page." The can* helpers from useHelpers() are applied in two patterns across the codebase.
Pattern 1: Resolved inside a facade composable ​
Facade composables (e.g. usePortalsList, useCollageDetails) call useHelpers() internally, resolve the current workspace from the auth store, and return computed booleans that pages consume directly. Pages never see the workspace object.
vue
<!-- portals/index.vue — canCreate comes pre-resolved from usePortalsList -->
<v-btn v-if="canCreate" @click="openAddPortal">Add Portal</v-btn>
<script setup lang="ts">
const { canCreate, canManageSettings, canAccessPortalDetail } = usePortalsList()
// canCreate = computed(() => canCreateBrandPortal(currentWorkspace.value))
// resolved inside usePortalsList via useHelpers() + useAuth()
</script>Pattern 2: Resolved directly in the page ​
Pages that don't use a facade (e.g. settings pages) call useHelpers() directly and resolve the workspace from useAuth() + route.params.workspace_id.
vue
<!-- workspace-settings/custom-fields/index.vue -->
<v-btn v-if="canManageCustomFields" @click="addField">Add Custom Field</v-btn>
<script setup lang="ts">
const { user: authUser } = useAuth()
const route = useRoute()
const { canManageCustomFieldsSettings } = useHelpers()
const canManageCustomFields = computed<boolean>(() => {
const workspaceId = route.params.workspace_id as string
const workspace = (authUser.value?.accessibleWorkspaces as { id: number | string }[] | undefined)?.find(
(w) => parseInt(String(w.id)) === parseInt(workspaceId),
)
return canManageCustomFieldsSettings(workspace as Parameters<typeof canManageCustomFieldsSettings>[0])
})
</script>In both patterns, the workspace object is never passed as a prop from a parent component. Every can* call resolves the workspace locally using the auth store and the route param. The DAM_CAPABILITIES matrix in useHelpers.ts is the single source of truth — changing a role's allowlist takes effect everywhere simultaneously.
Workflows ​
Permission Check Flow ​
1. User navigates to a DAM route
Middleware: auth-hydrate.global.ts (runs globally before named guards)
→ Hydrates user state from auth_token cookie on hard refresh
→ Sets isExternalSession = true for EXTERNAL_ROUTE_NAMES
↓
2. Named middleware: can-access-dam-module.ts
→ Fetches user if not already in state
→ Finds workspace in user.accessibleWorkspaces → 404 if missing
→ Calls getUserModulesAndRoles(workspace)
→ Checks modUser.hasDamAccess → 401 if false
→ Checks isAdmin || isViewer || isManager → 401 if none match
→ Route-specific gate: ROUTE_DAM_UPLOADED requires canUploadContent
→ Loads DAM instance via damStore.getInstances
→ If no instance and admin → redirect to /:workspace_id/dam/add-instance
→ If no instance and not admin → 503 error
↓
3. Component mounts
→ Component calls useHelpers()
→ Calls can*() helpers with current workspace object
→ Renders or hides controls based on returned booleans
↓
4. API call executes
→ Laravel backend validates the role server-side independently
→ Returns 401 if unauthorized regardless of frontend gateMember Invitation Flow ​
1. Admin navigates to Members page
Route: /:workspace_id/workspace-settings/user/list
Middleware: can-access-general-settings.ts
→ Checks hasGeneralSettingsAccess → admin only
↓
2. Admin clicks Invite
→ Enter email address + select role
→ useMembersApi.checkEmail({ email })
→ Validates email is not already a member
↓
3. Submit invitation
→ useMembersApi.inviteUser(formData)
→ POST /user/invite-user
→ Backend creates pending record and sends tokenized email
↓
4. Recipient clicks invite link
→ Routed to accept-invitation page
→ Creates account or logs in
→ Member record set to active with the assigned role slug
↓
5. New member accesses workspace
→ auth-hydrate.global.ts runs fetchUser
→ getUserModulesAndRoles resolves their role
→ can*() helpers apply with the new role immediatelyExternal User Access Flow ​
1. Guest accesses /:workspace_id/external/*
Middleware: auth-hydrate.global.ts
→ Sets isExternalSession = true
↓
2. check-external-access.ts (if active token + is_external flag)
→ GET /check-external-access?origin_url=...
→ If has_access === false → clear auth + redirect to request-access
↓
3a. No active token → external-upload-auth.ts
→ Redirect to /:workspace_id/external/verify (OTP page)
↓
3b. On OTP page: external-otp-verify.ts
→ Already active external user → redirect to upload
→ verifyEmail cookie invalid/expired → redirect to request-access
→ Valid cookie → allow OTP entry
↓
4. OTP verified → user becomes active external user
→ Auth token set in cookie and tokenState
→ Redirect to /:workspace_id/external/upload
↓
5. external-guest-redirect.ts on future visits
→ Already-active external users skip request-access
→ Redirect directly to uploadRelated Documentation ​
- Auth Methods — Login flows and session management
- Workspace — Member management and role assignment
- External Access — Guest upload OTP flow details
- Branding — Brand portal settings access
- Notifications — Notification settings access control