Files
dezky/apps/portal/nuxt.config.ts
T
Ronni Baslund 7f8516295c feat(portal): useFeatureFlag composable + /api/flags/evaluate proxy
Client-side helper for the portal to consume feature flags. Hits platform-api
through a new portal-side proxy that derives the tenant slug from the
signed-in user's JWT groups — so callers don't pass a slug, they just check
`useFeatureFlag('key')`.

apps/portal/server/api/flags/evaluate.post.ts:
- Reads access token from the nuxt-oidc-auth session
- Decodes the JWT and picks the first non-admin group as the tenant slug
  (admin groups: dezky-platform-admins, "authentik Admins"). Filters
  duplicates Authentik double-lists via policy bindings.
- Forwards { tenantSlug } to platform-api POST /flags/evaluate
- Caller can still pass an explicit tenantSlug in the request body to
  override the auto-derivation (rare).

apps/portal/composables/useFeatureFlag.ts:
- Singleton module-level state shared across every component — one bulk
  eval per session, not one per flag check
- `useFeatureFlag(key)` → ComputedRef<boolean>, lazily triggers the first
  eval, fail-closed (every flag stays false on error)
- `useFeatureFlags()` → { flags, ready, pending, refresh } for the rare
  case where you need the full map or want to re-evaluate (long-lived
  session, admin flipped a flag mid-flight)
- Returns refs that update once the bulk eval lands; gated UI stays
  hidden during the ~25ms round trip

apps/portal/nuxt.config.ts:
- Vite 7 `server.allowedHosts` set to ['app.dezky.local'] — same fix we
  already shipped on the operator side; without it, the proxy returned a
  plaintext 403 "Blocked request" instead of forwarding.

Verified end-to-end: signed in to app.dezky.local, hit /api/flags/evaluate
with no body → 200 with the full truth map (same shape as the operator's
direct eval), latency ~25ms, explicit-slug override returns identical
results.
2026-05-24 19:26:55 +02:00

90 lines
2.9 KiB
TypeScript

// Nuxt 3 configuration for Dezky portal
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2026-01-01',
devtools: { enabled: true },
modules: ['nuxt-oidc-auth'],
css: ['~/assets/styles/tokens.css', '~/assets/styles/base.css'],
app: {
head: {
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',
},
],
},
},
runtimeConfig: {
mongodbUri: process.env.MONGODB_URI,
apiBase: process.env.NUXT_API_BASE,
public: {
authUrl: process.env.NUXT_PUBLIC_AUTH_URL,
portalUrl: process.env.NUXT_PUBLIC_PORTAL_URL,
},
},
oidc: {
defaultProvider: 'oidc',
session: {
expirationCheck: true,
automaticRefresh: true,
},
middleware: {
globalMiddlewareEnabled: true,
customLoginPage: true,
},
providers: {
// Generic OIDC against our Authentik instance (provider preset key MUST be one of
// apple, auth0, cognito, entra, github, keycloak, logto, microsoft, oidc, paypal, zitadel).
oidc: {
clientId: process.env.NUXT_OIDC_CLIENT_ID || '',
clientSecret: process.env.NUXT_OIDC_CLIENT_SECRET || '',
redirectUri: process.env.NUXT_OIDC_REDIRECT_URI || '',
authorizationUrl: 'https://auth.dezky.local/application/o/authorize/',
tokenUrl: 'https://auth.dezky.local/application/o/token/',
userInfoUrl: 'https://auth.dezky.local/application/o/userinfo/',
logoutUrl: 'https://auth.dezky.local/application/o/dezky-portal/end-session/',
// Discovery URL — used by id_token validation to fetch JWKS + issuer
openIdConfiguration:
'https://auth.dezky.local/application/o/dezky-portal/.well-known/openid-configuration',
scope: ['openid', 'profile', 'email', 'groups'],
userNameClaim: 'preferred_username',
responseType: 'code',
grantType: 'authorization_code',
pkce: true,
// Authentik's access tokens aren't always parseable as JWT — skip strict parsing
skipAccessTokenParsing: true,
// Expose access token in the server-side session so Nitro route handlers can
// forward it to platform-api. Token never reaches the browser.
exposeAccessToken: true,
},
},
},
vite: {
server: {
hmr: {
protocol: 'wss',
clientPort: 443,
},
// Vite 7's strict allowedHosts blocks anything not in this list with a
// plaintext 403. We serve the portal behind Traefik on app.dezky.local.
allowedHosts: ['app.dezky.local'],
},
},
nitro: {
routeRules: {
'/api/**': { cors: true },
},
},
})