Files
edr-platform/apps/edr-freight-web/portal/src/hooks/useAuth.ts

308 lines
9.6 KiB
TypeScript

import { api } from "@/services/api";
import type { ProfileTypeValue } from "@/services/companies.service";
import { companiesService } from "@/services/companies.service";
import type {
LoginPayload,
LoginResponse,
OtpResponse,
SignupPayload,
SignupResponse,
} from "@/types/auth";
import type { Result } from "@/utils/result";
import { extractApiError } from "@/utils/result";
import { useQuery, useQueryClient } from "@tanstack/react-query";
function setCookie(name: string, value: string, days: number) {
const expires = new Date();
expires.setDate(expires.getDate() + days);
document.cookie = `${name}=${value}; path=/; expires=${expires.toUTCString()}; SameSite=Lax`;
}
function getCookie(name: string): string | undefined {
return document.cookie
.split("; ")
.find((row) => row.startsWith(`${name}=`))
?.split("=")[1];
}
const useAuth = () => {
const queryClient = useQueryClient();
const hasToken = !!getCookie("auth-token");
const authQuery = useQuery(
api.auth.getMyInfo.queryOptions({
enabled: hasToken,
retry: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
}),
);
const companyQuery = useQuery(
api.companies.getInfo.queryOptions({
enabled: !!authQuery.data?.id,
retry: false,
staleTime(query) {
// Fast-poll while anything is awaiting a backoffice decision: an
// unapproved role, or a pending profile-edit review. This surfaces
// approvals/rejections to the portal within a minute.
if (
query.state.data?.review?.status === "pending" ||
query.state.data?.company?.companyProfiles?.find(
(p) => p.status !== "active",
)
)
return 60;
return 10 * 60 * 1000;
},
refetchOnWindowFocus: false,
}),
);
const isPending = authQuery.isPending && hasToken;
const isAuthenticated = hasToken && !!authQuery.data && !authQuery.isError;
const login = async (
payload: LoginPayload,
): Promise<Result<LoginResponse>> => {
try {
const res = await api.auth.login.call(payload);
setCookie("auth-token", res.token, 7);
setCookie("refresh-token", res.refreshToken, 7);
await authQuery.refetch();
return { success: true, data: res };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const signup = async (
payload: SignupPayload,
): Promise<Result<SignupResponse>> => {
try {
const res = await api.auth.createUser.call(payload);
setCookie("auth-token", res.token, 7);
setCookie("refresh-token", res.refreshToken, 7);
await authQuery.refetch();
return { success: true, data: res };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const setPassword = async (data: {
newPassword: string;
confirmPassword: string;
}): Promise<Result<void>> => {
try {
const userId = authQuery.data?.id ?? "";
const email = localStorage.getItem("otp-email") ?? "";
const verificationCode = localStorage.getItem("otp") ?? "";
await api.auth.setPassword.call({
newPassword: data.newPassword,
confirmPassword: data.confirmPassword,
userId,
email,
verificationCode,
});
["userId", "otp", "otp-phone", "otp-email"].forEach((k) =>
localStorage.removeItem(k),
);
await queryClient.invalidateQueries({
queryKey: api.auth.getMyInfo.queryKey(),
});
return { success: true, data: undefined };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const verifyOTP = async (otp: string): Promise<Result<OtpResponse>> => {
try {
const phone = localStorage.getItem("otp-phone") ?? "";
const res = await api.auth.verifyOTP.call({ phone, otp });
return { success: true, data: res };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const sendOTP = async (otp: string): Promise<Result<OtpResponse>> => {
try {
const phone = localStorage.getItem("otp-phone") ?? "";
const res = await api.auth.sendOTP.call({ phone, otp });
return { success: true, data: res };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const generateVerificationCode = async (
type: string,
): Promise<Result<string>> => {
try {
const email = localStorage.getItem("otp-email") ?? "";
const phoneNumber = localStorage.getItem("otp-phone") ?? "";
const res = await api.auth.generateVerificationCode.call({
email,
phoneNumber,
type,
});
return { success: true, data: res };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
// Active-mode (importer/exporter) state, sourced from the persisted profile.
const companyInfo = isAuthenticated ? (companyQuery.data ?? null) : null;
const activeProfileType = companyInfo?.profile?.activeProfileType ?? null;
const activeCompanyProfileId =
companyInfo?.profile?.activeCompanyProfileId ?? null;
const companyType = companyInfo?.company?.type ?? null;
const companyStatus = companyInfo?.company?.status ?? null;
// A company can create bookings only once an admin has approved it (active).
const isCompanyApproved = companyStatus === "active";
const onboardingCompleted =
companyInfo?.profile?.onboardingCompleted ?? false;
const onboardingStep = companyInfo?.profile?.onboardingStep ?? null;
// Booking is gated on backoffice approval of the active operational profile:
// a customer can only book under a profile once its status is "active".
const activeProfile =
companyInfo?.company?.companyProfiles?.find(
(p) => p.id === activeCompanyProfileId,
) ?? null;
const activeProfileStatus = activeProfile?.status ?? null;
const canBook = activeProfileStatus === "active";
// Profile-edit review: while a change request is pending the customer is
// locked out of editing and of creating new contracts/bookings; a rejected
// request surfaces the reviewer note so they can amend and resubmit.
const review = companyInfo?.review ?? null;
const reviewStatus = review?.status ?? null;
const reviewNote = review?.note ?? null;
const isUnderReview = reviewStatus === "pending";
/** Refetch everything scoped to the active operational profile. */
const invalidateScopedData = async () => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: api.companies.getInfo.queryKey(),
}),
queryClient.invalidateQueries({
queryKey: api.companies.getDashboard.queryKey(),
}),
queryClient.invalidateQueries({ queryKey: ["bookings"] }),
]);
};
const switchMode = async (type: ProfileTypeValue): Promise<Result<void>> => {
try {
await api.companies.setActiveMode.call({ type });
await invalidateScopedData();
return { success: true, data: undefined };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
/**
* Add an operational role. The new role starts pending review, so the active
* mode is left untouched — the user keeps working under their approved role.
*/
const createProfile = async (
type: ProfileTypeValue,
licenseFiles: File[],
): Promise<Result<void>> => {
try {
const created = await api.companies.createCompanyProfile.call({ type });
if (licenseFiles.length > 0) {
await companiesService.uploadProfileLicense(created.id, licenseFiles);
}
await invalidateScopedData();
return { success: true, data: undefined };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
/**
* Resubmit a rejected operational role for approval — optionally replacing its
* business license first (the common reason a role is rejected) — then refresh.
*/
const reapplyProfile = async (
profileId: string,
licenseFiles: File[] = [],
): Promise<Result<void>> => {
try {
if (licenseFiles.length > 0) {
await companiesService.uploadProfileLicense(profileId, licenseFiles);
}
await api.companies.reapplyProfile.call({ profileId });
await invalidateScopedData();
return { success: true, data: undefined };
} catch (err) {
return { success: false, error: extractApiError(err) };
}
};
const logout = async () => {
try {
await api.auth.logout.call();
} catch {
// proceed with client-side cleanup even if server call fails
}
[
"auth-token",
"refresh-token",
"auth-user",
"current-position-id",
"selected-position-id",
].forEach((name) => {
document.cookie = `${name}=; Max-Age=0; path=/`;
});
localStorage.clear();
queryClient.clear();
};
return {
isPending,
isAuthenticated,
user: isAuthenticated ? (authQuery.data ?? null) : null,
company: isAuthenticated ? (companyQuery.data ?? null) : null,
customer: isAuthenticated ? (companyQuery.data ?? null) : null,
activeProfileType,
activeCompanyProfileId,
activeProfileStatus,
canBook,
companyType,
companyStatus,
isCompanyApproved,
reviewStatus,
reviewNote,
isUnderReview,
onboardingCompleted,
onboardingStep,
switchMode,
createProfile,
reapplyProfile,
login,
signup,
setPassword,
verifyOTP,
sendOTP,
generateVerificationCode,
logout,
authQuery,
companyQuery,
customerQuery: companyQuery,
};
};
export default useAuth;