import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react"; const STORAGE_KEY = "orcid_auth_token"; // Message type sent by AuthCallbackPage (runs in the OAuth popup) // to notify the parent window that authentication succeeded. export const AUTH_MESSAGE_TYPE = "ORCID_AUTH_TOKEN"; export const AUTH_ERROR_TYPE = "ORCID_AUTH_ERROR"; const AuthContext = createContext(null); function extractOrcidFromToken(token) { if (!token) return null; try { const payloadBase64 = token.split(".")[1]; if (!payloadBase64) return null; const payloadJson = atob(payloadBase64.replace(/-/g, "+").replace(/_/g, "/")); const payload = JSON.parse(payloadJson); return payload?.sub ?? null; } catch { return null; } } function extractNameFromToken(token) { if (!token) return null; try { const payloadBase64 = token.split(".")[1]; if (!payloadBase64) return null; const payloadJson = atob(payloadBase64.replace(/-/g, "+").replace(/_/g, "/")); const payload = JSON.parse(payloadJson); if (payload?.name) return payload.name; const parts = [payload?.given_name, payload?.family_name].filter(Boolean); return parts.length > 0 ? parts.join(" ") : null; } catch { return null; } } /** * Provides JWT-based authentication state throughout the app. * * Authentication flow (OAuth 3-legged): * 1. User clicks "Iniciar sesión" → frontend opens popup at * GET /api/auth/orcid/authorize. * 2. Backend redirects to ORCID (sandbox or production). * 3. User authenticates at orcid.org. * 4. ORCID redirects to ORCID_REDIRECT_URI (= frontend /auth/callback). * 5. AuthCallbackPage exchanges the `code` for a JWT via the backend. * 6. Popup sends postMessage({ type: "ORCID_AUTH_TOKEN", token }) to * opener and closes itself. * 7. This provider's message listener stores the token and updates state. * */ export function AuthProvider({ children }) { const [token, setToken] = useState(() => localStorage.getItem(STORAGE_KEY)); // Listen for messages from the OAuth popup window. useEffect(() => { function handleMessage(event) { // Only accept messages from the same origin (the React app itself, // running in the popup after the OAuth redirect lands there). if (event.origin !== window.location.origin) return; if (event.data?.type === AUTH_MESSAGE_TYPE && event.data?.token) { localStorage.setItem(STORAGE_KEY, event.data.token); setToken(event.data.token); } } window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, []); // Fallback when postMessage cannot reach the opener (e.g. browser policy // severs window.opener during the OAuth redirect chain). localStorage is // shared between same-origin windows, so the popup's `setItem(...)` fires // a storage event in this window and we can pick up the new token. useEffect(() => { function handleStorage(event) { if (event.key !== STORAGE_KEY) return; if (event.newValue) setToken(event.newValue); else setToken(null); } window.addEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage); }, []); /** * Stores a JWT directly (used by AuthCallbackPage). * Does NOT trigger any network request. */ const storeToken = useCallback((accessToken) => { localStorage.setItem(STORAGE_KEY, accessToken); setToken(accessToken); }, []); const logout = useCallback(() => { localStorage.removeItem(STORAGE_KEY); setToken(null); }, []); const value = useMemo( () => ({ token, isAuthenticated: Boolean(token), userOrcidId: extractOrcidFromToken(token), userName: extractNameFromToken(token), storeToken, logout, }), [token, storeToken, logout], ); return {children}; } export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used inside "); return ctx; }