Files
ORCID2SWORD/frontend/src/contexts/AuthContext.jsx
T
Alexis 25dfeec3f7 feat: enhance authentication flow and UI components
- Updated .env.example to include OAuth authentication details and bypass mode for development.
- Integrated AuthProvider in App component to manage authentication state.
- Added AuthCallbackPage for handling OAuth callback.
- Enhanced ExportDropdown and PublicationsTable components to display new publication indicators for authenticated users.
- Updated AppHeader to show authentication status and logout functionality.
- Improved LandingPage to support group search and simulate login in bypass mode.
- Refactored DashboardPage to conditionally handle publication exports based on user authentication status.
2026-04-29 12:19:47 +02:00

82 lines
2.7 KiB
React

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);
/**
* 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.
*
* For development / sandbox bypass (VITE_AUTH_BYPASS=true), the token is
* stored directly via storeToken() without going through ORCID.
*/
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);
}, []);
/**
* Stores a JWT directly (used by AuthCallbackPage and bypass mode).
* 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), storeToken, logout }),
[token, storeToken, logout],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx;
}