if (import.meta.env.DEV || import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true') { type StoredTokens = { accessToken: string; refreshToken: string; expiresAt: number; scope?: string; }; const CLIENTS: Record = { lumen: import.meta.env.VITE_OAUTH_CLIENT_ID || 'tenant-admin-app', storycraft: 'demo-tenant-admin-storycraft', viewfinder: 'demo-tenant-admin-viewfinder', pixel: 'demo-tenant-admin-pixel', }; const scopes = (import.meta.env.VITE_OAUTH_SCOPES as string | undefined) ?? 'tenant:read tenant:write'; const baseUrl = window.location.origin; const redirectUri = `${baseUrl}/event-admin/auth/callback`; async function loginAs(label: string): Promise { const clientId = CLIENTS[label]; if (!clientId) { console.warn('[DevAuth] Unknown tenant key', label); return; } try { const tokens = await fetchTokens(clientId); localStorage.setItem('tenant_oauth_tokens.v1', JSON.stringify(tokens)); window.location.assign('/event-admin/dashboard'); } catch (error) { if (error instanceof Error) { console.error('[DevAuth] Failed to login', error.message); } else { console.error('[DevAuth] Failed to login', error); } } } async function fetchTokens(clientId: string): Promise { const verifier = randomString(32); const challenge = await sha256(verifier); const state = randomString(12); const authorizeParams = new URLSearchParams({ response_type: 'code', client_id: clientId, redirect_uri: redirectUri, scope: scopes, state, code_challenge: challenge, code_challenge_method: 'S256', }); const callbackUrl = await requestAuthorization(`/api/v1/oauth/authorize?${authorizeParams}`); verifyState(callbackUrl.searchParams.get('state'), state); const code = callbackUrl.searchParams.get('code'); if (!code) { throw new Error('Authorize response missing code'); } const tokenResponse = await fetch('/api/v1/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: clientId, redirect_uri: redirectUri, code_verifier: verifier, }), }); if (!tokenResponse.ok) { throw new Error(`Token exchange failed with ${tokenResponse.status}`); } const body = await tokenResponse.json(); const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 3600; return { accessToken: body.access_token, refreshToken: body.refresh_token, expiresAt: Date.now() + Math.max(expiresIn - 30, 0) * 1000, scope: body.scope, }; } function randomString(bytes: number): string { const buffer = new Uint8Array(bytes); crypto.getRandomValues(buffer); return base64Url(buffer); } async function sha256(input: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(input); const digest = await crypto.subtle.digest('SHA-256', data); return base64Url(new Uint8Array(digest)); } function base64Url(data: Uint8Array): string { const binary = Array.from(data, (byte) => String.fromCharCode(byte)).join(''); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } const api = { loginAs, clients: CLIENTS }; console.info('[DevAuth] Demo tenant helpers ready', Object.keys(CLIENTS)); // @ts-expect-error Dev helper for debugging only. window.fotospielDemoAuth = api; // @ts-expect-error Dev helper for debugging only. globalThis.fotospielDemoAuth = api; } function requestAuthorization(url: string): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.withCredentials = true; xhr.onreadystatechange = () => { if (xhr.readyState !== XMLHttpRequest.DONE) { return; } const responseUrl = xhr.responseURL || xhr.getResponseHeader('Location'); if (xhr.status >= 200 && xhr.status < 400 && responseUrl) { resolve(new URL(responseUrl, window.location.origin)); return; } reject(new Error(`Authorize failed with ${xhr.status}`)); }; xhr.onerror = () => reject(new Error('Authorize request failed')); xhr.send(); }); } function verifyState(returnedState: string | null, expectedState: string): void { if (returnedState && returnedState !== expectedState) { throw new Error('Authorize state mismatch'); } }