From d6337c1ece4c9bd6fe3b952b295d95f07dcb1c8d Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 1 Apr 2026 17:37:16 +0200 Subject: [PATCH] Tenant-specific OIDC: separate Zitadel projects per tenant Each tenant config now includes oidcClientId. Auth composable reads client ID from tenant config instead of hardcoded fallback. Dev tenant uses restricted Zitadel project (role-check enabled, developer grant). Added NODE_ENV=production to env files to fix --mode dev builds. Co-Authored-By: Claude Opus 4.6 --- .env.dev | 1 + .env.loop42 | 1 + src/composables/auth.ts | 5 +++-- src/tenant.ts | 13 ++++++++----- tenants/dev/config.ts | 1 + tenants/loop42/config.ts | 1 + 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.env.dev b/.env.dev index b2ee630..0afbfbd 100644 --- a/.env.dev +++ b/.env.dev @@ -1 +1,2 @@ +NODE_ENV=production VITE_TENANT=dev diff --git a/.env.loop42 b/.env.loop42 index ba459df..8361bd0 100644 --- a/.env.loop42 +++ b/.env.loop42 @@ -1 +1,2 @@ +NODE_ENV=production VITE_TENANT=loop42 diff --git a/src/composables/auth.ts b/src/composables/auth.ts index 9974722..0b3e28c 100644 --- a/src/composables/auth.ts +++ b/src/composables/auth.ts @@ -12,14 +12,15 @@ import { ref, computed, type Ref } from 'vue'; import router from '../router'; import { getApiBase } from '../utils/apiBase'; +import tenant from '../tenant'; const TOKEN_KEY = 'nyx_session'; const VERIFIER_KEY = 'pkce_verifier'; const RETURN_KEY = 'auth_return_path'; -// Zitadel config — fetched from backend or hardcoded fallback +// Zitadel config — tenant-specific client ID, fetched from backend as override let _issuer = 'https://auth.loop42.de'; -let _clientId = '365996029172056091'; +let _clientId = tenant.oidcClientId; const _redirectUri = `${window.location.origin}/login`; const _scopes = 'openid profile email'; diff --git a/src/tenant.ts b/src/tenant.ts index 24ca157..2e749d9 100644 --- a/src/tenant.ts +++ b/src/tenant.ts @@ -2,8 +2,7 @@ * tenant.ts — Build-time tenant configuration * * VITE_TENANT selects which tenant config to load. - * Vite's static analysis + tree-shaking ensures only the - * active tenant's code ends up in the bundle. + * Direct dynamic import ensures only the active tenant is bundled. */ export interface TenantFeatures { @@ -18,17 +17,21 @@ export interface TenantConfig { name: string; domain: string; defaultTheme: string; + oidcClientId: string; features: TenantFeatures; } -const modules = import.meta.glob('../tenants/*/config.ts', { eager: true, import: 'default' }); +// Vite replaces import.meta.env.VITE_TENANT at build time, +// making the import path a string literal → only one tenant bundled. +const modules: Record = import.meta.glob('../tenants/*/config.ts', { eager: true }); const tenantId = import.meta.env.VITE_TENANT || 'loop42'; const key = `../tenants/${tenantId}/config.ts`; -const config = modules[key] as TenantConfig; +const mod = modules[key]; -if (!config) { +if (!mod) { throw new Error(`Unknown tenant: ${tenantId}. Available: ${Object.keys(modules).join(', ')}`); } +const config: TenantConfig = mod.default; export default config; diff --git a/tenants/dev/config.ts b/tenants/dev/config.ts index 2520af0..a2c88f9 100644 --- a/tenants/dev/config.ts +++ b/tenants/dev/config.ts @@ -5,6 +5,7 @@ const config: TenantConfig = { name: 'loop42 Dev', domain: 'loop42.dev', defaultTheme: 'titan', + oidcClientId: '366683272559788059', features: { graph: true, trace: true, diff --git a/tenants/loop42/config.ts b/tenants/loop42/config.ts index 1104563..781fc7f 100644 --- a/tenants/loop42/config.ts +++ b/tenants/loop42/config.ts @@ -5,6 +5,7 @@ const config: TenantConfig = { name: 'loop42', domain: 'loop42.de', defaultTheme: 'loop42', + oidcClientId: '365996029172056091', features: { graph: false, trace: false,