import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { EOtpType, getProfile, login, logout, OtpVerificationPayload, refreshAuthToken, resendOtpCode, resendVerificationCode, setFayidaPass, SetFayidaPasswordPayload, setPassword, SetPasswordPayload, verifyMFAUser, verifyOTPCode, } from "@/shared/services/authService"; import Cookies from "js-cookie"; import { MeDto } from "@/shared/dto/user/meDto"; import { toast } from "sonner"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@/shared/context/AuthContext"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useErrorHandler } from "@/shared/hooks/useErrorHandler"; import { VerifyUserPayload } from "@/record-management/services/api/authService"; import { clearRememberMePreference, getAuthCookieOptions, persistRememberMePreference, setAuthCookies, } from "@/shared/utils/authPersistence"; import { getPositionCookie, setPositionCookie, } from "@/shared/utils/positionCookie"; import { clearComplaintVerification } from "@/complaints/utils/complaintVerificationStorage"; interface LoginPayload { email?: string; phoneNumber?: string; userName?: string; password: string; } type LoginResponse = { mfaRequired: true } | MeDto; // full user details export const useAuthUser = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); const { t } = useTranslation(); const { handleError } = useErrorHandler(t); const delegatedPositionId = Cookies.get("delegatedPositionId"); const currentPositionId = getPositionCookie(); const { setUser, user: contextUser, selectedPositionId, setSelectedPositionId, showOtpModal, setShowOtpModal, rememberMePreference, setRememberMePreference, } = useAuth(); const finalizeAuthenticatedSession = async ({ token, refreshToken, rememberMe, }: { token: string; refreshToken: string; rememberMe: boolean; }) => { const cookieOptions = getAuthCookieOptions(rememberMe); persistRememberMePreference(rememberMe); setAuthCookies({ token, refreshToken, rememberMe }); Cookies.set("i18nextLng", "am", cookieOptions); const { data } = await getProfile(); const userDetails = data as MeDto; Cookies.set( "auth-user", JSON.stringify({ ...userDetails, id: userDetails.id, name: userDetails.name, permissions: userDetails.permissions, roles: userDetails.roles, }), cookieOptions, ); setUser(userDetails); if ((userDetails.employee ?? []).length > 0) { const firstPositionId = userDetails.employee?.[0]?.positions?.[0]?.employeePositionId; if (firstPositionId) { setSelectedPositionId(firstPositionId); setPositionCookie(firstPositionId, cookieOptions); } } queryClient.invalidateQueries({ queryKey: ["authUser"] }); return userDetails; }; const { data: userDetails, isLoading, isFetching, isError, refetch, } = useQuery({ queryKey: [ "authUser", selectedPositionId, delegatedPositionId, currentPositionId, ], queryFn: async () => { const { data } = await getProfile(); return data as MeDto; }, placeholderData: (previousData) => previousData, staleTime: 5 * 60 * 1000, retry: false, enabled: true, }); useEffect(() => { if (userDetails) { setUser(userDetails); const allPositions = (userDetails.employee ?? []).flatMap( (emp) => emp.positions ?? [], ); if (!selectedPositionId && allPositions.length > 0) { const fallbackId = allPositions[0]?.employeePositionId; if (fallbackId) { setSelectedPositionId(fallbackId); setPositionCookie(fallbackId); } } else if (selectedPositionId) { // Self-heal stale cookies that were set to position.id instead of // employeePositionId before this distinction was fixed. const matchesEmployeePositionId = allPositions.some( (pos) => pos.employeePositionId === selectedPositionId, ); if (!matchesEmployeePositionId) { const matchingPosition = allPositions.find( (pos) => pos.id === selectedPositionId, ); if (matchingPosition?.employeePositionId) { setSelectedPositionId(matchingPosition.employeePositionId); setPositionCookie(matchingPosition.employeePositionId); } } } } }, [selectedPositionId, setSelectedPositionId, setUser, userDetails]); const activePositionLookupId = delegatedPositionId ? currentPositionId || selectedPositionId : selectedPositionId; const filteredUserDetailsCandidate: MeDto | undefined = useMemo( () => userDetails ? { ...userDetails, employee: (userDetails.employee ?? []).map((emp) => ({ ...emp, positions: emp.positions.filter( (p) => p.employeePositionId === activePositionLookupId || p.id === activePositionLookupId, ), })), } : undefined, [activePositionLookupId, userDetails], ); const hasResolvedActivePosition = useMemo( () => !!filteredUserDetailsCandidate?.employee?.some( (employee) => employee.positions.length > 0, ), [filteredUserDetailsCandidate], ); const [lastResolvedUserDetails, setLastResolvedUserDetails] = useState(); useEffect(() => { if (filteredUserDetailsCandidate && hasResolvedActivePosition) { setLastResolvedUserDetails(filteredUserDetailsCandidate); } }, [filteredUserDetailsCandidate, hasResolvedActivePosition]); const filteredUserDetails = hasResolvedActivePosition || !isFetching ? filteredUserDetailsCandidate : (lastResolvedUserDetails ?? filteredUserDetailsCandidate); const selectedPosition = useMemo( () => (filteredUserDetails?.employee ?? userDetails?.employee ?? []) .flatMap((employee) => employee.positions) .find( (position) => position.employeePositionId === activePositionLookupId || position.id === activePositionLookupId || position.employeePositionId === selectedPositionId || position.id === selectedPositionId, ), [ activePositionLookupId, filteredUserDetails?.employee, selectedPositionId, userDetails?.employee, ], ); const selectedPositionPermissionKeys = useMemo( () => (filteredUserDetails?.employee ?? []) .flatMap((employee) => employee.positions) .flatMap((position) => position.permissions.map((permission) => permission.key), ), [filteredUserDetails], ); const { mutate: logoutMutate, isError: logoutError, isSuccess: logoutSuccess, } = useMutation({ mutationFn: async (redirectPath?: string) => { await logout(); return redirectPath; }, onSuccess: (redirectPath) => { // Clear user state setUser(null); setSelectedPositionId(null); // Clear all cookies Object.keys(Cookies.get()).forEach((cookieName) => { Cookies.remove(cookieName); }); // Clear only auth-related localStorage keys clearRememberMePreference(); setRememberMePreference(false); clearComplaintVerification(); // Clear all React Query cache to prevent stale data from leaking between users queryClient.clear(); // Show success message toast.success("Logged out successfully"); window.location.replace(redirectPath || "/"); }, onError: (error, redirectPath) => { // Even if API call fails, still clear local data setUser(null); setSelectedPositionId(null); Object.keys(Cookies.get()).forEach((cookieName) => { Cookies.remove(cookieName); }); clearRememberMePreference(); setRememberMePreference(false); clearComplaintVerification(); queryClient.clear(); handleError(error); window.location.replace(redirectPath || "/"); }, }); const { mutate: loginMutate, isPending: isLoggingInMutating, isError: loginError, isSuccess: loginSuccess, } = useMutation({ mutationFn: async ({ payload, rememberMeValue, }: { payload: LoginPayload; rememberMeValue: boolean; }) => { const response = await login(payload); if (response.data.mfaRequired) { return { mfaRequired: true, rememberMeValue }; // no "data" at all } return { data: response.data, rememberMeValue }; }, onSuccess: async (response) => { if ("mfaRequired" in response && response.mfaRequired) { setRememberMePreference(response.rememberMeValue); setShowOtpModal(true); return; } setRememberMePreference(response.rememberMeValue); await finalizeAuthenticatedSession({ token: response.data.token, refreshToken: response.data.refreshToken, rememberMe: response.rememberMeValue, }); toast.success("Login Successful", { description: "Redirecting...", }); navigate("/homepage"); }, onError: (error) => { handleError(error); }, }); const { mutate: setPasswordMutate, isPending: isSettingPassword, isError: setPasswordError, isSuccess: setPasswordSuccess, error: setPassError, } = useMutation({ mutationFn: async (payload: SetPasswordPayload) => { const response = await setPassword(payload); return response.data; }, onSuccess: () => { toast.success("Password updated successfully", { description: "You can now log in with your new password.", }); }, onError: (error) => { handleError(error); }, }); const { mutate: setFayidaPasswordMutate, isPending: isSettingFayidaPassword, isError: setFayidaPasswordError, isSuccess: setFayidaPasswordSuccess, error: setFayidaPassError, } = useMutation({ mutationFn: async (payload: SetFayidaPasswordPayload) => { const response = await setFayidaPass(payload); return response.data; }, onSuccess: () => { toast.success("Password updated successfully", { description: "You can now log in with your new password.", }); }, onError: (error) => { handleError(error); }, }); const { mutateAsync: verifyOtpMutate, isPending: isVerifyingOtp, isError: isVerifyOtpError, isSuccess: isVerifyOtpSuccess, error: setVerifyOtpError, } = useMutation({ mutationFn: async (payload: OtpVerificationPayload) => { const response = await verifyOTPCode(payload); return response; // return full response (including status, headers, data, etc) }, onSuccess: () => { toast.success("Phone Number Verified successfully"); }, onError: (error) => { handleError(error); }, }); const { mutate: resendVerificationMutate, isPending: isResendingCode, isError: resendError, isSuccess: resendSuccess, data: resendData, error: resendErrorData, } = useMutation({ mutationFn: async ({ phoneNumber, email, }: { phoneNumber: string; email?: string; }) => { const response = await resendVerificationCode({ phoneNumber, email }); return response; }, onSuccess: () => { toast.success("Verification code resent successfully!"); }, onError: (error) => { handleError(error); }, }); const { mutate: resendVerificationCode, isPending: isResendingOtpCode, isError: resendOtpError, isSuccess: resendOtpSuccess, data: resendOtpData, error: resendOtpErrorData, } = useMutation({ mutationFn: async ({ email, phoneNumber, }: { phoneNumber: string; email?: string; }) => { const response = await resendOtpCode({ phoneNumber, email }); return response; }, onSuccess: () => { toast.success("OTP code resent successfully!"); }, onError: (error) => { handleError(error); }, }); const { mutateAsync: refreshTokenMutate } = useMutation({ mutationFn: async (refreshToken: string) => { const response = await refreshAuthToken({ refreshToken }); const { token, refreshToken: newRefreshToken } = response.data; setAuthCookies({ token, refreshToken: newRefreshToken, rememberMe: rememberMePreference, }); return token; }, }); const { mutate: verifyMFAMutate, isPending: isMFAPending, isError: mfaError, isSuccess: mfaSuccess, } = useMutation({ mutationFn: async (payload: VerifyUserPayload) => { const response = await verifyMFAUser(payload); return response.data; }, onSuccess: async (response) => { await finalizeAuthenticatedSession({ token: response.token, refreshToken: response.refreshToken, rememberMe: rememberMePreference, }); toast.success("Two Factor Authentication Successful", { description: "Redirecting...", }); navigate("/homepage"); }, onError: (error) => { handleError(error); }, }); return { userDetails: filteredUserDetails, unFilteredUserDetails: userDetails, selectedPosition, selectedPositionPermissionKeys, activePositionLookupId, selectedPositionId, setSelectedPositionId, isLoading, isFetching, isError, refetch, login: loginMutate, isLoggingIn: isLoggingInMutating, logout: logoutMutate, logoutError, logoutSuccess, loginSuccess, loginError, setPassword: setPasswordMutate, verifyOtpMutate, isVerifyingOtp, isVerifyOtpError, isVerifyOtpSuccess, setVerifyOtpError, isSettingPassword, setPasswordError: { message: setPassError?.message, }, setPasswordSuccess, resendVerificationCode: resendVerificationMutate, isResendingCode, resendError: resendErrorData ? { message: resendErrorData?.message } : null, resendSuccess, refreshToken: refreshTokenMutate, resendOtpCode: resendVerificationCode, isResendingOtpCode, resendOtpError: resendOtpErrorData ? { message: resendOtpErrorData?.message } : null, resendOtpSuccess, resendOtpData, resendData, setFayidaPassword: setFayidaPasswordMutate, setFayidaPassError, setFayidaPasswordSuccess, setFayidaPasswordError, isSettingFayidaPassword, verifyMFA: verifyMFAMutate, isMFAPending, mfaError, mfaSuccess, }; };