Files
edr-platform/apps/edr-freight-web/backoffice/src/auth/AuthProvider.tsx

162 lines
3.9 KiB
TypeScript

import {
createContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { getMeRequest, loginRequest, verifyMfaRequest } from "./api";
import {
AUTH_TOKEN_COOKIE,
AUTH_USER_COOKIE,
clearSessionCookies,
getCookie,
setCookie,
} from "./cookies";
import { applyTokens } from "./http";
import {
startTokenRefreshScheduler,
stopTokenRefreshScheduler,
} from "./refreshScheduler";
import type { AuthTokens, AuthUser } from "./types";
interface LoginPayload {
email: string;
password: string;
}
interface VerifyMfaPayload {
email: string;
otp: string;
}
export interface AuthContextValue {
user: AuthUser | null;
loading: boolean;
login: (payload: LoginPayload) => Promise<{ mfaRequired: boolean }>;
verifyMfa: (payload: VerifyMfaPayload) => Promise<void>;
logout: () => void;
}
export const AuthContext = createContext<AuthContextValue | null>(null);
const persistUser = (user: AuthUser | null) => {
if (user) {
setCookie(AUTH_USER_COOKIE, JSON.stringify(user));
return;
}
clearSessionCookies();
};
const bootstrapCachedUser = (): AuthUser | null => {
const serialized = getCookie(AUTH_USER_COOKIE);
if (!serialized) {
return null;
}
try {
return JSON.parse(serialized) as AuthUser;
} catch {
return null;
}
};
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(() => bootstrapCachedUser());
const [loading, setLoading] = useState(true);
const mfaEmailRef = useRef<string | null>(null);
const loadCurrentUser = async () => {
const currentUser = await getMeRequest();
setUser(currentUser);
persistUser(currentUser);
};
useEffect(() => {
["auth-token", "refresh-token"].forEach((name) => {
const value = getCookie(name);
if (value === "undefined" || value === "null" || value === "") {
clearSessionCookies();
}
});
const bootstrap = async () => {
const token = getCookie(AUTH_TOKEN_COOKIE);
if (!token) {
setLoading(false);
return;
}
try {
await loadCurrentUser();
} catch {
clearSessionCookies();
setUser(null);
} finally {
setLoading(false);
}
};
void bootstrap();
}, []);
// Keep the server session alive while a user is logged in. Runs after
// login, MFA verification, and page-reload bootstrap alike.
useEffect(() => {
if (!user) {
stopTokenRefreshScheduler();
return;
}
startTokenRefreshScheduler();
return stopTokenRefreshScheduler;
}, [user]);
const value = useMemo<AuthContextValue>(
() => ({
user,
loading,
login: async ({ email, password }) => {
const result = await loginRequest({ email, password });
if (result.mfaRequired) {
mfaEmailRef.current = email;
return { mfaRequired: true };
}
const tokens = result as AuthTokens;
applyTokens(tokens);
await loadCurrentUser();
mfaEmailRef.current = null;
return { mfaRequired: false };
},
verifyMfa: async ({ email, otp }) => {
const identifier = mfaEmailRef.current ?? email;
const tokens = await verifyMfaRequest({ email: identifier, otp });
applyTokens(tokens);
await loadCurrentUser();
mfaEmailRef.current = null;
},
logout: () => {
const preservedTheme = window.localStorage.getItem("edr-theme");
clearSessionCookies();
window.localStorage.clear();
if (preservedTheme === "dark" || preservedTheme === "light") {
window.localStorage.setItem("edr-theme", preservedTheme);
}
setUser(null);
window.location.replace("/auth");
},
}),
[loading, user],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};