Skip to content

Middleware ​

All middleware lives in app/middleware/. Nuxt 4 route guards — global ones run first on every navigation, named ones are applied per-page in declaration order.

Global Middleware ​

Runs on every navigation. Declared with the .global.ts suffix.

FileDescription
auth-hydrate.global.tsClient-side only (skips SSR). Sets isExternalSession on useAuth() from the stored auth strategy (readStrategy(), 'external' means external); only when no strategy is stored does it fall back to whether the destination is in EXTERNAL_ROUTE_NAMES. If user is null and either isAuthenticated is true or readStoredToken() finds a token (the auth_token cookie, or localStorage on the client when the cookie is missing), calls fetchUser() so all downstream named guards see the real user rather than null.

Named Middleware ​

Declared per-page via definePageMeta({ middleware: [...] }). Applied in declaration order.

Internal auth and access guards ​

FileGuardsDescription
auth-check.tsAll authenticated internal pagesPrimary auth gate for the internal app. Skips auth pages (login, forgot-password, etc.). On SSR, trusts the auth_token cookie and returns early. It no longer calls fetchUser() itself: auth-hydrate.global.ts loads the user first, and this guard only reads the result. If useAuthGuard().isBackendUnreachable is true (the last /user call failed with no status, status 0 or 5xx) it returns without redirecting, so an API outage does not log the user out. Redirects external users to their workspace upload page. Validates workspace_id format and membership; stores a referring_url_data cookie and redirects unauthenticated users to /.
can-access.tsPages that require module-level accessPost-auth module guard. If useAuth().user is not loaded it returns unresolvedUserResult() (it no longer fetches the user or shows an Authentication failed snackbar). Checks hasDamAccess, hasLeadAccess, and hasGeneralSettingsAccess from getUserModulesAndRoles. Redirects to the user's landing path if the requested module is off-limits. Also tracks referringUrl* cookies for post-login redirect.
can-access-dam-module.tsDAM pages (workspace_id-dam-*)Reads the user from useAuthGuard(); a missing user returns unresolvedUserResult() (navigateTo('/', { replace: true })). Verifies the user has DAM access and a valid role (admin, manager, or viewer). The uploaded-assets page additionally requires canUploadContent (manager/admin only). If no DAM instance exists, admins are sent to add-instance; non-admins receive a 503 fatal error.
can-access-dam-settings.tsDAM portal/branding settings pagesRequires hasDamAccess at minimum. Viewers can reach the settings shell but individual panels are further gated by per-component canManage* checks.
can-access-general-settings.tsGeneral settings pagesOn SSR, returns early when the auth_token cookie is present. Otherwise reads the already-loaded user from useAuthGuard() (no user returns unresolvedUserResult(), a replace-navigation to /), resolves the workspace from accessibleWorkspaces (missing workspace_id is a 400, unknown workspace a 404 fatal error) and requires hasGeneralSettingsAccess from getUserModulesAndRoles (401 fatal error if missing). Then checks a per-route capability with canPerformDamOperation and redirects to the DAM dashboard if it fails: workspace settings (ROUTE_WORKSPACE_SETTINGS) needs controlling_brand_portal_settings, custom fields (ROUTE_WORKSPACE_SETTINGS_CUSTOM_FIELDS) needs managing_custom_fields_settings, and the user list and external user list need adding_users.
can-access-sso-settings.tsSingle Sign-On pages (workspace_id-workspace-settings-sso, -sso-setup)On SSR, returns early when the auth_token cookie is present. On the client: no user returns unresolvedUserResult(); missing workspace_id is a 400 fatal error; a workspace not in accessibleWorkspaces is a 404 fatal error (Workspace not found or access denied.); and useHelpers().canManageSsoSettings(workspace) must be true, which is workspace.is_owner === true, otherwise a 401 fatal error. The plan flag (sso_allowed) is not checked here: the page itself shows an upgrade state when isSsoAllowed(workspace) is false. See Single Sign-On.

Workspace guards ​

FileGuardsDescription
check-workspace.tsPages that depend on a current workspaceVerifies the currentWorkspace cookie is set and has a non-zero id. If the user is authenticated but the cookie is missing or invalid, logs them out and redirects to /. No longer fetches the user; returns early when unauthenticated or when isBackendUnreachable is true.
check-workspace-access.tsAny page with a workspace_id route paramLightweight membership check (client only; no user returns unresolvedUserResult()): confirms the workspace_id param appears in user.accessibleWorkspaces. Returns a 403 fatal error if not found.
check-if-suspended.tsDAM pages inside suspended workspacesReads the user from useAuthGuard() (no user returns unresolvedUserResult()). When the workspace is_suspended flag is set, DAM admins are restricted to the dashboard only (all other routes redirect there). Non-admins receive a 401 fatal error.
check-dam-instance.tsadd-instance setup pagePrevents duplicate instance creation. Redirects to the DAM dashboard if a DAM instance already exists for the workspace. Allows through when no instance exists so the user can complete setup.

External partner flow guards ​

FileGuardsDescription
check-external-access.tsExternal routes (/external/*)For authenticated active external users, calls the check-external-access API with the origin_url. Clears the auth token and redirects to the request-access page if the API returns has_access: false or a 401/402/403/404 status. No-ops for unauthenticated or internal users.
external-guest-redirect.tsRequest-access pageIf the visitor already has a valid token and is an active external user for this workspace, redirects them straight to the upload page. Pass-through otherwise.
external-otp-verify.tsOTP verify pageRedirects already-active external users to upload. For everyone else, checks that the verifyEmail cookie exists and has not passed its 10-minute expiry; if invalid, redirects back to request-access.
external-upload-auth.tsExternal upload and request-submitted pagesRequires an active token and an active user. No token or inactive user → redirects to OTP verify. Active user whose workspace_url_slug does not match the current workspace → redirects to request-access for the correct workspace.

Utility and redirect guards ​

FileGuardsDescription
guest-check.tsLogin / index routeRedirects already-authenticated users away from the login page. Returns early when no token is found (cookie, auth-token-mem state, user state or localStorage) or when isBackendUnreachable is true. It no longer calls fetchUser() to fill an empty workspace list. On the client, if a referring_url_data cookie holds a same-origin fullPath (starts with /, not //, and differs from the current route), it clears the cookie, marks useState('referring-url-consumed') and redirects there once, so a restored session lands on the page originally requested. Otherwise resolves the target workspace from the currentWorkspace cookie or accessibleWorkspaces, then navigates to the appropriate DAM dashboard using role/module-aware landing logic.

useAuthGuard() and the unreachable-backend rule ​

Since PR #110 the named guards never call fetchUser(). useAuthGuard() (app/composables/auth/useAuthGuard.ts) returns user, status, isBackendUnreachable from useAuth() plus unresolvedUserResult(), which is navigateTo('/', { replace: true }). The reason is the /user request storm fixed in 65cdeb93: when the API was down, every guard fetched the user again and every failure redirected. Now only auth-hydrate.global.ts fetches, useAuth backs off between retries, and guards that see isBackendUnreachable let the navigation continue instead of logging the user out.

GuardUser missingBackend unreachable
auth-checkstores referring_url_data, redirects to /returns (no redirect)
guest-checkreturns (no token)returns
check-workspacereturnsreturns
can-access, can-access-dam-module, can-access-general-settings, can-access-sso-settings, check-if-suspended, check-workspace-accessunresolvedUserResult()not checked (user is null, so unresolvedUserResult())

Middleware Execution Order ​

Nuxt runs global middleware first, then named middleware in array order. Example for a DAM settings page:

ts
definePageMeta({
  middleware: ['auth-check', 'check-workspace-access', 'can-access-dam-module', 'can-access-dam-settings'],
})
  1. auth-hydrate.global.ts — hydrates user state from the token if needed
  2. auth-check — verifies authentication, validates workspace membership
  3. check-workspace-access — confirms workspace_id is in accessibleWorkspaces
  4. can-access-dam-module — confirms DAM access and role, checks for DAM instance
  5. can-access-dam-settings — confirms the user can reach settings pages

If any guard returns navigateTo or showError, subsequent guards do not run.

Writing New Middleware ​

ts
// app/middleware/my-guard.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { user } = useAuth()
  if (!user.value) {
    return navigateTo('/', { replace: true })
  }
})

Rules:

  • Never call useRoute() or useRouter() in middleware — use the to/from params
  • Guard only; fetch heavy data in the page itself
  • Return navigateTo(path) to redirect, showError({ statusCode, message, fatal: true }) for hard errors, or nothing to continue
  • Skip SSR API calls with if (import.meta.server) return when the call would fail without a browser origin