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;
}