diff --git a/client/lib/foundation-oauth.ts b/client/lib/foundation-oauth.ts new file mode 100644 index 00000000..94443e77 --- /dev/null +++ b/client/lib/foundation-oauth.ts @@ -0,0 +1,99 @@ +/** + * Foundation OAuth Client + * + * This module handles the OAuth flow with aethex.foundation as the identity provider. + * aethex.dev acts as an OAuth client, redirecting users to Foundation for authentication. + */ + +const FOUNDATION_URL = import.meta.env.VITE_FOUNDATION_URL || "https://aethex.foundation"; +const API_BASE = import.meta.env.VITE_API_BASE || "https://aethex.dev"; + +/** + * Generate authorization URL for redirecting to Foundation login + */ +export function getFoundationAuthorizationUrl(options?: { + redirectTo?: string; + state?: string; +}): string { + const params = new URLSearchParams(); + + // client_id identifies aethex.dev as the client + params.set("client_id", "aethex-corp"); + + // Redirect back to aethex.dev after authentication + params.set("redirect_uri", `${API_BASE}/api/auth/foundation-callback`); + + // OAuth standard - maintain context across redirect + params.set("response_type", "code"); + params.set("scope", "openid profile email"); + + // Custom state for additional context + if (options?.state) { + params.set("state", options.state); + } + + return `${FOUNDATION_URL}/api/oauth/authorize?${params.toString()}`; +} + +/** + * Start the OAuth flow by redirecting to Foundation + */ +export function initiateFoundationLogin(redirectTo?: string): void { + // Store intended destination for after auth + if (redirectTo) { + sessionStorage.setItem("auth_redirect_to", redirectTo); + } + + const state = JSON.stringify({ + redirectTo: redirectTo || "/dashboard", + timestamp: Date.now(), + }); + + const authUrl = getFoundationAuthorizationUrl({ + redirectTo, + state: encodeURIComponent(state), + }); + + window.location.href = authUrl; +} + +/** + * Exchange authorization code for token + * This is called from the Foundation callback endpoint on the backend + */ +export async function exchangeCodeForToken(code: string): Promise<{ + accessToken: string; + user: any; +}> { + const response = await fetch(`${API_BASE}/api/auth/exchange-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), + credentials: "include", // Include cookies for session + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.message || "Failed to exchange code for token"); + } + + return response.json(); +} + +/** + * Get the stored redirect destination after auth + */ +export function getStoredRedirectTo(): string | null { + if (typeof window === "undefined") return null; + return sessionStorage.getItem("auth_redirect_to"); +} + +/** + * Clear the stored redirect destination + */ +export function clearStoredRedirectTo(): void { + if (typeof window === "undefined") return; + sessionStorage.removeItem("auth_redirect_to"); +}