mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
fix(web):centralize session api handling
This commit is contained in:
parent
fcf2485863
commit
411bb0019e
2 changed files with 98 additions and 6 deletions
|
|
@ -19,6 +19,25 @@ enum ResponseType {
|
|||
// Add more response types as needed
|
||||
}
|
||||
|
||||
const REFRESH_RETRY_BLOCK_MS = 30_000;
|
||||
const refreshRetryBlockedUntil = new Map<string, number>();
|
||||
|
||||
function getRefreshRetryKey(method: RequestOptions["method"], url: string): string {
|
||||
return `${method}:${url}`;
|
||||
}
|
||||
|
||||
function isRefreshRetryBlocked(key: string): boolean {
|
||||
const blockedUntil = refreshRetryBlockedUntil.get(key);
|
||||
if (!blockedUntil) return false;
|
||||
if (Date.now() < blockedUntil) return true;
|
||||
refreshRetryBlockedUntil.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
function blockRefreshRetry(key: string): void {
|
||||
refreshRetryBlockedUntil.set(key, Date.now() + REFRESH_RETRY_BLOCK_MS);
|
||||
}
|
||||
|
||||
export type RequestOptions = {
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
|
|
@ -31,7 +50,12 @@ export type RequestOptions = {
|
|||
};
|
||||
|
||||
class BaseApiService {
|
||||
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"];
|
||||
noAuthEndpoints: string[] = [
|
||||
"/auth/jwt/login",
|
||||
"/auth/register",
|
||||
"/auth/refresh",
|
||||
"/auth/jwt/refresh",
|
||||
];
|
||||
|
||||
// Prefixes that don't require auth (checked with startsWith)
|
||||
noAuthPrefixes: string[] = ["/api/v1/public/"];
|
||||
|
|
@ -42,6 +66,10 @@ class BaseApiService {
|
|||
return typeof window !== "undefined" ? getBearerToken() || "" : "";
|
||||
}
|
||||
|
||||
get isDesktopClient(): boolean {
|
||||
return typeof window !== "undefined" && !!window.electronAPI;
|
||||
}
|
||||
|
||||
// Keep for backward compatibility, but token is now always read from localStorage
|
||||
setBearerToken(_bearerToken: string) {
|
||||
void _bearerToken;
|
||||
|
|
@ -71,7 +99,9 @@ class BaseApiService {
|
|||
*/
|
||||
const defaultOptions: RequestOptions = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.bearerToken || ""}`,
|
||||
...(this.isDesktopClient && this.bearerToken
|
||||
? { Authorization: `Bearer ${this.bearerToken}` }
|
||||
: {}),
|
||||
"X-SurfSense-Client-Platform":
|
||||
typeof window === "undefined" ? "web" : getClientPlatform(),
|
||||
},
|
||||
|
|
@ -93,7 +123,8 @@ class BaseApiService {
|
|||
this.noAuthEndpoints.includes(url) ||
|
||||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix)) ||
|
||||
/^\/api\/v1\/invites\/[^/]+\/info$/.test(url);
|
||||
if (!this.bearerToken && !isNoAuthEndpoint) {
|
||||
const refreshRetryKey = getRefreshRetryKey(mergedOptions.method, url);
|
||||
if (this.isDesktopClient && !this.bearerToken && !isNoAuthEndpoint) {
|
||||
throw new AuthenticationError("You are not authenticated. Please login again.");
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +135,7 @@ class BaseApiService {
|
|||
method: mergedOptions.method,
|
||||
headers: mergedOptions.headers,
|
||||
signal: mergedOptions.signal,
|
||||
credentials: "include",
|
||||
};
|
||||
|
||||
// Automatically stringify body if Content-Type is application/json and body is an object
|
||||
|
|
@ -150,18 +182,21 @@ class BaseApiService {
|
|||
|
||||
// Handle 401 - try to refresh token first (only once)
|
||||
if (response.status === 401) {
|
||||
if (!options?._isRetry) {
|
||||
if (options?._isRetry) {
|
||||
blockRefreshRetry(refreshRetryKey);
|
||||
} else if (!isNoAuthEndpoint && !isRefreshRetryBlocked(refreshRetryKey)) {
|
||||
const newToken = await refreshAccessToken();
|
||||
if (newToken) {
|
||||
return this.request(url, responseSchema, {
|
||||
...mergedOptions,
|
||||
headers: {
|
||||
...mergedOptions.headers,
|
||||
Authorization: `Bearer ${newToken}`,
|
||||
...(this.isDesktopClient ? { Authorization: `Bearer ${newToken}` } : {}),
|
||||
},
|
||||
_isRetry: true,
|
||||
} as RequestOptions & { responseType?: R });
|
||||
}
|
||||
blockRefreshRetry(refreshRetryKey);
|
||||
}
|
||||
handleUnauthorized();
|
||||
throw new AuthenticationError(
|
||||
|
|
@ -196,6 +231,7 @@ class BaseApiService {
|
|||
);
|
||||
}
|
||||
}
|
||||
refreshRetryBlockedUntil.delete(getRefreshRetryKey(mergedOptions.method, url));
|
||||
|
||||
// biome-ignore lint/suspicious: Unknown
|
||||
let data;
|
||||
|
|
@ -381,7 +417,9 @@ class BaseApiService {
|
|||
...options,
|
||||
headers: {
|
||||
// Don't set Content-Type - let browser set it with multipart boundary
|
||||
Authorization: `Bearer ${this.bearerToken}`,
|
||||
...(this.isDesktopClient && this.bearerToken
|
||||
? { Authorization: `Bearer ${this.bearerToken}` }
|
||||
: {}),
|
||||
...headersWithoutContentType,
|
||||
},
|
||||
responseType: ResponseType.JSON,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue