mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 09:31:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
310
src-old/lib/authService.ts
Normal file
310
src-old/lib/authService.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Centralized Authentication Service
|
||||
* Handles all authentication flows with consistent AAL checking and MFA verification
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
import type {
|
||||
AALLevel,
|
||||
MFAFactor,
|
||||
CheckAalResult,
|
||||
AuthServiceResponse,
|
||||
MFAChallengeResult
|
||||
} from '@/types/auth';
|
||||
import { setStepUpRequired, setAuthMethod, clearAllAuthFlags } from './sessionFlags';
|
||||
import { logger } from './logger';
|
||||
import { getErrorMessage, handleNonCriticalError } from './errorHandler';
|
||||
|
||||
/**
|
||||
* Extract AAL level from session using Supabase API
|
||||
* Always returns ground truth from server, not cached session data
|
||||
*/
|
||||
export async function getSessionAal(session: Session | null): Promise<AALLevel> {
|
||||
if (!session) {
|
||||
logger.log('[AuthService] No session, returning aal1');
|
||||
return 'aal1';
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
||||
|
||||
logger.log('[AuthService] getSessionAal result', {
|
||||
hasData: !!data,
|
||||
currentLevel: data?.currentLevel,
|
||||
nextLevel: data?.nextLevel,
|
||||
error: error?.message
|
||||
});
|
||||
|
||||
if (error) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Get session AAL',
|
||||
});
|
||||
return 'aal1';
|
||||
}
|
||||
|
||||
const level = (data.currentLevel as AALLevel) || 'aal1';
|
||||
logger.log('[AuthService] Returning AAL', { level });
|
||||
return level;
|
||||
} catch (error: unknown) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Get session AAL exception',
|
||||
});
|
||||
return 'aal1';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enrolled MFA factors for the current user
|
||||
*/
|
||||
export async function getEnrolledFactors(): Promise<MFAFactor[]> {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.mfa.listFactors();
|
||||
|
||||
if (error) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'List MFA factors',
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return (data?.totp || [])
|
||||
.filter(f => f.status === 'verified')
|
||||
.map(f => ({
|
||||
id: f.id,
|
||||
factor_type: 'totp' as const,
|
||||
status: 'verified' as const,
|
||||
friendly_name: f.friendly_name,
|
||||
created_at: f.created_at,
|
||||
updated_at: f.updated_at,
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'List MFA factors exception',
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user needs AAL step-up
|
||||
* Returns detailed information about enrollment and current AAL level
|
||||
*/
|
||||
export async function checkAalStepUp(session: Session | null): Promise<CheckAalResult> {
|
||||
if (!session?.user) {
|
||||
return {
|
||||
needsStepUp: false,
|
||||
hasMfaEnrolled: false,
|
||||
currentLevel: 'aal1',
|
||||
hasEnrolledFactors: false,
|
||||
};
|
||||
}
|
||||
|
||||
const [currentLevel, factors] = await Promise.all([
|
||||
getSessionAal(session),
|
||||
getEnrolledFactors(),
|
||||
]);
|
||||
|
||||
const hasEnrolledFactors = factors.length > 0;
|
||||
const needsStepUp = hasEnrolledFactors && currentLevel === 'aal1';
|
||||
|
||||
return {
|
||||
needsStepUp,
|
||||
hasMfaEnrolled: hasEnrolledFactors,
|
||||
currentLevel,
|
||||
hasEnrolledFactors,
|
||||
factorId: factors[0]?.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify MFA is required for a user based on their role
|
||||
*/
|
||||
export async function verifyMfaRequired(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('user_roles')
|
||||
.select('role')
|
||||
.eq('user_id', userId)
|
||||
.in('role', ['admin', 'moderator']);
|
||||
|
||||
if (error) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Verify MFA required',
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return (data?.length || 0) > 0;
|
||||
} catch (error: unknown) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Verify MFA required exception',
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post-authentication flow for all auth methods
|
||||
* Detects if MFA step-up is needed and redirects accordingly
|
||||
*/
|
||||
export async function handlePostAuthFlow(
|
||||
session: Session,
|
||||
authMethod: 'password' | 'oauth' | 'magiclink'
|
||||
): Promise<AuthServiceResponse<{ shouldRedirect: boolean; redirectTo?: string }>> {
|
||||
try {
|
||||
// Store auth method for audit logging
|
||||
setAuthMethod(authMethod);
|
||||
|
||||
// Check if step-up is needed
|
||||
const aalCheck = await checkAalStepUp(session);
|
||||
|
||||
if (aalCheck.needsStepUp) {
|
||||
logger.info('[AuthService] MFA step-up required', {
|
||||
authMethod,
|
||||
currentAal: aalCheck.currentLevel
|
||||
});
|
||||
|
||||
// Set flag and redirect to step-up page
|
||||
setStepUpRequired(true, window.location.pathname);
|
||||
|
||||
// Log audit event
|
||||
await logAuthEvent(session.user.id, 'mfa_step_up_required', {
|
||||
auth_method: authMethod,
|
||||
current_aal: aalCheck.currentLevel,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
shouldRedirect: true,
|
||||
redirectTo: '/auth/mfa-step-up',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Log successful authentication
|
||||
await logAuthEvent(session.user.id, 'authentication_success', {
|
||||
auth_method: authMethod,
|
||||
aal: aalCheck.currentLevel,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
shouldRedirect: false,
|
||||
},
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Handle post-auth flow',
|
||||
metadata: { authMethod },
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify MFA challenge was successful and session upgraded to AAL2
|
||||
*/
|
||||
export async function verifyMfaUpgrade(session: Session | null): Promise<MFAChallengeResult> {
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No session found',
|
||||
};
|
||||
}
|
||||
|
||||
const currentAal = await getSessionAal(session);
|
||||
|
||||
if (currentAal !== 'aal2') {
|
||||
handleNonCriticalError(new Error('MFA verification failed'), {
|
||||
action: 'Verify MFA upgrade',
|
||||
metadata: { expectedAal: 'aal2', actualAal: currentAal },
|
||||
});
|
||||
await logAuthEvent(session.user.id, 'mfa_verification_failed', {
|
||||
expected_aal: 'aal2',
|
||||
actual_aal: currentAal,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to upgrade session to AAL2',
|
||||
newAal: currentAal,
|
||||
};
|
||||
}
|
||||
|
||||
// Log successful upgrade
|
||||
await logAuthEvent(session.user.id, 'mfa_verification_success', {
|
||||
new_aal: currentAal,
|
||||
});
|
||||
|
||||
// Clear auth flags
|
||||
clearAllAuthFlags();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newAal: currentAal,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log authentication event to audit log
|
||||
*/
|
||||
async function logAuthEvent(
|
||||
userId: string,
|
||||
action: string,
|
||||
details: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { error } = await supabase.rpc('log_admin_action', {
|
||||
_admin_user_id: userId,
|
||||
_action: action,
|
||||
_target_user_id: userId,
|
||||
_details: details,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Log auth event',
|
||||
metadata: { eventAction: action, userId },
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Log auth event exception',
|
||||
metadata: { eventAction: action, userId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sign out with proper cleanup
|
||||
*/
|
||||
export async function signOutUser(): Promise<AuthServiceResponse> {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Clear all session flags
|
||||
clearAllAuthFlags();
|
||||
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user