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.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user