Admin console
The admin console is where FINNS staff look up members and adjust points, and where admins manage who on staff has access. It is a backend-for-frontend: the browser only ever talks to the console’s own Worker, and the Worker calls core server to server.
Repository: admin. Test: https://test-admin.finnsbali.com.
Tech stack
Section titled “Tech stack”- Framework
- SvelteKit 2.70Svelte 5 (runes)TypeScript 6Vite 8
- UI
- shadcn-sveltebits-uiTailwind CSS 4Lucide iconsInter
- Forms
- SvelteKit form actionsuse:enhanceValidated in the Worker and again in core.
- Sign-in
- JumpCloud OIDCoauth4webapiPKCE + state + nonce
- Sessions
- Stateless sealed cookieAES-256-GCMHKDF keys8 h absolute; nothing stored server-side.
- Runs on
- Cloudflare Workers@sveltejs/adapter-cloudflare
- Quality
- VitestESLintPrettiersvelte-check
What staff can do
Section titled “What staff can do”| Screen | Who | What |
|---|---|---|
| Customers | Any active staff | Search by name, email, Keycloak subject, or the external id a partner quotes in a report (staff address a customer by subject only). See balance and the last 100 postings, each labelled with who posted it (partner, staff member, or FINNS for check-ins and bookings). |
| Customer → credit / debit | Any active staff | Adjust points with an optional reference. Debits can’t go below zero. To undo, post the opposite adjustment. |
| Staff | Admins | Add staff by email, change role (STAFF, ADMIN) or active status, unlink or delete, see each person’s change history. |
| Dashboard | Any active staff | Placeholder |
Core enforces the rules, not the console: nobody can change their own role or status, at least one active admin must remain, and concurrent edits are refused.
Architecture
Section titled “Architecture”flowchart LR
classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
classDef area fill:#E3F4F3,stroke:#13686C,color:#12191B
classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B
classDef person fill:#E9724C,stroke:#C55A37,color:#ffffff
staff(["Staff browser"]):::person
subgraph worker["Cloudflare Worker · trident-admin"]
direction TB
hooks["hooks.server.ts<br/>guard: session + access + role"]:::area
pages["+page.server.ts<br/>loads and form actions"]:::area
client["core client<br/>Bearer service token<br/>X-Finns-Actor-Sub"]:::area
hooks --> pages --> client
end
jc["JumpCloud<br/>OIDC"]:::ext
core["Core<br/>/api/v1/admin"]:::ours
staff -- "HTML, form posts<br/>sealed session cookie" --> hooks
hooks -- "sign-in redirect" --> jc
client -- "server to server" --> core
Staff sign-in
Section titled “Staff sign-in”Two gates decide access: JumpCloud group membership decides who may sign in at all; core’s staff table decides whether they get in and with which role.
sequenceDiagram
autonumber
actor S as Staff
box rgb(227,244,243) FINNS platform
participant W as Admin Worker
participant CORE as Core
end
box rgb(246,241,233) Third party
participant JC as JumpCloud
end
S->>W: GET /auth/login
Note over W: nowrap:Origin must be APP_ORIGIN.<br/>Seal {state, nonce, PKCE verifier, returnTo}<br/>in a 10-min transaction cookie
W-->>S: 303 to JumpCloud
S->>JC: Sign in
JC-->>S: 303 /auth/callback?code&state
S->>W: GET /auth/callback
W->>JC: Exchange code (client secret + verifier)
W->>JC: userinfo
Note over W: nowrap:memberOf must include<br/>an allowed group
W->>CORE: POST /api/v1/admin/staff-access/sign-ins {sub, email}
Note over CORE: nowrap:Link the JumpCloud account<br/>on first sign-in, record it
CORE-->>W: active (role) / disabled / none
W-->>S: Set sealed session cookie, 303 to returnTo
On every protected request the Worker asks core again (GET /staff-access/{sub}), caching only “active” answers for 30 seconds. So disabling someone or changing their role takes effect within 30 seconds; removing them from the JumpCloud group takes effect at their next sign-in (at most 8 hours). If core can’t answer, the console returns 503: it never fails open.
Manual points adjustment
Section titled “Manual points adjustment”sequenceDiagram
autonumber
actor S as Staff
box rgb(227,244,243) FINNS platform
participant W as Admin Worker
participant CORE as Core
end
S->>W: Open customer page
W->>CORE: GET /customers/{sub}
CORE-->>W: Customer, balance, last 100 postings
W-->>S: Page with hidden key = random UUID
S->>W: POST ?/credit {points, reference, key}
W->>CORE: POST /customers/{sub}/credits<br/>Idempotency-Key: key · X-Finns-Actor-Sub
Note over CORE: nowrap:Posting with client STAFF,<br/>reason staff_adjustment, staff id
CORE-->>W: 201 transaction
W-->>S: Reload with a new key
Note over S,CORE: nowrap:A resubmitted form reuses the key, so it replays instead of posting twice
Security notes
Section titled “Security notes”- Protected by default: every route outside
(public)/needs a session; the minimum role comes from the route folder, so a new admin page can’t be left open by mistake. The menu filter is cosmetic. - Configuration is validated on every request; a missing or bad setting returns
500, never an open console. - No identity-provider tokens are kept after sign-in; the session cookie holds only
{sub, name, email, groups}and is under 3.8 KB. - Sign-out is POST-only and clears only the console’s session, so the user isn’t silently signed straight back in by JumpCloud.
- Previews can’t sign in: sign-in only works on the configured origin.
Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).

