312 lines
8.3 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getCurrentUser, signIn, signOut as signOutRequest } from '@/shared/api/auth';
import { AuthExpiredError } from '@/shared/api/httpClient';
import { AUTH_EXPIRED_EVENT } from '@/shared/constants/auth';
import {
AUTH_MODAL_SIGNIN_CLOSE_DELAY_MS,
AUTH_MODAL_SIGNUP_CLOSE_DELAY_MS,
} from '@/shared/constants/auth';
import { APP_ROUTE_PATHS } from '@/shared/constants/routes';
import { CurrentUser } from '@/shared/types/auth';
import { UserProfile } from '@/shared/types/app';
import { toUserProfile } from '@/business/auth/mappers';
import { useCampusCatalog } from '@/business/campuses/hooks';
import { AuthActionResult, AuthSessionState } from '@/business/auth/types';
import { getErrorMessage, getOptionalErrorMessage } from '@/shared/errors/errorMessages';
import type {
AuthModalDraft,
AuthModalMode,
AuthModalWorkflowInput,
AuthSignupStep,
} from '@/business/auth/types';
import {
getNextSignupStep,
getPreviousSignupStep,
getSignupCampusName,
validateSignupStepOne,
} from '@/business/auth/selectors';
import type { CampusId, UserRole } from '@/shared/types/app';
export function useAuthSession(): AuthSessionState {
const navigate = useNavigate();
const [user, setUser] = useState<CurrentUser | null>(null);
const [loading, setLoading] = useState(true);
const clearSession = useCallback(() => {
setUser(null);
}, []);
const redirectToLogin = useCallback(() => {
navigate(APP_ROUTE_PATHS.login, { replace: true });
}, [navigate]);
const expireSession = useCallback(() => {
clearSession();
redirectToLogin();
}, [clearSession, redirectToLogin]);
useEffect(() => {
window.addEventListener(AUTH_EXPIRED_EVENT, expireSession);
return () => {
window.removeEventListener(AUTH_EXPIRED_EVENT, expireSession);
};
}, [expireSession]);
useEffect(() => {
let isActive = true;
void getCurrentUser()
.then((currentUser) => {
if (isActive) {
setUser(currentUser);
}
})
.catch((error: unknown) => {
if (isActive) {
clearSession();
if (error instanceof AuthExpiredError) {
expireSession();
}
}
})
.finally(() => {
if (isActive) {
setLoading(false);
}
});
return () => {
isActive = false;
};
}, [clearSession, expireSession]);
const handleSignIn = useCallback(async (email: string, password: string): Promise<AuthActionResult> => {
setLoading(true);
try {
const currentUser = await signIn({ email, password });
setUser(currentUser);
return { error: null };
} catch (error) {
clearSession();
if (error instanceof AuthExpiredError) {
expireSession();
}
return { error: getErrorMessage(error, 'Sign in failed') };
} finally {
setLoading(false);
}
}, [clearSession, expireSession]);
const signUp = useCallback(async (): Promise<AuthActionResult> => ({
error:
'Account creation is not available until backend product roles and campus assignment are implemented.',
}), []);
const signOut = useCallback(async (): Promise<AuthActionResult> => {
try {
await signOutRequest();
clearSession();
return { error: null };
} catch (error) {
if (error instanceof AuthExpiredError) {
expireSession();
return { error: null };
}
return { error: getErrorMessage(error, 'Sign out failed') };
}
}, [clearSession, expireSession]);
const updateProfile = useCallback(async (): Promise<AuthActionResult> => ({
error: 'Profile updates are handled by the profile page.',
}), []);
const refreshUser = useCallback(async (): Promise<void> => {
try {
const currentUser = await getCurrentUser();
setUser(currentUser);
} catch (error) {
if (error instanceof AuthExpiredError) {
expireSession();
}
}
}, [expireSession]);
const profile: UserProfile | null = useMemo(() => (user ? toUserProfile(user) : null), [user]);
return {
user,
profile,
loading,
signIn: handleSignIn,
signUp,
signOut,
updateProfile,
refreshUser,
isAuthenticated: Boolean(user),
};
}
const initialAuthModalDraft: AuthModalDraft = {
email: '',
password: '',
confirmPassword: '',
fullName: '',
role: 'teacher',
campus: '',
showPassword: false,
};
export function useAuthModalWorkflow({
signIn: signInAction,
signUp: signUpAction,
onClose,
}: AuthModalWorkflowInput) {
const campusCatalog = useCampusCatalog();
const [mode, setMode] = useState<AuthModalMode>('signin');
const [signupStep, setSignupStep] = useState<AuthSignupStep>(1);
const [draft, setDraft] = useState<AuthModalDraft>(initialAuthModalDraft);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const resetForm = useCallback(() => {
setDraft(initialAuthModalDraft);
setError(null);
setSuccess(null);
setMode('signin');
setSignupStep(1);
}, []);
const closeAfterSuccess = useCallback((delayMs: number) => {
window.setTimeout(() => {
onClose();
resetForm();
}, delayMs);
}, [onClose, resetForm]);
const updateDraft = useCallback((patch: Partial<AuthModalDraft>) => {
setDraft((currentDraft) => ({ ...currentDraft, ...patch }));
}, []);
const handleSignIn = useCallback(async () => {
setError(null);
setSuccess(null);
setLoading(true);
const result = await signInAction(draft.email, draft.password);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
setSuccess('Signed in successfully!');
closeAfterSuccess(AUTH_MODAL_SIGNIN_CLOSE_DELAY_MS);
}, [closeAfterSuccess, draft.email, draft.password, signInAction]);
const handleSignUp = useCallback(async () => {
setError(null);
setSuccess(null);
const campusName = getSignupCampusName(draft.campus, campusCatalog.campuses);
if (!campusName) {
setError(campusCatalog.error ? 'Campus list is unavailable. Please try again.' : 'Please select your campus mascot');
return;
}
setLoading(true);
const result = await signUpAction(draft.email, draft.password, draft.fullName, draft.role, campusName);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
setSuccess('Account created! You are now signed in.');
closeAfterSuccess(AUTH_MODAL_SIGNUP_CLOSE_DELAY_MS);
}, [
campusCatalog.campuses,
campusCatalog.error,
closeAfterSuccess,
draft.campus,
draft.email,
draft.fullName,
draft.password,
draft.role,
signUpAction,
]);
const goToNextStep = useCallback(() => {
setError(null);
if (signupStep === 1) {
const validationError = validateSignupStepOne(draft);
if (validationError) {
setError(validationError);
return;
}
setSignupStep(2);
return;
}
if (signupStep === 2) {
setSignupStep(3);
return;
}
void handleSignUp();
}, [draft, handleSignUp, signupStep]);
const goToPreviousStep = useCallback(() => {
setError(null);
setSignupStep((currentStep) => getPreviousSignupStep(currentStep));
}, []);
const handleClose = useCallback(() => {
resetForm();
onClose();
}, [onClose, resetForm]);
const switchMode = useCallback((nextMode: AuthModalMode) => {
setMode(nextMode);
setError(null);
setSuccess(null);
setSignupStep(1);
}, []);
const selectedCampusId = draft.campus;
return {
state: {
mode,
signupStep,
draft,
loading,
error,
success,
selectedCampusId,
campuses: campusCatalog.campuses,
campusesLoading: campusCatalog.isLoading,
campusesError: getOptionalErrorMessage(campusCatalog.error),
},
actions: {
updateDraft,
setRole: (role: UserRole) => updateDraft({ role }),
setCampus: (campus: CampusId | '') => updateDraft({ campus }),
setShowPassword: (showPassword: boolean) => updateDraft({ showPassword }),
handleSignIn,
goToNextStep,
goToPreviousStep,
handleClose,
switchMode,
getNextSignupStep,
},
};
}