Skip to main content
The operator admin panel — a Vue 3 SPA on its own origin, run against shipwide_api. It reads every account’s data and every support thread, so serve it separately from the app and put it behind a network gate (Cloudflare Access, IP allowlist). Single-app: no product registry or multi-app machinery — the surface is what a single-app operator needs (accounts, sessions, support tickets, uploads, push, audit, ops).

Design & i18n

  • Design system is the Shipwide site’s — same tokens/buttons/badges/<dialog>/header/footer, plus an Admin mark beside the brand so it’s never mistaken for the public site.
  • Multilingual (en + ru to start). Language + theme switch from the header and sign-in screen, both remembered.
  • Add a language by dropping <code>.json into src/i18n/locales/ + one line in src/i18n/locales.ts (rtl: true for RTL). Bundles load on demand.
  • RTL wired — a rtl flag drives dir; CSS uses flexbox + logical flow.

Quick start

Vite inlines VITE_* at build time → switch to prod before building. The API must allow this origin: ADMIN_URL in the API env is added to CORS, so ADMIN_URL and the port here must agree.

Signing in

The sign-in screen offers only what GET /v1/admin/auth/methods reports:
  1. Root credentialsADMIN_LOGIN + ADMIN_PASSWORD (API env). Independent of mail — works when everything else is broken.
  2. Emailed one-time code — staff-only passwordless route at /v1/admin/auth/code.
If TOTP is enrolled, neither route returns a session directly — both hand back a 5-minute challenge; a recovery code works in the same box.

Architecture

core/ never names the product — so you rebrand by editing .env, not code. Anything that would be a hardcoded string lives in src/config/.

Pieces that are easy to get wrong

  • One axios instance, not a factory. Refresh is single-flight — two concurrent 401s don’t each spend a rotating token.
  • Refresh happens before expiry, not only on a 401 — admin sessions are short.
  • Tokens in an encrypted IndexedDB store (non-exportable CryptoKey + canary). Keeps credentials out of plaintext at rest; not an XSS defence.
  • The router gate is not a security boundary — the API authorises every request and re-reads the role each time.
  • TypeScript pinned to 6.x, not 7.x — TS 7 drops the JS tsc that vue-tsc 3 patches, so build throws under TS 7.

Environment

Deployment

nginx serves the SPA, falls through to index.html, caches hashed assets forever / index.html never, sends noindex + DENY. Outside the image: (1) point ADMIN_URL in the API env at this origin or CORS refuses it; (2) put a network gate in front — nothing here needs to be publicly reachable.