120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
import { Middleware } from "@simple-api/core";
|
|
import { useAuthStore } from "./auth-store";
|
|
|
|
/**
|
|
* Middleware to inject the authentication token into requests.
|
|
* Skips login, register, and refresh endpoints.
|
|
*/
|
|
export const authMiddleware: Middleware = async ({ config, options }, next) => {
|
|
const { token } = useAuthStore.getState();
|
|
|
|
// Don't send Authorization header for sensitive auth-related endpoints,
|
|
// EXCEPT for logout which needs to identify the session.
|
|
const isAuthPath =
|
|
config.path === "auth/login" ||
|
|
config.path === "auth/register" ||
|
|
config.path === "auth/refresh";
|
|
|
|
if (token && !isAuthPath) {
|
|
options.headers = {
|
|
...options.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
}
|
|
|
|
return await next(options);
|
|
};
|
|
|
|
/**
|
|
* Middleware to handle token refreshment on 401 Unauthorized errors.
|
|
*/
|
|
export const refreshMiddleware: Middleware = async (
|
|
{ config, options },
|
|
next,
|
|
) => {
|
|
try {
|
|
return await next(options);
|
|
} catch (error: any) {
|
|
const status = error.status || error.statusCode;
|
|
const { refreshToken, setAuth, logout } = useAuthStore.getState();
|
|
|
|
// Skip refresh logic for the login/refresh endpoints themselves
|
|
const isAuthPath =
|
|
config.path?.includes("auth/login") ||
|
|
config.path?.includes("auth/refresh");
|
|
|
|
if (status === 401 && refreshToken && !isAuthPath) {
|
|
console.log(
|
|
`[API Refresh] 401 detected for ${config.path}. Attempting refresh...`,
|
|
);
|
|
|
|
try {
|
|
// We call the refresh endpoint manually here to avoid circular dependencies with the 'api' object
|
|
const refreshUrl = `${config.baseUrl}auth/refresh`;
|
|
|
|
const response = await fetch(refreshUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
refreshToken,
|
|
refresh_token: refreshToken,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
const refreshErr = new Error(
|
|
errorData.message ||
|
|
`Refresh failed with status ${response.status}`,
|
|
) as any;
|
|
refreshErr.status = response.status;
|
|
throw refreshErr;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Backend might return snake_case (access_token) or camelCase (accessToken)
|
|
// We handle both to be safe when using raw fetch
|
|
const accessToken = data.accessToken || data.access_token;
|
|
const newRefreshToken = data.refreshToken || data.refresh_token;
|
|
const user = data.user;
|
|
|
|
if (!accessToken) {
|
|
throw new Error("No access token returned from refresh");
|
|
}
|
|
|
|
setAuth(user, accessToken, newRefreshToken);
|
|
|
|
console.log("[API Refresh] Success. Retrying original request...");
|
|
|
|
// Update headers and retry
|
|
options.headers = {
|
|
...options.headers,
|
|
Authorization: `Bearer ${accessToken}`,
|
|
};
|
|
|
|
return await next(options);
|
|
} catch (refreshError: any) {
|
|
// Only logout if the refresh token itself is invalid (400, 401, 403)
|
|
// If it's a network error, we should NOT logout the user.
|
|
const refreshStatus = refreshError.status || refreshError.statusCode;
|
|
const isAuthError = refreshStatus === 401;
|
|
|
|
if (isAuthError) {
|
|
console.error("[API Refresh] Invalid refresh token. Logging out.");
|
|
logout();
|
|
} else {
|
|
console.error(
|
|
"[API Refresh] Network error or server issues during refresh. Staying logged in.",
|
|
);
|
|
}
|
|
throw refreshError;
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|