- Add detailed report viewing with full-screen ReportDetail component for both company and employee reports - Fix company wiki to display onboarding Q&A in card format matching Figma designs - Exclude company owners from employee submission counts (owners contribute to wiki, not employee data) - Fix employee report generation to include company context (wiki + company report + employee answers) - Fix company report generation to use filtered employee submissions only - Add proper error handling for submission data format variations - Update Firebase functions to use gpt-4o model instead of deprecated gpt-4.1 - Fix UI syntax errors and improve report display functionality - Add comprehensive logging for debugging report generation flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
276 lines
11 KiB
TypeScript
276 lines
11 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>;
|
|
signInWithOTP: (token: string, userData: any) => 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;
|
|
};
|
|
|
|
const signInWithOTP = async (token: string, userData: any) => {
|
|
const mockUser = {
|
|
uid: userData.uid,
|
|
email: userData.email,
|
|
displayName: userData.displayName,
|
|
emailVerified: true
|
|
} as unknown as User;
|
|
|
|
setUser(mockUser);
|
|
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
|
localStorage.setItem('auditly_auth_token', token);
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{
|
|
user,
|
|
loading,
|
|
signInWithGoogle,
|
|
signOutUser,
|
|
signInWithEmail,
|
|
signUpWithEmail,
|
|
sendOTP,
|
|
verifyOTP,
|
|
signInWithOTP
|
|
}}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = () => {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
return ctx;
|
|
};
|