PrimeBrickPrimeBrick
  • Docs
  • Contact
  • MIT License
  • Documentation
  • MCP Server
  • API Catalog
  • Services
  • Libraries
PrimeBrickPrimeBrick

© 2026 PrimeBrick. MIT License. v3.11.2

github
Backend
Frontend
    Developer Guide
      OverviewGetting startedUI stackApp shell & sidebarAuthentication & sessionsSystem settingsUI componentsUI patternsIcon ConventionsInput AnatomyField Hints & TooltipsButton Style StandardsEntity metadatai18n — Translations on the FrontendThe AI Tool Loop (caller-driven)Error HandlingComposables (use* hooks)
      Components
      API Reference
Microservices
powered by Zudoku
Developer Guide

Authentication & sessions

Login

src/routes/login/+page.svelte renders <LoginForm> from src/lib/components/auth/LoginForm.svelte. The form uses sveltekit-superforms with a Zod schema (username, password both required) and POSTs to /api/v1/auth/login.

On success it stores the user in userProfileStore and invokes the onsuccess callback, which the login page resolves through resolvePostLoginTarget() — sanitized ?redirect_path= query param first, then the sessionStorage-saved redirect, then /. On failure it maps RFC7807 error responses to i18n-localized messages, with special handling for rate-limited attempts (showing minutes remaining).

Session boot (auto-login)

Before showing the form, the login page runs a client-side boot check — no /auth/me round-trip:

  1. Valid local session — sessionStorage['user'] exists and its expires_at is not within the 30 s expiry buffer → navigate straight to the post-login target while a LoadingWatermark shows "Auto Logging" (cookie icon, flip animation).
  2. Expired or missing local session → a single triggerRefresh() attempt ("Refreshing Token" state, rotate-ccw-key icon). The HttpOnly refresh cookie is the authoritative channel, so this also recovers sessions in fresh tabs where sessionStorage is empty.
  3. Refresh fails → stale local state is cleared via userProfileStore.clear() and the normal login form renders.

The shared helpers live in src/lib/auth/session-check.ts (hasLocalSession, isTokenExpired, refreshAccessToken, triggerRefresh) and are also used by apiFetch's 401 retry path — triggerRefresh is single-flight, so concurrent callers share one refresh request.

Identity vs session — one certified source

sessionStorage['user'] / userProfileStore mixes two domains that are written by different paths:

WriterAllowed fields
GET /api/v1/auth/me → userProfileStore.set(profile)Identity — uuid, display_name, email, avatar_*, is_admin, is_verified, idp_*, permissions, audit fields. THE certified source (DB user_profiles).
login / refresh responses (IDP JWT claims) → userProfileStore.setSession(data.user)Session only — username, idp_code, expires_at, roles, organization. setSession whitelists these keys: claim-level display_name/email are dropped — never deduced identity.

setSession also carries an account-switch guard: a payload idp_code different from the stored one drops the current identity fields, which /auth/me then repopulates reactively — no stale identity of another account can linger. Never write sessionStorage['user'] directly or feed claims to set().

Post-login redirect

src/lib/auth/redirect-cache.ts exposes the redirect helpers:

HelperPurpose
saveRedirectUrl(url)Persists the current app path in sessionStorage (API routes skipped)
getAndClearRedirectUrl()Reads and clears the saved redirect
sanitizeRedirectPath(raw)Accepts only same-origin /... paths — rejects //host, /api/…, backslashes, control chars
resolvePostLoginTarget(qsParam, lastRoute)redirect_path QS → saved redirect → last meaningful route → /
goToLoginPage()Hard-navigates to /login preserving the current path both in sessionStorage and as a sanitized ?redirect_path= (survives reloads and new tabs)

SessionExpiredDialog's "Go to login" button uses goToLoginPage(), so the original destination is restored after re-authentication even from the full login page.

Last-route restore

pb:shell:lastRoute (localStorage) tracks the last meaningful page via afterNavigate — /, /login* and /api/* are never saved, so the post-login resolution's third fallback is deterministic rather than degenerating to /.

Module switching & route-aware translations

Selecting a module in the sidebar switcher calls shellNav.selectModule(id) and navigates to the module home — the first nav entry of the current root slice (home[root] from the /api/v1/modules payload; nav[0].href is the single source of truth). /system/settings itself redirects to /system/settings/profile via a +page.ts universal load() — redirect() inside a component <script> only works on SSR; on client-side navigation it surfaces as an unhandled "Redirect" error and a blank page.

The route change is what drives lazy translation loading: useModuleTranslations() (mounted in the (app) layout) resolves the module from page.url.pathname and fetches its dictionary — so module dictionaries load on navigation, not on selection.

Logout

POST /api/v1/auth/logout (PUBLIC) clears both HttpOnly auth cookies — access_token (path /) and refresh_token (path /api/v1/auth/refresh). JavaScript cannot clear HttpOnly cookies, so the server round-trip is required.

AppSidebar's handleLogout calls the endpoint (best-effort), then clears local state — userProfileStore.clear(), getAndClearRedirectUrl(), shellNav.clearLastRoute() — and navigates to /login without redirect_path. After a real logout the login boot's refresh attempt fails and the form renders: the session is officially over.

Session expiration

When an API call returns 401, the request is enqueued in the session-expired store and the SessionExpiredDialog opens so the user can re-authenticate without losing context.

src/lib/auth/session-expired-store.svelte.ts exposes:

MemberPurpose
enqueue(input, init)Enqueues a failed request and opens the dialog; returns a Promise<Response> that resolves after re-login
drainPending()Returns and clears the queued requests (called after successful re-login)
setFailed()Marks the last login attempt as failed (shows the error alert + "Go to login" button)
close()Closes the dialog and resets state

State is exposed read-only via state (isOpen, pendingRequests, hasFailedAttempt). After a successful re-login in the dialog, handleLoginSuccess() calls close() and drainPending(), and the enqueued requests are retried with a _sessionRetry flag to avoid re-triggering the dialog.

Password policy

The backend exposes the active password policy at GET /api/v1/system/password-policy. The usePasswordPolicy() composable fetches it and exposes both the rules and a derived validation regex.

Policies

PolicyRules enforced
alpha_numericLength 8–64, alphanumeric only
letter_and_numberLength, ≥1 letter, ≥1 number
letter_number_specialLength, letter, number, special char
mixed_case_specialLength, lowercase, uppercase, number, special char

Allowed special characters: *-_.#@!|?^:

The regex patterns are kept in sync with the backend exactly:

Code
ALPHA_NUMERIC: /^[A-Za-z0-9]{8,64}$/ LETTER_AND_NUMBER: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9]{8,64}$/ LETTER_NUMBER_SPECIAL:/^(?=.*[A-Za-z])(?=.*\d)(?=.*[*\-_.#@!|?^:])[A-Za-z0-9*\-_.#@!|?^:]{8,64}$/ MIXED_CASE_SPECIAL: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[*\-_.#@!|?^:])[A-Za-z0-9*\-_.#@!|?^:]{8,64}$/

Composable API

Code
const policy = usePasswordPolicy(); policy.state.policy // PasswordPolicy enum policy.state.checklistRules // PasswordChecklistRule[] policy.state.specialChars // string policy.state.loading // boolean policy.state.loaded // boolean policy.regex // RegExp matching the active policy policy.load() // fetch from backend

Password checklist

PasswordChecklist.svelte renders a live checklist of the active rules (length, letter, lowercase, uppercase, number, special) with check/circle icons, updating as the user types. It takes password, rules: PasswordChecklistRule[], and optional specialChars.

Change password dialog

ChangePasswordDialog.svelte (in the entity-list-table dialogs) loads the policy on open, validates the new password against policy.regex, requires confirmation match, and POSTs to /api/v1/entities/user_profile/{uuid}/change-password. It embeds <PasswordChecklist> for inline feedback.

Password UI components

The src/lib/components/ui/password/ family wraps password input with strength checking via @zxcvbn-ts/core:

  • Password.Root — context provider (usePassword({ hidden, minScore }))
  • Password.Input — the input itself (usePasswordInput({ value, ref }))
  • Password.ToggleVisibility — show/hide toggle
  • Password.Copy — copy-to-clipboard
  • Password.Strength — zxcvbn strength meter
  • PasswordInputField — convenience wrapper combining Root + Input + Toggle

zxcvbn is lazy-loaded and strength checking is debounced with request cancellation. When the score is below minScore, the input's custom validity is set.

Active roles

useActiveRoles() fetches roles from /api/v1/system/roles/active and exposes state.roles (ActiveRole[] with idp_role, label_key, permissions, is_admin) plus a derived roleNames getter for use in forms.

MCP OAuth consent

src/lib/mcp-oauth.ts provides OAuth client helpers for the MCP consent screen at /mcp/consent:

FunctionPurpose
parseConsentParams(searchParams)Parses OAuth params from the URL query string
parseScopes(scopeString)Splits the scope string into { name, description } entries
buildApproveUrl(params, beBaseUrl)Builds the backend authorize URL with consent_approved=true
buildDenyUrl(params)Builds the deny redirect URL per RFC 6749 §4.1.2.1

The consent page checks /api/v1/auth/me; if the user is not logged in it shows <LoginForm> and reloads on success. Otherwise it shows the client name and requested scopes, then redirects to the approve or deny URL.

Form guards

Two composables consolidate logic shared across the settings form pages:

useFormGuard

Derives hasChanges and canSave from a SuperForm instance. Because Superform's auto-subscription does not work inside .svelte.ts files, it takes getter functions for $tainted and $errors plus the form's isTainted function:

Code
const guard = useFormGuard(() => $tainted, () => $errors, (p) => isTainted(p)); $derived(guard.hasChanges); // true when form is tainted $derived(guard.canSave); // hasChanges && no errors

useUnsavedChangesGuard

Registers navigation guards for unsaved changes:

Code
const guard = useUnsavedChangesGuard(() => formGuard.hasChanges, 'settings.unsavedChangesConfirm');
  • handleBeforeUnload — for <svelte:window onbeforeunload>, prevents browser close/refresh
  • handleCancel — for the footer Cancel button; confirms then closes the window or navigates back
  • Internally registers a SvelteKit™ beforeNavigate guard that prompts on in-app navigation

Next steps

  • System settings
  • Entity list table
Last modified on October 28, 2026
App shell & sidebarSystem settings
On this page
  • Login
    • Session boot (auto-login)
    • Identity vs session — one certified source
    • Post-login redirect
    • Last-route restore
  • Module switching & route-aware translations
  • Logout
  • Session expiration
  • Password policy
    • Policies
    • Composable API
    • Password checklist
    • Change password dialog
  • Password UI components
  • Active roles
  • MCP OAuth consent
  • Form guards
    • useFormGuard
    • useUnsavedChangesGuard
  • Next steps
TypeScript
TypeScript
TypeScript
TypeScript