@authava/react-client
Version:
React client library for seamless integration with Authava's white-label authentication service
195 lines • 9.54 kB
JavaScript
import React, { createContext, useContext, useEffect, useState, useMemo, useRef } from 'react';
import { AuthavaClient, MockAuthavaClient, } from '@authava/client';
import { hasPermission, hasRole, isValidUser } from './utils/authUtils';
const AuthavaContext = createContext(undefined);
export function AuthavaProvider({ config, children, sessionMode = 'protected', useMock = false, }) {
const isProtected = sessionMode === 'protected';
const log = (...args) => {
if (config.debug) {
console.log(...args);
}
};
log('[AuthavaProvider] Initializing with config:', config);
const [isLoading, setIsLoading] = useState(true);
const [session, setSession] = useState(null);
const hasLoggedOutRef = useRef(false);
const [sessionState, setSessionState] = useState({
status: 'expired',
user: null,
});
// Flag to indicate that the initial session fetch (or a valid session update) has occurred.
const hasInitialSessionResolvedRef = useRef(false);
const lastValidSessionTimeRef = useRef(0);
const hasFetchedSession = useRef(false);
const lastRefetch = useRef(0);
const client = useMemo(() => {
const newClient = useMock ? new MockAuthavaClient(config) : new AuthavaClient(config);
log('[AuthavaProvider] Created new AuthavaClient instance:', newClient);
return newClient;
}, [config]);
useEffect(() => {
// Only trigger logout automatically if we're in the protected mode
if (!isProtected) {
log('[AuthavaProvider] Session mode is public; skipping logout on session change.');
setSession(null);
setIsLoading(false);
return;
}
log('[AuthavaProvider] Setting up session change subscription.');
let isMounted = true;
const handleSessionChange = (state) => {
log('[AuthavaProvider] handleSessionChange called with state:', state);
if (!isMounted) {
log('[AuthavaProvider] Component unmounted. Ignoring session change.');
return;
}
setSessionState(state);
log('[AuthavaProvider] Updated sessionState:', state);
if (state.status === 'valid' && isValidUser(state.user)) {
log('[AuthavaProvider] Received valid session. Setting session:', state.user);
setSession({ user: state.user, redirect_url: '' });
lastValidSessionTimeRef.current = Date.now();
log('[AuthavaProvider] Recorded lastValidSessionTime:', lastValidSessionTimeRef.current);
if (!hasInitialSessionResolvedRef.current) {
hasInitialSessionResolvedRef.current = true;
log('[AuthavaProvider] Marked initial session as resolved from onSessionChange callback.');
}
}
else if (state.status === 'expired' ||
state.status === 'error' ||
(state.status === 'valid' && !isValidUser(state.user))) {
// Immediately trigger logout if initial session has been resolved.
if (!hasInitialSessionResolvedRef.current) {
log('[AuthavaProvider] Initial session not resolved; ignoring expired/error state.');
return;
}
if (!hasLoggedOutRef.current) {
log('[AuthavaProvider] Triggering logout from session change handler.');
hasLoggedOutRef.current = true;
setTimeout(() => {
log('[AuthavaProvider] Executing deferred logout.');
client.logout();
}, 0);
}
log('[AuthavaProvider] Clearing session due to logout trigger.');
setSession(null);
}
};
const unsubscribe = client.onSessionChange(handleSessionChange);
log('[AuthavaProvider] Subscribed to session changes.');
if (!hasFetchedSession.current) {
log('[AuthavaProvider] Fetching initial session.');
hasFetchedSession.current = true;
client.getSession().then((newSession) => {
if (!isMounted) {
log('[AuthavaProvider] Component unmounted before getSession completed.');
return;
}
if (!isProtected) {
setSession(null);
log('[AuthavaProvider] Session mode is public; skipping session update.');
return;
}
log('[AuthavaProvider] Initial getSession result:', newSession);
setSession(newSession);
if (newSession && isValidUser(newSession.user)) {
lastValidSessionTimeRef.current = Date.now();
log('[AuthavaProvider] Recorded lastValidSessionTime from initial getSession:', lastValidSessionTimeRef.current);
}
else {
// If getSession returns null (or invalid data), trigger logout.
if (!hasLoggedOutRef.current) {
log('[AuthavaProvider] getSession returned null or invalid session; triggering logout.');
hasLoggedOutRef.current = true;
client.logout();
}
}
setIsLoading(false);
hasInitialSessionResolvedRef.current = true;
log('[AuthavaProvider] Set isLoading to false and marked initial session as resolved.');
});
}
return () => {
log('[AuthavaProvider] Cleaning up session change subscription.');
isMounted = false;
unsubscribe();
};
}, [client, isProtected]);
useEffect(() => {
if (!isProtected) {
log('[AuthavaProvider] Session mode is public; skipping visibility/focus event listeners.');
return;
}
log('[AuthavaProvider] Setting up visibility/focus event listeners.');
const handler = () => {
log('[AuthavaProvider] Visibility/Focus event triggered.');
if (hasLoggedOutRef.current) {
log('[AuthavaProvider] Logout already triggered. Skipping getSession on event.');
return;
}
if (!hasFetchedSession.current) {
log('[AuthavaProvider] Session not fetched yet. Skipping getSession on event.');
return;
}
const timeSinceLastRefetch = Date.now() - lastRefetch.current;
log('[AuthavaProvider] Time since last refetch (ms):', timeSinceLastRefetch);
if (timeSinceLastRefetch < 10000) {
log('[AuthavaProvider] Refetch throttled. Skipping getSession on event.');
return;
}
lastRefetch.current = Date.now();
log('[AuthavaProvider] Triggering getSession on visibility/focus event.');
client.getSession();
};
document.addEventListener('visibilitychange', handler);
window.addEventListener('focus', handler);
log('[AuthavaProvider] Event listeners added for visibilitychange and focus.');
return () => {
log('[AuthavaProvider] Removing visibility/focus event listeners.');
document.removeEventListener('visibilitychange', handler);
window.removeEventListener('focus', handler);
};
}, [client, isProtected]);
const logout = async () => {
if (!isProtected) {
log('[AuthavaProvider] Session mode is public; skipping logout.');
setSession(null);
return;
}
log('[AuthavaProvider] logout called from provider.');
await client.logout();
};
const value = useMemo(() => ({
isLoading,
session,
sessionState,
logout,
login: (data) => client.login(data),
register: (data) => client.register(data),
forgotPassword: (data) => client.forgotPassword(data),
resetPassword: (data) => client.resetPassword(data),
verifyMfa: (data) => client.verifyMfa(data),
sendMfaEmail: (data) => client.sendMfaEmail(data),
getProfile: () => client.getProfile(),
changeEmail: (data) => client.changeEmail(data),
changePassword: (data) => client.changePassword(data),
updateNotificationPreferences: (data) => client.updateNotificationPreferences(data),
removeMfaMethod: (methodId) => client.removeMfaMethod(methodId),
setupEmailMfa: (data) => client.setupEmailMfa(data),
verifyEmailMfa: (data) => client.verifyEmailMfa(data),
setupTotp: (data) => client.setupTotp(data),
verifyTotp: (data) => client.verifyTotp(data),
hasRole: (r) => (session?.user ? hasRole(session.user, r) : false),
hasPermission: (p) => session?.user ? hasPermission(session.user, p) : false,
}), [isLoading, session, sessionState, client]);
log('[AuthavaProvider] Rendering provider with value:', value);
return React.createElement(AuthavaContext.Provider, { value: value }, children);
}
export function useAuthava() {
const context = useContext(AuthavaContext);
if (context === undefined) {
throw new Error('useAuthava must be used within an AuthavaProvider');
}
return context;
}
//# sourceMappingURL=AuthavaContext.js.map