Files
dezky/apps/operator/nuxt.config.ts
T
Ronni Baslund 901cc69ba3
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Successful in 20s
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_platform_api (push) Has been skipped
ci / build_operator (push) Successful in 31s
ci / build_portal (push) Successful in 39s
ci / deploy (push) Successful in 41s
fix(auth): silent session renewal + 401 auto-recovery
Idle sessions died and left a broken page: when the access token expired,
nuxt-oidc-auth's automatic refresh had no refresh token to use — neither
Authentik provider carried the offline_access scope mapping (and the
operator never requested the scope), so the module cleared the session
and every /api call 401'd until a manual F5 happened to re-auth through
Authentik's still-alive SSO session.

Fix 1: offline_access end to end — scope mapping attached to both live
providers (and blueprints, prod + dev), operator now requests the scope.
Sessions renew server-side for up to 30 days of activity (Redis store +
pinned token key from earlier make the refresh tokens durable).

Fix 2: client plugin in both apps — a 401 from /api sends the browser
through /auth/oidc/login instead of leaving dead buttons; invisible when
Authentik's session is alive, a clean sign-in screen when it isn't.
Loop-guarded. Full sign-out behavior unchanged.
2026-06-11 09:21:15 +02:00

120 lines
4.5 KiB
TypeScript

// Nuxt 3 configuration for the Dezky operator portal.
// Separate app from apps/portal — different OAuth client, different cookies,
// different domain, stricter authorization. See docs/OPERATOR-PLAN.md.
// Base URLs are environment-driven so one build runs in dev (.local) and
// production (.eu) — same approach as apps/portal. Set at BUILD (CI) and
// RUNTIME (fleet/apps/operator.yaml + operator-secrets); the .local defaults
// keep local dev working with no env.
const AUTH_URL = (process.env.NUXT_PUBLIC_AUTH_URL || 'https://auth.dezky.local').replace(/\/$/, '')
const OPERATOR_URL = (process.env.NUXT_PUBLIC_OPERATOR_URL || 'https://operator.dezky.local').replace(/\/$/, '')
const OPERATOR_OIDC_APP_SLUG = process.env.OPERATOR_OIDC_APP_SLUG || 'dezky-operator'
export default defineNuxtConfig({
compatibilityDate: '2026-01-01',
devtools: { enabled: true },
modules: ['nuxt-oidc-auth'],
runtimeConfig: {
public: {
// Overridable at runtime via NUXT_PUBLIC_AUTH_URL / NUXT_PUBLIC_OPERATOR_URL
// (both set in production). Used for Authentik links and host labels.
authUrl: AUTH_URL,
operatorUrl: OPERATOR_URL,
},
},
css: ['~/assets/styles/tokens.css', '~/assets/styles/base.css'],
// Auto-import from the shared packages/ui workspace in addition to the
// app's own components/. /shared-packages is bind-mounted in
// docker-compose.yml — outside containers the same files live at
// <repo>/packages/ui/components/. The local dir keeps the default
// directory-based prefix; the shared dir uses no prefix so
// CountrySelect.vue is just <CountrySelect>.
components: [
'~/components',
{ path: '/shared-packages/ui/components', pathPrefix: false },
],
app: {
head: {
htmlAttrs: { 'data-theme': 'dark' },
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap',
},
],
},
},
oidc: {
defaultProvider: 'oidc',
session: {
expirationCheck: true,
automaticRefresh: true,
},
middleware: {
globalMiddlewareEnabled: true,
customLoginPage: true,
},
providers: {
// Generic OIDC against the dezky-operator Authentik client. Same shape
// as the customer portal's config but pointed at a different provider
// and a different audience.
oidc: {
clientId: process.env.NUXT_OIDC_CLIENT_ID || '',
clientSecret: process.env.NUXT_OIDC_CLIENT_SECRET || '',
redirectUri: process.env.NUXT_OIDC_REDIRECT_URI || `${OPERATOR_URL}/auth/oidc/callback`,
authorizationUrl: `${AUTH_URL}/application/o/authorize/`,
tokenUrl: `${AUTH_URL}/application/o/token/`,
userInfoUrl: `${AUTH_URL}/application/o/userinfo/`,
logoutUrl: `${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/end-session/`,
openIdConfiguration:
`${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/.well-known/openid-configuration`,
// offline_access: refresh tokens for silent session renewal — without
// it, an expired access token kills the session (dead UI until F5).
scope: ['openid', 'profile', 'email', 'groups', 'offline_access'],
userNameClaim: 'preferred_username',
responseType: 'code',
grantType: 'authorization_code',
pkce: true,
skipAccessTokenParsing: true,
exposeAccessToken: true,
// Also expose id_token so /api/auth/sign-out can pass it as
// id_token_hint to Authentik's end-session endpoint. Without it
// Authentik can't identify the session to terminate and falls back
// to its own "you've logged out" confirmation page.
exposeIdToken: true,
},
},
},
vite: {
server: {
// Vite 7 added a strict host check; allow Traefik-fronted hostnames in dev
allowedHosts: ['operator.dezky.local'],
hmr: {
protocol: 'wss',
clientPort: 443,
},
},
},
nitro: {
routeRules: {
'/api/**': { cors: true },
},
// Persist nuxt-oidc-auth's session store on disk so HMR / dev-server
// restarts don't sign operators out. The default memory driver is fine
// in prod where one long-running container holds the state.
storage: {
oidc: { driver: 'fs', base: '.nuxt/oidc-store' },
},
},
})