Yaltopia-Tickets-App/lib/api-middlewares.ts

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;
}
};