UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

525 lines (459 loc) 14.7 kB
/** * @frank-auth/react - useSession Hook * * Session management hook that provides access to session operations, * multi-session handling, and session security features. */ import {useCallback, useEffect, useMemo, useState} from 'react'; import type {Session, SessionInfo} from '@frank-auth/client'; import {useAuth} from './use-auth'; import {useConfig} from '../provider/config-provider'; import type {AuthError} from '../provider/types'; // ============================================================================ // Session Hook Interface // ============================================================================ export interface UseSessionReturn { // Session state session: Session | null; sessions: SessionInfo[]; isLoaded: boolean; isLoading: boolean; error: AuthError | null; // Session management createSession: (token: string) => Promise<Session>; setActiveSession: (sessionId: string) => Promise<void>; refreshSession: () => Promise<Session | null>; revokeSession: (sessionId: string) => Promise<void>; revokeAllSessions: (exceptCurrent?: boolean) => Promise<void>; endSession: () => Promise<void>; // Session information sessionId: string | null; sessionToken: string | null; expiresAt: Date | null; lastActiveAt: Date | null; // Session status isActive: boolean; isExpired: boolean; isExpiring: boolean; // Expires within 5 minutes timeUntilExpiry: number | null; // Minutes until expiry // Device information deviceInfo: DeviceInfo | null; // Security features isCurrentDevice: boolean; isTrustedDevice: boolean; // Multi-session support hasMultipleSessions: boolean; sessionCount: number; otherSessions: SessionInfo[]; } export interface DeviceInfo { userAgent: string; browser: string; os: string; device: string; ipAddress: string; location?: { city?: string; country?: string; region?: string; }; } // ============================================================================ // Main useSession Hook // ============================================================================ /** * Session management hook providing access to all session functionality * * @example Basic session management * ```tsx * import { useSession } from '@frank-auth/react'; * * function SessionManager() { * const { * session, * sessions, * revokeSession, * revokeAllSessions, * isExpiring * } = useSession(); * * if (isExpiring) { * return ( * <div className="session-warning"> * <p>Your session is about to expire</p> * <button onClick={refreshSession}>Extend Session</button> * </div> * ); * } * * return ( * <div> * <h3>Active Sessions ({sessions.length})</h3> * {sessions.map((session) => ( * <div key={session.id}> * <p>{session.deviceInfo?.browser} on {session.deviceInfo?.os}</p> * <p>Last active: {session.lastActiveAt}</p> * <button onClick={() => revokeSession(session.id)}> * Revoke Session * </button> * </div> * ))} * <button onClick={() => revokeAllSessions(true)}> * Revoke All Other Sessions * </button> * </div> * ); * } * ``` * * @example Session expiry warning * ```tsx * function SessionExpiryWarning() { * const { isExpiring, timeUntilExpiry, refreshSession } = useSession(); * * if (!isExpiring || !timeUntilExpiry) return null; * * return ( * <div className="alert alert-warning"> * <p>Session expires in {timeUntilExpiry} minutes</p> * <button onClick={refreshSession}> * Extend Session * </button> * </div> * ); * } * ``` */ export function useSession(): UseSessionReturn { const {session, sdk, createSession: authCreateSession, reload, userType} = useAuth(); const {apiUrl, publishableKey} = useConfig(); const [sessions, setSessions] = useState<SessionInfo[]>([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<AuthError | null>(null); // Error handler const handleError = useCallback((err: any) => { const authError: AuthError = { code: err.code || 'UNKNOWN_ERROR', message: err.message || 'An unknown error occurred', details: err.details, field: err.field, }; setError(authError); throw authError; }, []); // Load sessions on mount and session change const loadSessions = useCallback(async () => { try { setIsLoading(true); setError(null); const sessionsData = await sdk.session.listSessions({fields: []}); setSessions(sessionsData.data as any); } catch (err) { console.error('Failed to load sessions:', err); setError({ code: 'SESSIONS_LOAD_FAILED', message: 'Failed to load sessions', }); } finally { setIsLoading(false); } }, [sdk.session]); useEffect(() => { loadSessions(); }, [loadSessions]); // Session management methods const createSession = useCallback(async (token: string): Promise<Session> => { return authCreateSession(token); }, [authCreateSession]); const setActiveSession = useCallback(async (sessionId: string): Promise<void> => { try { setIsLoading(true); setError(null); sdk.session.activeSession = sessionId; await reload(); // Refresh auth state await loadSessions(); // Refresh sessions list } catch (err) { handleError(err); } finally { setIsLoading(false); } }, [sdk.session, reload, loadSessions, handleError]); const refreshSession = useCallback(async (): Promise<Session | null> => { try { setIsLoading(true); setError(null); const refreshedSession = await sdk.session.refreshSession(); await reload(); // Refresh auth state return refreshedSession; } catch (err) { handleError(err); return null; } finally { setIsLoading(false); } }, [sdk.session, reload, handleError]); const revokeSession = useCallback(async (sessionId: string): Promise<void> => { try { setIsLoading(true); setError(null); await sdk.session.revokeSession(sessionId); // If we revoked the current session, reload auth state if (sessionId === session?.id) { await reload(); } else { // Just refresh the sessions list await loadSessions(); } } catch (err) { handleError(err); } finally { setIsLoading(false); } }, [sdk.session, session?.id, reload, loadSessions, handleError]); const revokeAllSessions = useCallback(async (exceptCurrent = false): Promise<void> => { try { setIsLoading(true); setError(null); await sdk.session.revokeAllSessions({ exceptCurrent, }); if (!exceptCurrent) { // All sessions revoked, user is signed out await reload(); } else { // Only other sessions revoked, refresh sessions list await loadSessions(); } } catch (err) { handleError(err); } finally { setIsLoading(false); } }, [sdk.session, reload, loadSessions, handleError]); const endSession = useCallback(async (): Promise<void> => { try { setIsLoading(true); setError(null); // await frankSession.endSession(); await reload(); // This will sign out the user } catch (err) { handleError(err); } finally { setIsLoading(false); } }, [sdk.session, reload, handleError]); // Session information const sessionId = useMemo(() => session?.id || null, [session]); const sessionToken = useMemo(() => session?.accessToken || null, [session]); const expiresAt = useMemo(() => session?.expiresAt ? new Date(session.expiresAt) : null, [session] ); const lastActiveAt = useMemo(() => session?.lastActiveAt ? new Date(session.lastActiveAt) : null, [session] ); // Session status const isActive = useMemo(() => !!session && !session.expired, [session]); const isExpired = useMemo(() => { if (!expiresAt) return false; return expiresAt.getTime() <= Date.now(); }, [expiresAt]); const isExpiring = useMemo(() => { if (!expiresAt) return false; const fiveMinutesFromNow = Date.now() + (5 * 60 * 1000); return expiresAt.getTime() <= fiveMinutesFromNow && !isExpired; }, [expiresAt, isExpired]); const timeUntilExpiry = useMemo(() => { if (!expiresAt) return null; const msUntilExpiry = expiresAt.getTime() - Date.now(); return Math.max(0, Math.floor(msUntilExpiry / 60000)); // Convert to minutes }, [expiresAt]); // Device information const deviceInfo = useMemo((): DeviceInfo | null => { if (!session?.deviceInfo) return null; return { userAgent: session.deviceInfo.userAgent || '', browser: session.deviceInfo.browser || 'Unknown', os: session.deviceInfo.os || 'Unknown', device: session.deviceInfo.device || 'Unknown', ipAddress: session.deviceInfo.ipAddress || '', location: session.deviceInfo.location, }; }, [session]); // Security features const isCurrentDevice = useMemo(() => { if (!session || !deviceInfo) return false; // Check if this is the current device by comparing user agent return typeof navigator !== 'undefined' && deviceInfo.userAgent === navigator.userAgent; }, [session, deviceInfo]); const isTrustedDevice = useMemo(() => session?.trustedDevice || false, [session] ); // Multi-session support const hasMultipleSessions = useMemo(() => sessions.length > 1, [sessions]); const sessionCount = useMemo(() => sessions.length, [sessions]); const otherSessions = useMemo(() => sessions.filter(s => s.id !== sessionId), [sessions, sessionId] ); return { // Session state session, sessions, isLoaded: !!session, isLoading, error, // Session management createSession, setActiveSession, refreshSession, revokeSession, revokeAllSessions, endSession, // Session information sessionId, sessionToken, expiresAt, lastActiveAt, // Session status isActive, isExpired, isExpiring, timeUntilExpiry, // Device information deviceInfo, // Security features isCurrentDevice, isTrustedDevice, // Multi-session support hasMultipleSessions, sessionCount, otherSessions, }; } // ============================================================================ // Specialized Session Hooks // ============================================================================ /** * Hook for session status monitoring */ export function useSessionStatus() { const { isActive, isExpired, isExpiring, timeUntilExpiry, expiresAt, refreshSession, } = useSession(); return { isActive, isExpired, isExpiring, timeUntilExpiry, expiresAt, refreshSession, status: isExpired ? 'expired' : isExpiring ? 'expiring' : isActive ? 'active' : 'inactive', }; } /** * Hook for multi-session management */ export function useMultiSession() { const { sessions, sessionCount, otherSessions, hasMultipleSessions, revokeSession, revokeAllSessions, setActiveSession, isLoading, error, } = useSession(); return { sessions, sessionCount, otherSessions, hasMultipleSessions, revokeSession, revokeAllSessions, setActiveSession, isLoading, error, revokeAllOthers: () => revokeAllSessions(true), }; } /** * Hook for device and security information */ export function useSessionSecurity() { const { deviceInfo, isCurrentDevice, isTrustedDevice, sessionId, lastActiveAt, } = useSession(); return { deviceInfo, isCurrentDevice, isTrustedDevice, sessionId, lastActiveAt, isSecure: isTrustedDevice && isCurrentDevice, }; } // ============================================================================ // Session Expiry Hook with Auto-refresh // ============================================================================ /** * Hook that automatically handles session expiry and refresh */ export function useSessionExpiry(options: { autoRefresh?: boolean; refreshThreshold?: number; // Minutes before expiry to refresh onExpiry?: () => void; onExpiring?: () => void; } = {}) { const { autoRefresh = false, refreshThreshold = 5, onExpiry, onExpiring, } = options; const { isExpired, isExpiring, timeUntilExpiry, refreshSession, } = useSession(); // Auto-refresh when approaching expiry useEffect(() => { if (autoRefresh && isExpiring && timeUntilExpiry && timeUntilExpiry <= refreshThreshold) { refreshSession().catch(console.error); } }, [autoRefresh, isExpiring, timeUntilExpiry, refreshThreshold, refreshSession]); // Handle expiry callback useEffect(() => { if (isExpired) { onExpiry?.(); } }, [isExpired, onExpiry]); // Handle expiring callback useEffect(() => { if (isExpiring) { onExpiring?.(); } }, [isExpiring, onExpiring]); return { isExpired, isExpiring, timeUntilExpiry, refreshSession, autoRefresh, }; }