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
- User enters their email and password in the login form (
LoginForm.svelte) - The admin panel sends a POST request to
/api/v1/auth/login/jsonwith the credentials - If the credentials are valid, the API returns an access token and sets a refresh token as an HttpOnly cookie
signIn()(tracker-shared/state/auth.ts) stores the returned user in the sharedauthStatestore, and the login page redirects to/- Subsequent API requests include the access token in the
Authorization: Bearer <token>header, added automatically byrequest()intracker-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:
- It first calls
refreshSession(), which does aGET /api/v1/auth/refresh-cookierequest. 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). - 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'sexpclaim client-side) and, if so, uses it to fetch the current user. - If neither works, the stored token is cleared and the user is treated as logged out; the
(app)layout's$effectthen 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, likeuser, 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:
- The user is an admin, or
- 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).