Files
auditly/src/contexts/AuthContext.tsx

262 lines
10 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
import { demoStorage } from '../services/demoStorage';
import { API_URL } from '../constants';
interface AuthContextType {
user: User | null;
loading: boolean;
signInWithGoogle: () => Promise<void>;
signOutUser: () => Promise<void>;
signInWithEmail: (email: string, password: string) => Promise<void>;
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
if (isFirebaseConfigured) {
// Firebase mode: Set up proper Firebase auth state listener
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
console.log('Firebase auth state changed:', firebaseUser?.email);
if (firebaseUser) {
setUser(firebaseUser);
} else {
// Check for OTP session as fallback
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring OTP session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
} else {
setUser(null);
}
}
setLoading(false);
});
return unsubscribe;
} else {
// Demo/OTP mode: Check localStorage for persisted session
console.log('Checking for persisted OTP session');
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
} else {
setUser(null);
}
setLoading(false);
return () => { };
}
}, []);
const signInWithGoogle = async () => {
if (!isFirebaseConfigured) {
// No-op in demo mode
return;
}
await signInWithPopup(auth, googleProvider);
};
const signOutUser = async () => {
try {
// Sign out from Firebase if configured and user is signed in via Firebase
if (isFirebaseConfigured && auth.currentUser) {
await signOut(auth);
console.log('Firebase signout completed');
}
} catch (error) {
console.error('Firebase signout error:', error);
}
// Always clear all local session data
localStorage.removeItem('auditly_demo_session');
localStorage.removeItem('auditly_auth_token');
localStorage.removeItem('auditly_selected_org');
sessionStorage.clear();
setUser(null);
console.log('User signed out and all sessions cleared');
};
const signInWithEmail = async (email: string, password: string) => {
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
if (!isFirebaseConfigured) {
console.log('Demo mode: authenticating user', email);
const existingUser = demoStorage.getUserByEmail(email);
if (existingUser) {
// Verify password
if (demoStorage.verifyPassword(password, existingUser.passwordHash)) {
const mockUser = {
uid: existingUser.uid,
email: existingUser.email,
displayName: existingUser.displayName
} as unknown as User;
setUser(mockUser);
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
console.log('Demo login successful for:', email);
} else {
throw new Error('Invalid password');
}
} else {
throw new Error('User not found. Please sign up first.');
}
return;
}
try {
console.log('Attempting Firebase auth');
await signInWithEmailAndPassword(auth, email, password);
} catch (e: any) {
const code = e?.code || '';
console.error('Firebase Auth Error:', code, e?.message);
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
setUser(mock);
return;
}
throw e;
}
};
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
if (!isFirebaseConfigured) {
console.log('Demo mode: creating new user', email);
// Check if user already exists
const existingUser = demoStorage.getUserByEmail(email);
if (existingUser) {
throw new Error('User already exists with this email');
}
// Create new user
const uid = `demo-${btoa(email).slice(0, 8)}`;
const newUser = {
uid,
email,
displayName: displayName || email.split('@')[0],
passwordHash: demoStorage.hashPassword(password)
};
demoStorage.saveUser(newUser);
const mockUser = {
uid: newUser.uid,
email: newUser.email,
displayName: newUser.displayName
} as unknown as User;
setUser(mockUser);
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
console.log('Demo signup successful for:', email);
return;
}
try {
const cred = await createUserWithEmailAndPassword(auth, email, password);
if (displayName) {
try { await updateProfile(cred.user, { displayName }); } catch { }
}
} catch (e: any) {
const code = e?.code || '';
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
setUser(mock);
return;
}
throw e;
}
};
const sendOTP = async (email: string, inviteCode?: string) => {
const response = await fetch(`${API_URL}/sendOTP`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, inviteCode })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to send OTP');
}
return response.json();
};
const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
const response = await fetch(`${API_URL}/verifyOTP`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otp, inviteCode })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to verify OTP');
}
const data = await response.json();
// Set user in auth context
const mockUser = {
uid: data.user.uid,
email: data.user.email,
displayName: data.user.displayName,
emailVerified: true
} as unknown as User;
setUser(mockUser);
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
localStorage.setItem('auditly_auth_token', data.token);
return data;
};
return (
<AuthContext.Provider value={{
user,
loading,
signInWithGoogle,
signOutUser,
signInWithEmail,
signUpWithEmail,
sendOTP,
verifyOTP,
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
};