|
| 1 | +import { useEffect, useState, useMemo } from 'react'; |
| 2 | +import { useRouter } from 'next/router'; |
| 3 | +import { onAuthStateChanged, User } from 'firebase/auth'; |
| 4 | +import { auth } from '@/config/firebase'; |
| 5 | +import { UserRole } from '@/types/authTypes'; |
| 6 | +import baseAPIClient from '@/APIClients/baseAPIClient'; |
| 7 | + |
| 8 | +interface AxiosError { |
| 9 | + response?: { |
| 10 | + status: number; |
| 11 | + data: unknown; |
| 12 | + }; |
| 13 | + request?: unknown; |
| 14 | + message?: string; |
| 15 | +} |
| 16 | + |
| 17 | +interface AuthGuardState { |
| 18 | + loading: boolean; |
| 19 | + authorized: boolean; |
| 20 | +} |
| 21 | + |
| 22 | +// Map role IDs to UserRole enum |
| 23 | +const roleIdToUserRole = (roleId: number): UserRole | null => { |
| 24 | + switch (roleId) { |
| 25 | + case 1: |
| 26 | + return UserRole.PARTICIPANT; |
| 27 | + case 2: |
| 28 | + return UserRole.VOLUNTEER; |
| 29 | + case 3: |
| 30 | + return UserRole.ADMIN; |
| 31 | + default: |
| 32 | + return null; |
| 33 | + } |
| 34 | +}; |
| 35 | + |
| 36 | +/** |
| 37 | + * Hook to protect pages with authentication and role-based access control |
| 38 | + * @param allowedRoles - Array of roles that can access this page |
| 39 | + * @returns Object with loading and authorized states |
| 40 | + */ |
| 41 | +export const useAuthGuard = (allowedRoles: UserRole[]): AuthGuardState => { |
| 42 | + const router = useRouter(); |
| 43 | + const [authState, setAuthState] = useState<AuthGuardState>({ |
| 44 | + loading: true, |
| 45 | + authorized: false, |
| 46 | + }); |
| 47 | + |
| 48 | + // Memoize allowedRoles to prevent infinite re-renders |
| 49 | + const memoizedAllowedRoles = useMemo(() => allowedRoles, [allowedRoles]); |
| 50 | + |
| 51 | + const getUserRole = async (user: User): Promise<UserRole | null> => { |
| 52 | + const cacheKey = `userRole_${user.uid}`; |
| 53 | + |
| 54 | + // Check cache first |
| 55 | + try { |
| 56 | + const cached = sessionStorage.getItem(cacheKey); |
| 57 | + if (cached) { |
| 58 | + const { role, timestamp } = JSON.parse(cached); |
| 59 | + // Cache valid for 1 hour |
| 60 | + if (Date.now() - timestamp < 3600000) { |
| 61 | + return role; |
| 62 | + } |
| 63 | + // Remove expired cache |
| 64 | + sessionStorage.removeItem(cacheKey); |
| 65 | + } |
| 66 | + } catch { |
| 67 | + // If cache is corrupted, remove it and continue |
| 68 | + sessionStorage.removeItem(cacheKey); |
| 69 | + } |
| 70 | + |
| 71 | + try { |
| 72 | + // Get the Firebase ID token |
| 73 | + const token = await user.getIdToken(); |
| 74 | + // Call your backend to get user data with role |
| 75 | + const response = await baseAPIClient.get('/auth/me', { |
| 76 | + headers: { |
| 77 | + Authorization: `Bearer ${token}`, |
| 78 | + }, |
| 79 | + }); |
| 80 | + |
| 81 | + // Convert roleId to UserRole enum (API client converts snake_case to camelCase) |
| 82 | + const userRole = roleIdToUserRole(response.data.roleId); |
| 83 | + |
| 84 | + // Cache the result |
| 85 | + if (userRole) { |
| 86 | + sessionStorage.setItem( |
| 87 | + cacheKey, |
| 88 | + JSON.stringify({ |
| 89 | + role: userRole, |
| 90 | + timestamp: Date.now(), |
| 91 | + }), |
| 92 | + ); |
| 93 | + } |
| 94 | + |
| 95 | + return userRole; |
| 96 | + } catch (error: unknown) { |
| 97 | + console.error('[useAuthGuard] Error fetching user role:', error); |
| 98 | + if (error && typeof error === 'object' && 'response' in error) { |
| 99 | + const axiosError = error as AxiosError; |
| 100 | + console.error('[useAuthGuard] API Error status:', axiosError.response?.status); |
| 101 | + console.error('[useAuthGuard] API Error data:', axiosError.response?.data); |
| 102 | + } else if (error && typeof error === 'object' && 'request' in error) { |
| 103 | + console.error('[useAuthGuard] No response received:', (error as AxiosError).request); |
| 104 | + } else if (error && typeof error === 'object' && 'message' in error) { |
| 105 | + console.error('[useAuthGuard] Request setup error:', (error as AxiosError).message); |
| 106 | + } |
| 107 | + console.error('[useAuthGuard] Full error object:', error); |
| 108 | + return null; |
| 109 | + } |
| 110 | + }; |
| 111 | + |
| 112 | + useEffect(() => { |
| 113 | + const unsubscribe = onAuthStateChanged(auth, async (user) => { |
| 114 | + try { |
| 115 | + if (!user) { |
| 116 | + // No authenticated user - redirect to login |
| 117 | + router.push('/'); |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + // Check if email is verified |
| 122 | + if (!user.emailVerified) { |
| 123 | + router.push(`/verify?email=${encodeURIComponent(user.email || '')}`); |
| 124 | + return; |
| 125 | + } |
| 126 | + |
| 127 | + // Get user role from backend |
| 128 | + const userRole = await getUserRole(user); |
| 129 | + |
| 130 | + if (!userRole) { |
| 131 | + // Could not get user role - redirect to login |
| 132 | + router.push('/'); |
| 133 | + return; |
| 134 | + } |
| 135 | + |
| 136 | + if (!memoizedAllowedRoles.includes(userRole)) { |
| 137 | + // User doesn't have required role - redirect to unauthorized |
| 138 | + router.push('/unauthorized'); |
| 139 | + return; |
| 140 | + } |
| 141 | + |
| 142 | + // User is authorized |
| 143 | + setAuthState({ loading: false, authorized: true }); |
| 144 | + } catch (error) { |
| 145 | + console.error('Auth guard error:', error); |
| 146 | + router.push('/'); |
| 147 | + } |
| 148 | + }); |
| 149 | + |
| 150 | + return () => unsubscribe(); |
| 151 | + }, [router, memoizedAllowedRoles]); |
| 152 | + |
| 153 | + return authState; |
| 154 | +}; |
| 155 | + |
| 156 | +/** |
| 157 | + * Clear all cached user role data from session storage |
| 158 | + * Call this function when the user logs out |
| 159 | + */ |
| 160 | +export const clearAuthCache = (): void => { |
| 161 | + try { |
| 162 | + Object.keys(sessionStorage).forEach((key) => { |
| 163 | + if (key.startsWith('userRole_')) { |
| 164 | + sessionStorage.removeItem(key); |
| 165 | + } |
| 166 | + }); |
| 167 | + } catch (error) { |
| 168 | + // Session storage might not be available (e.g., in SSR or private browsing) |
| 169 | + console.warn('[useAuthGuard] Could not clear auth cache:', error); |
| 170 | + } |
| 171 | +}; |
0 commit comments