Spaces:
Sleeping
Sleeping
| import { createContext, useContext, useState, useEffect, useRef } from 'react'; | |
| import { | |
| createUserWithEmailAndPassword, | |
| signInWithEmailAndPassword, | |
| signOut, | |
| onAuthStateChanged, | |
| updateProfile, | |
| sendPasswordResetEmail, | |
| GoogleAuthProvider, | |
| signInWithPopup, | |
| signInWithRedirect, | |
| getRedirectResult, | |
| sendEmailVerification, | |
| } from 'firebase/auth'; | |
| import { doc, setDoc, getDoc, serverTimestamp } from 'firebase/firestore'; | |
| import { auth, db } from '../services/firebase'; | |
| import { COLLECTIONS, ROLES } from '../utils/constants'; | |
| const AuthContext = createContext(null); | |
| export function useAuth() { | |
| const context = useContext(AuthContext); | |
| if (!context) { | |
| throw new Error('useAuth must be used within an AuthProvider'); | |
| } | |
| return context; | |
| } | |
| export function AuthProvider({ children }) { | |
| const [currentUser, setCurrentUser] = useState(null); | |
| const [userProfile, setUserProfile] = useState(null); | |
| const [loading, setLoading] = useState(true); | |
| // Fetch user profile from Firestore | |
| async function fetchUserProfile(user) { | |
| if (!user) { | |
| setUserProfile(null); | |
| return null; | |
| } | |
| try { | |
| const docRef = doc(db, COLLECTIONS.USERS, user.uid); | |
| const docSnap = await getDoc(docRef); | |
| if (docSnap.exists()) { | |
| const profile = { uid: user.uid, ...docSnap.data() }; | |
| setUserProfile(profile); | |
| return profile; | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error('Error fetching user profile:', error); | |
| return null; | |
| } | |
| } | |
| // Register with email/password | |
| async function register(email, password, displayName, phoneNumber = '') { | |
| const { user } = await createUserWithEmailAndPassword(auth, email, password); | |
| // Update Firebase Auth profile | |
| await updateProfile(user, { displayName }); | |
| // Create Firestore user document | |
| const userDoc = { | |
| email, | |
| displayName, | |
| phoneNumber, | |
| role: ROLES.CITIZEN, | |
| createdAt: serverTimestamp(), | |
| isWhitelisted: false, | |
| profileImage: '', | |
| status: 'active', | |
| }; | |
| await setDoc(doc(db, COLLECTIONS.USERS, user.uid), userDoc); | |
| setUserProfile({ uid: user.uid, ...userDoc }); | |
| // Send verification email | |
| await sendEmailVerification(user); | |
| return user; | |
| } | |
| // Login with email/password | |
| async function login(email, password) { | |
| const { user } = await signInWithEmailAndPassword(auth, email, password); | |
| await fetchUserProfile(user); | |
| return user; | |
| } | |
| // Create or fetch Google user profile in Firestore | |
| async function handleGoogleUser(user) { | |
| const docRef = doc(db, COLLECTIONS.USERS, user.uid); | |
| const docSnap = await getDoc(docRef); | |
| if (!docSnap.exists()) { | |
| // First time Google sign-in — create profile | |
| const userDoc = { | |
| email: user.email, | |
| displayName: user.displayName || '', | |
| phoneNumber: user.phoneNumber || '', | |
| role: ROLES.CITIZEN, | |
| createdAt: serverTimestamp(), | |
| isWhitelisted: false, | |
| profileImage: user.photoURL || '', | |
| status: 'active', | |
| }; | |
| await setDoc(docRef, userDoc); | |
| setUserProfile({ uid: user.uid, ...userDoc }); | |
| } else { | |
| await fetchUserProfile(user); | |
| } | |
| return user; | |
| } | |
| // Google sign-in — try popup first, fall back to redirect | |
| async function loginWithGoogle() { | |
| const provider = new GoogleAuthProvider(); | |
| try { | |
| const { user } = await signInWithPopup(auth, provider); | |
| return await handleGoogleUser(user); | |
| } catch (error) { | |
| // If popup blocked by COOP or browser policy, use redirect | |
| if ( | |
| error.code === 'auth/popup-blocked' || | |
| error.code === 'auth/popup-closed-by-user' || | |
| error.code === 'auth/cancelled-popup-request' || | |
| error.message?.includes('Cross-Origin-Opener-Policy') | |
| ) { | |
| await signInWithRedirect(auth, provider); | |
| return null; // Will be handled by getRedirectResult on reload | |
| } | |
| throw error; | |
| } | |
| } | |
| // Logout | |
| async function logout() { | |
| await signOut(auth); | |
| setCurrentUser(null); | |
| setUserProfile(null); | |
| } | |
| // Reset password | |
| async function resetPassword(email) { | |
| await sendPasswordResetEmail(auth, email); | |
| } | |
| // Resend Verification Email | |
| async function resendVerification() { | |
| if (currentUser) { | |
| await sendEmailVerification(currentUser); | |
| } | |
| } | |
| // Check role access | |
| function hasRole(requiredRole) { | |
| if (!userProfile) return false; | |
| const roleHierarchy = [ | |
| ROLES.CITIZEN, | |
| ROLES.VOLUNTEER_COORDINATOR, | |
| ROLES.MANAGER, | |
| ROLES.CONTENT_MANAGER, | |
| ROLES.ADMIN, | |
| ]; | |
| const userRoleIndex = roleHierarchy.indexOf(userProfile.role); | |
| const requiredRoleIndex = roleHierarchy.indexOf(requiredRole); | |
| return userRoleIndex >= requiredRoleIndex; | |
| } | |
| // Check specific role (exact match) | |
| function isRole(role) { | |
| return userProfile?.role === role; | |
| } | |
| // Handle redirect result (for Google sign-in redirect fallback) | |
| const redirectHandled = useRef(false); | |
| useEffect(() => { | |
| if (redirectHandled.current) return; | |
| redirectHandled.current = true; | |
| getRedirectResult(auth) | |
| .then(async (result) => { | |
| if (result?.user) { | |
| await handleGoogleUser(result.user); | |
| } | |
| }) | |
| .catch((error) => { | |
| console.error('Redirect result error:', error); | |
| }); | |
| }, []); | |
| // Auth state listener | |
| useEffect(() => { | |
| const unsubscribe = onAuthStateChanged(auth, async (user) => { | |
| setCurrentUser(user); | |
| if (user) { | |
| await fetchUserProfile(user); | |
| } else { | |
| setUserProfile(null); | |
| } | |
| setLoading(false); | |
| }); | |
| return unsubscribe; | |
| }, []); | |
| const value = { | |
| currentUser, | |
| userProfile, | |
| loading, | |
| register, | |
| login, | |
| loginWithGoogle, | |
| logout, | |
| resetPassword, | |
| resendVerification, | |
| hasRole, | |
| isRole, | |
| fetchUserProfile, | |
| }; | |
| return ( | |
| <AuthContext.Provider value={value}> | |
| {children} | |
| </AuthContext.Provider> | |
| ); | |
| } | |
| export default AuthContext; | |