Hook for Foundation OAuth callback handling

cgen-fe47700826674f57b452acb781e51833
This commit is contained in:
Builder.io 2025-11-17 02:24:51 +00:00
parent b677da8a60
commit 78972e1f88

View file

@ -1,24 +1,25 @@
/**
* useFoundationAuth Hook
*
* Manages Foundation OAuth flow including:
* - Detecting authorization code in URL
* - Exchanging code for token
* - Syncing user profile from Foundation
* - Handling session establishment
*
* Handles Foundation OAuth callback:
* - Detects authorization code in URL
* - Validates state token (CSRF protection)
* - Exchanges code for access token
* - Syncs user profile from Foundation
* - Establishes session
*/
import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import {
getFoundationAuthCode,
hasFoundationAuthCode,
getFoundationAccessToken,
getAuthUserId,
clearFoundationAuth,
fetchUserProfileFromFoundation,
syncFoundationProfileToLocal,
} from "@/lib/foundation-auth";
getAuthorizationCode,
getStateFromUrl,
hasAuthorizationCode,
validateState,
exchangeCodeForToken,
clearOAuthStorage,
getStoredRedirectTo,
} from "@/lib/foundation-oauth";
import { aethexToast } from "@/lib/aethex-toast";
interface UseFoundationAuthReturn {
@ -26,6 +27,10 @@ interface UseFoundationAuthReturn {
error: string | null;
}
/**
* Hook that processes Foundation OAuth callback
* Should be called in your main App component to catch oauth redirects
*/
export function useFoundationAuth(): UseFoundationAuthReturn {
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -33,74 +38,84 @@ export function useFoundationAuth(): UseFoundationAuthReturn {
const location = useLocation();
useEffect(() => {
// Check if we just received an auth code from Foundation
if (!hasFoundationAuthCode()) {
// Check if we have an authorization code in the URL
if (!hasAuthorizationCode()) {
return;
}
setIsProcessing(true);
setError(null);
const processFoundationAuth = async () => {
const processFoundationCallback = async () => {
try {
const code = getFoundationAuthCode();
// Get authorization code and state from URL
const code = getAuthorizationCode();
const urlState = getStateFromUrl();
if (!code) {
throw new Error("No authorization code found");
throw new Error("No authorization code found in URL");
}
// Exchange code for token with backend
const API_BASE = import.meta.env.VITE_API_BASE || "";
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
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || "Failed to exchange authorization code",
);
// Validate state token (CSRF protection)
if (!validateState(urlState)) {
console.warn("[Foundation Auth] State validation failed");
throw new Error("Invalid state token - possible CSRF attack");
}
const data = await response.json();
console.log("[Foundation Auth] Processing OAuth callback with code");
if (!data.accessToken || !data.user) {
throw new Error("Invalid response from token exchange");
// Exchange authorization code for access token
try {
const tokenData = await exchangeCodeForToken(code);
if (!tokenData.accessToken || !tokenData.user) {
throw new Error("Invalid response from token exchange");
}
console.log("[Foundation Auth] Token exchange successful for user:", tokenData.user.id);
// Clear auth parameters from URL and storage
const url = new URL(window.location.href);
url.searchParams.delete("code");
url.searchParams.delete("state");
window.history.replaceState({}, "", url.toString());
clearOAuthStorage();
// Show success message
aethexToast.success({
title: "Authenticated",
description: `Welcome back, ${tokenData.user.username || tokenData.user.email}!`,
});
// Determine redirect destination
const storedRedirect = getStoredRedirectTo();
const redirectTo = storedRedirect || "/dashboard";
// Redirect to dashboard or stored destination
navigate(redirectTo, { replace: true });
} catch (exchangeError) {
const message = exchangeError instanceof Error
? exchangeError.message
: "Failed to exchange authorization code";
console.error("[Foundation Auth] Token exchange failed:", exchangeError);
throw new Error(message);
}
// Sync Foundation user profile to local database
await syncFoundationProfileToLocal(data.user);
// Clear auth code from URL
const url = new URL(window.location.href);
url.searchParams.delete("code");
url.searchParams.delete("state");
window.history.replaceState({}, "", url.toString());
aethexToast.success({
title: "Authenticated",
description: "Successfully authenticated with Foundation",
});
// Redirect to dashboard or stored redirect
const redirectTo = sessionStorage.getItem("auth_redirect_to") || "/dashboard";
sessionStorage.removeItem("auth_redirect_to");
navigate(redirectTo, { replace: true });
} catch (err) {
const message = err instanceof Error ? err.message : "Authentication failed";
setError(message);
console.error("[Foundation Auth] Callback processing error:", err);
aethexToast.error({
title: "Authentication failed",
description: message,
});
// Clear auth state on error
clearFoundationAuth();
// Clear OAuth storage on error
clearOAuthStorage();
// Redirect back to login after a delay
setTimeout(() => {
@ -111,7 +126,7 @@ export function useFoundationAuth(): UseFoundationAuthReturn {
}
};
processFoundationAuth();
processFoundationCallback();
}, [location.search, navigate]);
return {
@ -121,28 +136,35 @@ export function useFoundationAuth(): UseFoundationAuthReturn {
}
/**
* Check if user has active Foundation authentication
* Hook to check current Foundation authentication status
*/
export function useFoundationAuthStatus(): {
isAuthenticated: boolean;
userId: string | null;
accessToken: string | null;
} {
const [status, setStatus] = useState({
isAuthenticated: false,
userId: null as string | null,
accessToken: null as string | null,
});
useEffect(() => {
const token = getFoundationAccessToken();
const userId = getAuthUserId();
// Check for foundation_access_token cookie
const cookies = document.cookie.split(";").map((c) => c.trim());
const tokenCookie = cookies.find((c) => c.startsWith("foundation_access_token="));
const userCookie = cookies.find((c) => c.startsWith("auth_user_id="));
setStatus({
isAuthenticated: !!token && !!userId,
userId,
accessToken: token,
});
if (tokenCookie && userCookie) {
const userId = userCookie.split("=")[1];
setStatus({
isAuthenticated: true,
userId,
});
} else {
setStatus({
isAuthenticated: false,
userId: null,
});
}
}, []);
return status;