mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-03 07:23:40 +00:00
Both apps wrote a cookie named `current-position-id` but stored different ids in it — freight the `employeePositionId`, Smart Office the `position.id`. On a shared domain each login overwrote the other's desk selection, and the loser silently fell back to the first position. Freight now uses `freight-current-position-id` through a small helper that reads the old name once, so a session live across the deploy keeps its desk, and clears it on every write. Also mounts the position switcher in the freight dashboard header. It only existed under /performance-management, so on every other page a two-desk user had no way to switch and was stuck on whatever `useAuthUser` defaulted to. It now hides below two positions rather than showing a one-option dropdown to the single-desk majority. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
550 lines
15 KiB
TypeScript
550 lines
15 KiB
TypeScript
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<MeDto>({
|
|
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<MeDto>();
|
|
|
|
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,
|
|
};
|
|
};
|