mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 17:11:13 -05:00
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
/**
|
|
* MFA Step-Up Context
|
|
*
|
|
* Provides global MFA step-up functionality that automatically
|
|
* prompts users for MFA verification when operations require AAL2
|
|
*/
|
|
|
|
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { MFAStepUpModal } from '@/components/auth/MFAStepUpModal';
|
|
import { getEnrolledFactors } from '@/lib/authService';
|
|
import { isAAL2PolicyError, getAAL2ErrorMessage, MFACancelledError } from '@/lib/aalErrorDetection';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
interface MFAStepUpContextType {
|
|
/**
|
|
* Wrap an operation that may require AAL2 verification
|
|
* If operation fails due to AAL2, prompts user for MFA and retries
|
|
*/
|
|
requireAAL2: <T>(operation: () => Promise<T>, errorMessage?: string) => Promise<T>;
|
|
|
|
/**
|
|
* Check if error is AAL2-related
|
|
*/
|
|
isAAL2Error: (error: unknown) => boolean;
|
|
}
|
|
|
|
const MFAStepUpContext = createContext<MFAStepUpContextType | undefined>(undefined);
|
|
|
|
interface MFAStepUpProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function MFAStepUpProvider({ children }: MFAStepUpProviderProps) {
|
|
const { session } = useAuth();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [factorId, setFactorId] = useState<string>('');
|
|
const [pendingOperation, setPendingOperation] = useState<(() => Promise<any>) | null>(null);
|
|
const [pendingResolve, setPendingResolve] = useState<((value: any) => void) | null>(null);
|
|
const [pendingReject, setPendingReject] = useState<((error: any) => void) | null>(null);
|
|
|
|
/**
|
|
* Handle successful MFA verification
|
|
* Retries the pending operation with upgraded AAL2 session
|
|
*/
|
|
const handleMFASuccess = useCallback(async () => {
|
|
setModalOpen(false);
|
|
|
|
if (!pendingOperation || !pendingResolve || !pendingReject) {
|
|
// Invalid state - missing pending operations
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.log('[MFAStepUp] Retrying operation after AAL2 upgrade');
|
|
const result = await pendingOperation();
|
|
pendingResolve(result);
|
|
} catch (error) {
|
|
// Operation failed after AAL2 - will be handled by caller
|
|
pendingReject(error);
|
|
} finally {
|
|
// Clean up
|
|
setPendingOperation(null);
|
|
setPendingResolve(null);
|
|
setPendingReject(null);
|
|
}
|
|
}, [pendingOperation, pendingResolve, pendingReject]);
|
|
|
|
/**
|
|
* Handle MFA cancellation
|
|
*/
|
|
const handleMFACancel = useCallback(() => {
|
|
setModalOpen(false);
|
|
|
|
if (pendingReject) {
|
|
pendingReject(new MFACancelledError());
|
|
}
|
|
|
|
// Clean up
|
|
setPendingOperation(null);
|
|
setPendingResolve(null);
|
|
setPendingReject(null);
|
|
}, [pendingReject]);
|
|
|
|
/**
|
|
* Wrap an operation with AAL2 requirement checking
|
|
*/
|
|
const requireAAL2 = useCallback(
|
|
async <T,>(operation: () => Promise<T>, customErrorMessage?: string): Promise<T> => {
|
|
try {
|
|
// Try the operation first
|
|
return await operation();
|
|
} catch (error) {
|
|
// Check if error is AAL2-related
|
|
if (!isAAL2PolicyError(error)) {
|
|
// Not an AAL2 error, re-throw
|
|
throw error;
|
|
}
|
|
|
|
logger.log('[MFAStepUp] AAL2 error detected, prompting for MFA', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
|
|
// Get enrolled factors
|
|
const factors = await getEnrolledFactors();
|
|
|
|
if (factors.length === 0) {
|
|
// No MFA set up
|
|
throw new Error('MFA is not set up for your account');
|
|
}
|
|
|
|
const factor = factors[0];
|
|
setFactorId(factor.id);
|
|
|
|
// Return a promise that resolves when MFA is verified
|
|
return new Promise<T>((resolve, reject) => {
|
|
setPendingOperation(() => operation);
|
|
setPendingResolve(() => resolve);
|
|
setPendingReject(() => reject);
|
|
setModalOpen(true);
|
|
});
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const value: MFAStepUpContextType = {
|
|
requireAAL2,
|
|
isAAL2Error: isAAL2PolicyError,
|
|
};
|
|
|
|
return (
|
|
<MFAStepUpContext.Provider value={value}>
|
|
{children}
|
|
<MFAStepUpModal
|
|
open={modalOpen}
|
|
factorId={factorId}
|
|
onSuccess={handleMFASuccess}
|
|
onCancel={handleMFACancel}
|
|
/>
|
|
</MFAStepUpContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to access MFA step-up functionality
|
|
*/
|
|
export function useMFAStepUp(): MFAStepUpContextType {
|
|
const context = useContext(MFAStepUpContext);
|
|
|
|
if (!context) {
|
|
throw new Error('useMFAStepUp must be used within MFAStepUpProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|