Skip to content

Authentication & Authorization

This page covers how authentication and authorization work in the admin panel, including login, session handling, and permissions.

Authentication

The admin panel uses JWT-based authentication with the Tracker API, via the shared tracker-shared/api/auth and tracker-shared/state/auth modules (also used by tracker-frontend-svelte). When a user logs in, the admin panel sends a request to the API's /api/v1/auth/login/json endpoint with the user's credentials. The API returns a JWT access token, which the admin panel stores in localStorage (key "token", see packages/tracker-shared/src/api/client.ts) and includes in subsequent requests.

The admin panel also supports SSO login via Google and Microsoft (getSsoLoginUrl in tracker-shared/api/auth.ts), shown on the login form only when the corresponding client ID is configured at build time (GOOGLE_CLIENT_ID/MICROSOFT_CLIENT_ID).

Login Process

  1. User enters their email and password in the login form (LoginForm.svelte)
  2. The admin panel sends a POST request to /api/v1/auth/login/json with the credentials
  3. If the credentials are valid, the API returns an access token and sets a refresh token as an HttpOnly cookie
  4. signIn() (tracker-shared/state/auth.ts) stores the returned user in the shared authState store, and the login page redirects to /
  5. Subsequent API requests include the access token in the Authorization: Bearer <token> header, added automatically by request() in tracker-shared/api/client.ts

Session Restoration and Token Lifetime

The access token is valid for 4 hours (ACCESS_TOKEN_EXPIRE_MINUTES = 240 in app/core/config.py); the refresh token cookie is valid for 7 days (REFRESH_TOKEN_EXPIRE_DAYS = 7).

On app load, initializeAuth() (tracker-shared/state/auth.ts) restores a session as follows:

  1. It first calls refreshSession(), which does a GET /api/v1/auth/refresh-cookie request. If the HttpOnly refresh cookie is present and valid, the API returns a new access token, which is stored and used to fetch the current user (GET /api/v1/users/me).
  2. If that fails (no cookie, expired, etc.), it falls back to checking whether a still-unexpired access token already exists in localStorage (decoding the JWT's exp claim client-side) and, if so, uses it to fetch the current user.
  3. If neither works, the stored token is cleared and the user is treated as logged out; the (app) layout's $effect then redirects to /login.

There is currently no automatic proactive refresh timer, page-visibility-based refresh, activity tracking, or retry-on-401 while the app stays open — session restoration only happens once, at app load. If the access token expires mid-session, API calls will start failing with 401 until the page is reloaded (which re-runs initializeAuth() and attempts to silently refresh via the cookie) or the user logs in again.

Logout

signOut() calls POST /api/v1/auth/logout (best-effort — the local session is cleared even if this call fails) and clears the stored access token, then the caller redirects to /login.

Authorization

The admin panel respects the authorization rules enforced by the API. Users can only access resources they have permission to access, based on their roles and client_list.

Admin-Only Access to the Admin Panel

Unlike the public frontend, the admin panel itself is gated on the admin role at the UI level: tracker-admin-svelte/src/routes/(app)/+layout.svelte checks $authState.user?.roles.includes("admin") and renders an "Access denied" screen for any other authenticated user, rather than the app shell. Authentication alone is not sufficient — a logged-in manager or user cannot use the admin panel at all.

User Roles

The system supports three roles, stored as a string array on the user record (roles: string[]):

  • admin: Full access to all resources and to the admin panel itself; bypasses client filtering entirely
  • manager: Access scoped to their client_list, like user, but with a few additional permissions enforced by the backend — managers (in addition to admins) can move trackers between production runs and create or edit child production runs (they cannot create/edit parent "batch" production runs)
  • user: Access scoped to their client_list, with no elevated permissions beyond that

Outside of tracker moves and child-production-run management, manager and user are treated identically by the backend — most endpoints (clients, brands, locations, general tracker CRUD) only distinguish admin from everyone else.

Client Filtering

Non-admin users can only access resources associated with clients in their client_list. List endpoints (for example, listing clients, brands, or production runs) filter results down to the caller's client_list (or return everything for admins). However, single-resource-by-ID endpoints use a slightly different rule:

Direct ID Access for Resources

When fetching a single resource by ID (for example GET /api/v1/clients/{id}, GET /api/v1/brands/{id}, or GET /api/v1/production-runs/{id}), the API grants access to that specific resource if:

  1. The user is an admin, or
  2. The user has access to the resource through their client_list

This lets the UI link directly to a specific client, brand, or campaign — for example, following the trackersHref/locationsHref links on a campaign card in Campaign Management, or drilling from a client into its brands — without needing that resource to also appear in a paginated list the user has permission to browse in full.

Permission Checking

The admin panel checks permissions for various actions:

  • Read: Users can only view resources they have permission to access
  • Create: Users can only create resources for clients they have permission to access
  • Update: Users can only update resources they have permission to access (with the manager/child-production-run exception above)
  • Delete: Users can only delete resources they have permission to access

These permissions are enforced both in the frontend UI (by disabling/hiding add, edit, and delete affordances — see EntityColumn's onAdd/onEdit/onDelete props and deleteDisabledReason) and, authoritatively, in the backend API (returning 403 Forbidden for unauthorized requests).