if (import.meta.env.DEV || import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true') { type StoredTokens = { accessToken: string; refreshToken: string; expiresAt: number; scope?: string; clientId?: 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) { const message = error instanceof Error ? error.message : String(error); console.error('[DevAuth] Failed to login', message); throw error instanceof Error ? error : new Error(message); } } 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}`, redirectUri); 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, clientId, }; } 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; } async function requestAuthorization(url: string, fallbackRedirect?: string): Promise { const requestUrl = new URL(url, window.location.origin); let response: Response; try { response = await fetch(requestUrl.toString(), { method: 'GET', credentials: 'include', headers: { Accept: 'application/json, text/plain, */*', 'X-Requested-With': 'XMLHttpRequest', }, redirect: 'manual', }); } catch (error) { throw new Error('Authorize request failed'); } const status = response.status; const isSuccess = (status >= 200 && status < 400) || status === 0; if (!isSuccess) { throw new Error(`Authorize failed with ${status}`); } const contentType = response.headers.get('Content-Type') ?? ''; if (contentType.includes('application/json')) { try { const payload = (await response.json()) as { code?: string; state?: string | null; redirect_url?: string | null; }; const target = payload.redirect_url ?? fallbackRedirect; if (!target) { throw new Error('Authorize response missing redirect target'); } const finalUrl = new URL(target, window.location.origin); if (payload.code && !finalUrl.searchParams.has('code')) { finalUrl.searchParams.set('code', payload.code); } if (payload.state && !finalUrl.searchParams.has('state')) { finalUrl.searchParams.set('state', payload.state); } return finalUrl; } catch (error) { throw error instanceof Error ? error : new Error(String(error)); } } const locationHeader = response.headers.get('Location'); if (locationHeader) { return new URL(locationHeader, window.location.origin); } if (response.url && response.url !== requestUrl.toString()) { return new URL(response.url, window.location.origin); } if (fallbackRedirect) { return new URL(fallbackRedirect, window.location.origin); } throw new Error('Authorize response missing redirect target'); } function verifyState(returnedState: string | null, expectedState: string): void { if (returnedState && returnedState !== expectedState) { throw new Error('Authorize state mismatch'); } }