import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react' import type { User, Session, AuthError, Factor } from '@supabase/supabase-js' import { supabase } from '../lib/supabase' import { api } from '../api/client' interface UserProfile { id: string email: string displayName: string | null isAdmin: boolean } interface MfaState { requiresMfa: boolean factorId: string | null enrolledFactors: Factor[] } interface AuthState { user: User | null session: Session | null loading: boolean isAdmin: boolean mfa: MfaState } interface MfaEnrollResult { factorId: string qrCode: string secret: string recoveryCodes?: string[] } interface AuthContextValue extends AuthState { signInWithEmail: ( email: string, password: string ) => Promise<{ error: AuthError | null; requiresMfa?: boolean }> signUpWithEmail: (email: string, password: string) => Promise<{ error: AuthError | null }> signInWithGoogle: () => Promise<{ error: AuthError | null }> signInWithDiscord: () => Promise<{ error: AuthError | null }> signOut: () => Promise verifyMfa: (code: string) => Promise<{ error: AuthError | null }> enrollMfa: () => Promise<{ data: MfaEnrollResult | null; error: AuthError | null }> verifyMfaEnrollment: (factorId: string, code: string) => Promise<{ error: AuthError | null }> unenrollMfa: (factorId: string) => Promise<{ error: AuthError | null }> isOAuthUser: boolean refreshMfaState: () => Promise } const AuthContext = createContext(null) async function syncUserProfile(session: Session | null): Promise { if (!session) return false try { const profile = await api.post('/users/me', {}) return profile.isAdmin } catch { return false } } async function getMfaState(): Promise { const defaultState: MfaState = { requiresMfa: false, factorId: null, enrolledFactors: [] } try { const { data: aalData } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel() if (!aalData) return defaultState const { data: factorsData } = await supabase.auth.mfa.listFactors() const verifiedFactors = factorsData?.totp?.filter((f) => f.status === 'verified') ?? [] const requiresMfa = aalData.currentLevel === 'aal1' && aalData.nextLevel === 'aal2' const factorId = requiresMfa ? (verifiedFactors[0]?.id ?? null) : null return { requiresMfa, factorId, enrolledFactors: verifiedFactors } } catch { return defaultState } } export function AuthProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState({ user: null, session: null, loading: true, isAdmin: false, mfa: { requiresMfa: false, factorId: null, enrolledFactors: [] }, }) const refreshMfaState = useCallback(async () => { const mfa = await getMfaState() setState((prev) => ({ ...prev, mfa })) }, []) useEffect(() => { supabase.auth.getSession().then(async ({ data: { session } }) => { const [isAdmin, mfa] = await Promise.all([syncUserProfile(session), getMfaState()]) setState({ user: session?.user ?? null, session, loading: false, isAdmin, mfa }) }) const { data: { subscription }, } = supabase.auth.onAuthStateChange(async (_event, session) => { const [isAdmin, mfa] = await Promise.all([syncUserProfile(session), getMfaState()]) setState({ user: session?.user ?? null, session, loading: false, isAdmin, mfa }) }) return () => subscription.unsubscribe() }, []) const signInWithEmail = useCallback(async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) return { error } const mfa = await getMfaState() setState((prev) => ({ ...prev, mfa })) return { error: null, requiresMfa: mfa.requiresMfa } }, []) const signUpWithEmail = useCallback(async (email: string, password: string) => { const { error } = await supabase.auth.signUp({ email, password }) return { error } }, []) const signInWithGoogle = useCallback(async () => { const { error } = await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback` }, }) return { error } }, []) const signInWithDiscord = useCallback(async () => { const { error } = await supabase.auth.signInWithOAuth({ provider: 'discord', options: { redirectTo: `${window.location.origin}/auth/callback` }, }) return { error } }, []) const signOut = useCallback(async () => { await supabase.auth.signOut() }, []) const verifyMfa = useCallback( async (code: string) => { const factorId = state.mfa.factorId if (!factorId) { return { error: { message: 'No MFA factor found' } as AuthError } } const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ factorId, }) if (challengeError) return { error: challengeError } const { error } = await supabase.auth.mfa.verify({ factorId, challengeId: challengeData.id, code, }) if (!error) { const mfa = await getMfaState() setState((prev) => ({ ...prev, mfa })) } return { error } }, [state.mfa.factorId] ) const enrollMfa = useCallback(async () => { const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp' }) if (error || !data) { return { data: null, error } } return { data: { factorId: data.id, qrCode: data.totp.qr_code, secret: data.totp.secret, }, error: null, } }, []) const verifyMfaEnrollment = useCallback(async (factorId: string, code: string) => { const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ factorId, }) if (challengeError) return { error: challengeError } const { error } = await supabase.auth.mfa.verify({ factorId, challengeId: challengeData.id, code, }) if (!error) { const mfa = await getMfaState() setState((prev) => ({ ...prev, mfa })) } return { error } }, []) const unenrollMfa = useCallback(async (factorId: string) => { const { error } = await supabase.auth.mfa.unenroll({ factorId }) if (!error) { const mfa = await getMfaState() setState((prev) => ({ ...prev, mfa })) } return { error } }, []) const isOAuthUser = useMemo(() => { if (!state.user) return false const provider = state.user.app_metadata?.['provider'] return provider === 'google' || provider === 'discord' }, [state.user]) const value = useMemo( () => ({ ...state, signInWithEmail, signUpWithEmail, signInWithGoogle, signInWithDiscord, signOut, verifyMfa, enrollMfa, verifyMfaEnrollment, unenrollMfa, isOAuthUser, refreshMfaState, }), [ state, signInWithEmail, signUpWithEmail, signInWithGoogle, signInWithDiscord, signOut, verifyMfa, enrollMfa, verifyMfaEnrollment, unenrollMfa, isOAuthUser, refreshMfaState, ] ) return {children} } export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } return context }