All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
81 lines
2.1 KiB
TypeScript
81 lines
2.1 KiB
TypeScript
import { useCallback, useContext } from "react";
|
|
|
|
import { AuthContext } from "../contexts/AuthContext.ts";
|
|
|
|
import { type AuthResponse } from "../model.ts";
|
|
|
|
function isTokenExpired(token: string): boolean {
|
|
try {
|
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
return typeof payload.exp === "number" && payload.exp * 1000 < Date.now();
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export const useAuth = () => {
|
|
const { authResponse, setAuthResponse } = useContext(AuthContext);
|
|
|
|
const login = useCallback((response: AuthResponse) => {
|
|
setAuthResponse(response);
|
|
localStorage.setItem("authResponse", JSON.stringify(response));
|
|
}, [setAuthResponse]);
|
|
|
|
const logout = useCallback(() => {
|
|
setAuthResponse(null);
|
|
localStorage.removeItem("authResponse");
|
|
}, [setAuthResponse]);
|
|
|
|
const authFetch = useCallback(async (
|
|
input: RequestInfo,
|
|
init: RequestInit = {},
|
|
) => {
|
|
const token = authResponse?.token;
|
|
|
|
if (token && isTokenExpired(token)) {
|
|
logout();
|
|
// Return a synthetic 401 so callers handle it consistently
|
|
return new Response(null, { status: 401 });
|
|
}
|
|
|
|
const isFormData = init.body instanceof FormData;
|
|
|
|
const res = await fetch(input, {
|
|
...init,
|
|
headers: {
|
|
...(init.headers ?? {}),
|
|
// Only attach a token when we actually have one — avoids sending a
|
|
// `Bearer undefined` header that the server would reject as invalid.
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
// Let the browser set Content-Type for FormData (it includes the boundary)
|
|
...(isFormData ? {} : { "Content-Type": "application/json" }),
|
|
},
|
|
});
|
|
|
|
if (res.status === 401) {
|
|
logout();
|
|
}
|
|
|
|
return res;
|
|
}, [authResponse?.token, logout]);
|
|
|
|
return {
|
|
user: authResponse?.user ?? null,
|
|
token: authResponse?.token ?? null,
|
|
login,
|
|
logout,
|
|
authFetch,
|
|
};
|
|
};
|
|
|
|
export const useRequiredAuth = () => {
|
|
const { user, token, login, logout, authFetch } = useAuth();
|
|
|
|
if (!user) {
|
|
throw new Error(
|
|
"Invariant: useRequiredAuth called outside a protected route",
|
|
);
|
|
}
|
|
return { user, token, login, logout, authFetch };
|
|
};
|