mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
fix(web):centralize session auth fetch
This commit is contained in:
parent
951cb1a717
commit
d2a8d088c7
6 changed files with 142 additions and 266 deletions
|
|
@ -7,8 +7,8 @@ export const loginRequest = z.object({
|
|||
});
|
||||
|
||||
export const loginResponse = z.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
authenticated: z.boolean(),
|
||||
access_expires_at: z.number(),
|
||||
});
|
||||
|
||||
export const registerRequest = loginRequest.omit({ grant_type: true, username: true }).extend({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { buildBackendUrl } from "@/lib/env-config";
|
|||
|
||||
type SessionState =
|
||||
| { status: "loading"; authenticated: false; accessExpiresAt: null }
|
||||
| { status: "authenticated"; authenticated: true; accessExpiresAt: number }
|
||||
| { status: "authenticated"; authenticated: true; accessExpiresAt: number | null }
|
||||
| { status: "unauthenticated"; authenticated: false; accessExpiresAt: null };
|
||||
|
||||
export function useSession() {
|
||||
|
|
@ -30,7 +30,7 @@ export function useSession() {
|
|||
}
|
||||
const data = (await response.json()) as {
|
||||
authenticated: boolean;
|
||||
access_expires_at: number;
|
||||
access_expires_at: number | null;
|
||||
};
|
||||
setState({
|
||||
status: "authenticated",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ZodType } from "zod";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { getClientPlatform } from "../agent-filesystem";
|
||||
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
||||
import { handleUnauthorized, refreshSession } from "../auth-utils";
|
||||
import {
|
||||
AbortedError,
|
||||
AppError,
|
||||
|
|
@ -53,27 +53,19 @@ class BaseApiService {
|
|||
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/"];
|
||||
|
||||
// Use a getter to always read fresh token from localStorage
|
||||
// This ensures the token is always up-to-date after login/logout
|
||||
get bearerToken(): string {
|
||||
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;
|
||||
// No-op: token is now always read fresh from localStorage via the getter
|
||||
private async getDesktopAccessToken(): Promise<string> {
|
||||
if (!this.isDesktopClient) return "";
|
||||
return (await window.electronAPI?.getAccessToken?.()) || "";
|
||||
}
|
||||
|
||||
async request<T, R extends ResponseType = ResponseType.JSON>(
|
||||
|
|
@ -97,11 +89,15 @@ class BaseApiService {
|
|||
* REQUEST
|
||||
* ----------
|
||||
*/
|
||||
const isNoAuthEndpoint =
|
||||
this.noAuthEndpoints.includes(url) ||
|
||||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix)) ||
|
||||
/^\/api\/v1\/invites\/[^/]+\/info$/.test(url);
|
||||
const desktopAccessToken =
|
||||
this.isDesktopClient && !isNoAuthEndpoint ? await this.getDesktopAccessToken() : "";
|
||||
const defaultOptions: RequestOptions = {
|
||||
headers: {
|
||||
...(this.isDesktopClient && this.bearerToken
|
||||
? { Authorization: `Bearer ${this.bearerToken}` }
|
||||
: {}),
|
||||
...(desktopAccessToken ? { Authorization: `Bearer ${desktopAccessToken}` } : {}),
|
||||
"X-SurfSense-Client-Platform":
|
||||
typeof window === "undefined" ? "web" : getClientPlatform(),
|
||||
},
|
||||
|
|
@ -118,13 +114,8 @@ class BaseApiService {
|
|||
},
|
||||
};
|
||||
|
||||
// Validate the bearer token
|
||||
const isNoAuthEndpoint =
|
||||
this.noAuthEndpoints.includes(url) ||
|
||||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix)) ||
|
||||
/^\/api\/v1\/invites\/[^/]+\/info$/.test(url);
|
||||
const refreshRetryKey = getRefreshRetryKey(mergedOptions.method, url);
|
||||
if (this.isDesktopClient && !this.bearerToken && !isNoAuthEndpoint) {
|
||||
if (this.isDesktopClient && !desktopAccessToken && !isNoAuthEndpoint) {
|
||||
throw new AuthenticationError("You are not authenticated. Please login again.");
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +176,9 @@ class BaseApiService {
|
|||
if (options?._isRetry) {
|
||||
blockRefreshRetry(refreshRetryKey);
|
||||
} else if (!isNoAuthEndpoint && !isRefreshRetryBlocked(refreshRetryKey)) {
|
||||
const newToken = await refreshAccessToken();
|
||||
if (newToken) {
|
||||
const refreshed = await refreshSession();
|
||||
if (refreshed) {
|
||||
const newToken = this.isDesktopClient ? await this.getDesktopAccessToken() : "";
|
||||
return this.request(url, responseSchema, {
|
||||
...mergedOptions,
|
||||
headers: {
|
||||
|
|
@ -417,9 +409,6 @@ class BaseApiService {
|
|||
...options,
|
||||
headers: {
|
||||
// Don't set Content-Type - let browser set it with multipart boundary
|
||||
...(this.isDesktopClient && this.bearerToken
|
||||
? { Authorization: `Bearer ${this.bearerToken}` }
|
||||
: {}),
|
||||
...headersWithoutContentType,
|
||||
},
|
||||
responseType: ResponseType.JSON,
|
||||
|
|
|
|||
79
surfsense_web/lib/auth-fetch.ts
Normal file
79
surfsense_web/lib/auth-fetch.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { handleUnauthorized, isDesktopClient, refreshSession } from "@/lib/auth-utils";
|
||||
|
||||
let desktopAccessToken: string | null = null;
|
||||
let didSubscribeToDesktopAuth = false;
|
||||
|
||||
function subscribeToDesktopAuth(): void {
|
||||
if (
|
||||
didSubscribeToDesktopAuth ||
|
||||
typeof window === "undefined" ||
|
||||
!window.electronAPI
|
||||
) {
|
||||
return;
|
||||
}
|
||||
didSubscribeToDesktopAuth = true;
|
||||
|
||||
window.electronAPI.onAuthChanged?.(({ accessToken }) => {
|
||||
desktopAccessToken = accessToken;
|
||||
});
|
||||
void window.electronAPI.getAccessToken?.().then((token) => {
|
||||
if (token) desktopAccessToken = token;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDesktopAccessToken(): Promise<string | null> {
|
||||
if (!isDesktopClient()) return null;
|
||||
subscribeToDesktopAuth();
|
||||
if (desktopAccessToken) return desktopAccessToken;
|
||||
const token = (await window.electronAPI?.getAccessToken?.()) || null;
|
||||
desktopAccessToken = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
export function getAuthHeaders(additionalHeaders?: Record<string, string>): Record<string, string> {
|
||||
subscribeToDesktopAuth();
|
||||
return {
|
||||
...(desktopAccessToken ? { Authorization: `Bearer ${desktopAccessToken}` } : {}),
|
||||
...additionalHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
export async function authenticatedFetch(
|
||||
url: string,
|
||||
options?: RequestInit & { skipAuthRedirect?: boolean; skipRefresh?: boolean }
|
||||
): Promise<Response> {
|
||||
const { skipAuthRedirect = false, skipRefresh = false, ...fetchOptions } = options || {};
|
||||
const token = await getDesktopAccessToken();
|
||||
const headers = {
|
||||
...(fetchOptions.headers as Record<string, string>),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 401 && !skipAuthRedirect) {
|
||||
if (!skipRefresh) {
|
||||
const refreshed = await refreshSession();
|
||||
if (refreshed) {
|
||||
const newToken = await getDesktopAccessToken();
|
||||
return fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...(fetchOptions.headers as Record<string, string>),
|
||||
...(newToken ? { Authorization: `Bearer ${newToken}` } : {}),
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleUnauthorized();
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
@ -1,23 +1,20 @@
|
|||
/**
|
||||
* Authentication utilities for handling token expiration and redirects
|
||||
* Authentication utilities for handling session expiration and redirects.
|
||||
*/
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
|
||||
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
||||
const REFRESH_TOKEN_KEY = "surfsense_refresh_token";
|
||||
const LEGACY_BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
||||
const LEGACY_REFRESH_TOKEN_KEY = "surfsense_refresh_token";
|
||||
|
||||
let desktopBearerToken: string | null = null;
|
||||
let desktopRefreshToken: string | null = null;
|
||||
|
||||
function isDesktopClient(): boolean {
|
||||
export function isDesktopClient(): boolean {
|
||||
return typeof window !== "undefined" && !!window.electronAPI;
|
||||
}
|
||||
|
||||
function purgeLegacyStoredTokens(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(BEARER_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(LEGACY_BEARER_TOKEN_KEY);
|
||||
localStorage.removeItem(LEGACY_REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** Path prefixes for routes that do not require auth (no current-user fetch, no redirect on 401) */
|
||||
|
|
@ -52,24 +49,20 @@ export function getLoginPath(): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Clears tokens and optionally redirects to login.
|
||||
* Clears auth state and optionally redirects to login.
|
||||
* Call this when a 401 response is received.
|
||||
* Only redirects when the current route is protected; on public routes we just clear tokens.
|
||||
* Only redirects when the current route is protected; on public routes we just clear state.
|
||||
*/
|
||||
export function handleUnauthorized(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const pathname = window.location.pathname;
|
||||
|
||||
// Always clear tokens
|
||||
purgeLegacyStoredTokens();
|
||||
desktopBearerToken = null;
|
||||
desktopRefreshToken = null;
|
||||
|
||||
// Only redirect on protected routes; stay on public pages (e.g. /docs)
|
||||
if (!isPublicRoute(pathname)) {
|
||||
const currentPath = pathname + window.location.search + window.location.hash;
|
||||
const excludedPaths = ["/auth", "/auth/callback", "/"];
|
||||
const excludedPaths = ["/auth", "/"];
|
||||
if (!excludedPaths.includes(pathname)) {
|
||||
setRedirectPath(currentPath);
|
||||
}
|
||||
|
|
@ -99,111 +92,8 @@ export function getAndClearRedirectPath(): string | null {
|
|||
return redirectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bearer token from localStorage
|
||||
*/
|
||||
export function getBearerToken(): string | null {
|
||||
if (typeof window === "undefined" || !isDesktopClient()) return null;
|
||||
return desktopBearerToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bearer token in localStorage
|
||||
*/
|
||||
export function setBearerToken(token: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
purgeLegacyStoredTokens();
|
||||
desktopBearerToken = isDesktopClient() ? token : null;
|
||||
syncTokensToElectron();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the bearer token from localStorage
|
||||
*/
|
||||
export function clearBearerToken(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(BEARER_TOKEN_KEY);
|
||||
desktopBearerToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the refresh token from localStorage
|
||||
*/
|
||||
export function getRefreshToken(): string | null {
|
||||
if (typeof window === "undefined" || !isDesktopClient()) return null;
|
||||
return desktopRefreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the refresh token in localStorage
|
||||
*/
|
||||
export function setRefreshToken(token: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
purgeLegacyStoredTokens();
|
||||
desktopRefreshToken = isDesktopClient() ? token : null;
|
||||
syncTokensToElectron();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the refresh token from localStorage
|
||||
*/
|
||||
export function clearRefreshToken(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
desktopRefreshToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all auth tokens from localStorage
|
||||
*/
|
||||
export function clearAllTokens(): void {
|
||||
clearBearerToken();
|
||||
clearRefreshToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the current localStorage tokens into the Electron main process
|
||||
* so that other BrowserWindows (Quick Ask, Autocomplete) can access them.
|
||||
*/
|
||||
function syncTokensToElectron(): void {
|
||||
if (typeof window === "undefined" || !window.electronAPI?.setAuthTokens) return;
|
||||
const bearer = desktopBearerToken || "";
|
||||
const refresh = desktopRefreshToken || "";
|
||||
if (bearer) {
|
||||
window.electronAPI.setAuthTokens(bearer, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to pull auth tokens from the Electron main process into localStorage.
|
||||
* Useful for popup windows (Quick Ask, Autocomplete) on platforms where
|
||||
* localStorage is not reliably shared across BrowserWindow instances.
|
||||
* Returns true if tokens were found and written to localStorage.
|
||||
*/
|
||||
export async function ensureTokensFromElectron(): Promise<boolean> {
|
||||
if (typeof window === "undefined" || !window.electronAPI?.getAuthTokens) return false;
|
||||
if (getBearerToken()) return true;
|
||||
|
||||
try {
|
||||
if (window.electronAPI.getAccessToken) {
|
||||
const token = await window.electronAPI.getAccessToken();
|
||||
if (token) {
|
||||
desktopBearerToken = token;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const tokens = await window.electronAPI.getAuthTokens();
|
||||
if (tokens?.bearer) {
|
||||
desktopBearerToken = tokens.bearer;
|
||||
if (tokens.refresh) {
|
||||
desktopRefreshToken = tokens.refresh;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// IPC failure — fall through
|
||||
}
|
||||
return false;
|
||||
export function getPostLoginRedirectPath(defaultPath = "/dashboard"): string {
|
||||
return getAndClearRedirectPath() || defaultPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -211,46 +101,45 @@ export async function ensureTokensFromElectron(): Promise<boolean> {
|
|||
* Returns true if logout was successful (or tokens were cleared), false otherwise.
|
||||
*/
|
||||
export async function logout(): Promise<boolean> {
|
||||
const refreshToken = getRefreshToken();
|
||||
const isDesktop = isDesktopClient();
|
||||
|
||||
if (isDesktop && window.electronAPI?.logout) {
|
||||
await window.electronAPI.logout();
|
||||
clearAllTokens();
|
||||
purgeLegacyStoredTokens();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call backend to revoke the refresh token
|
||||
if (refreshToken || !isDesktop) {
|
||||
try {
|
||||
const response = await fetch(buildBackendUrl("/auth/jwt/revoke"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
...(refreshToken ? { body: JSON.stringify({ refresh_token: refreshToken }) } : {}),
|
||||
});
|
||||
try {
|
||||
const response = await fetch(buildBackendUrl("/auth/jwt/revoke"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn("Failed to revoke refresh token:", response.status, await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to revoke refresh token on server:", error);
|
||||
// Continue to clear local tokens even if server call fails
|
||||
if (!response.ok) {
|
||||
console.warn("Failed to revoke refresh token:", response.status, await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to revoke refresh token on server:", error);
|
||||
// Continue to clear local state even if server revoke fails.
|
||||
}
|
||||
|
||||
// Clear all tokens from localStorage
|
||||
clearAllTokens();
|
||||
purgeLegacyStoredTokens();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is authenticated (has a token)
|
||||
* Compatibility helper for legacy query gates.
|
||||
*
|
||||
* Web auth is cookie-backed, so the client cannot synchronously prove whether a
|
||||
* session exists. Return true and let `/auth/session` or API 401s settle it.
|
||||
* Desktop can synchronously check for the Electron bridge, while the access
|
||||
* token itself is resolved asynchronously by auth-fetch.
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
return isDesktopClient() ? !!getBearerToken() : true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -265,7 +154,7 @@ export function redirectToLogin(): void {
|
|||
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
||||
|
||||
// Don't save auth-related paths or home page
|
||||
const excludedPaths = ["/auth", "/auth/callback", "/", "/login", "/register", "/desktop/login"];
|
||||
const excludedPaths = ["/auth", "/", "/login", "/register", "/desktop/login"];
|
||||
if (!excludedPaths.includes(window.location.pathname)) {
|
||||
setRedirectPath(currentPath);
|
||||
}
|
||||
|
|
@ -273,32 +162,10 @@ export function redirectToLogin(): void {
|
|||
window.location.href = getLoginPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates headers with authorization bearer token
|
||||
*/
|
||||
export function getAuthHeaders(additionalHeaders?: Record<string, string>): Record<string, string> {
|
||||
const token = getBearerToken();
|
||||
return {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...additionalHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to refresh the access token using the stored refresh token.
|
||||
* Returns the new access token if successful, null otherwise.
|
||||
*/
|
||||
async function doRefreshSession(): Promise<string | null> {
|
||||
const currentRefreshToken = getRefreshToken();
|
||||
if (isDesktopClient() && !currentRefreshToken) {
|
||||
if (window.electronAPI?.refreshAccessToken) {
|
||||
const token = await window.electronAPI.refreshAccessToken();
|
||||
if (token) {
|
||||
desktopBearerToken = token;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
return null;
|
||||
async function doRefreshSession(): Promise<boolean> {
|
||||
if (isDesktopClient()) {
|
||||
const token = await window.electronAPI?.refreshAccessToken?.();
|
||||
return !!token;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -308,80 +175,22 @@ async function doRefreshSession(): Promise<string | null> {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
...(currentRefreshToken ? { body: JSON.stringify({ refresh_token: currentRefreshToken }) } : {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearAllTokens();
|
||||
return null;
|
||||
purgeLegacyStoredTokens();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (isDesktopClient() && data.access_token) {
|
||||
setBearerToken(data.access_token);
|
||||
if (data.refresh_token) {
|
||||
setRefreshToken(data.refresh_token);
|
||||
}
|
||||
}
|
||||
return data.access_token ?? null;
|
||||
return true;
|
||||
} catch {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshSession(): Promise<string | null> {
|
||||
export async function refreshSession(): Promise<boolean> {
|
||||
if (typeof navigator !== "undefined" && "locks" in navigator) {
|
||||
return navigator.locks.request("ss-token-refresh", () => doRefreshSession());
|
||||
}
|
||||
return doRefreshSession();
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<string | null> {
|
||||
return refreshSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated fetch wrapper that handles 401 responses uniformly.
|
||||
* On 401, attempts to refresh the token and retry the request.
|
||||
* If refresh fails, redirects to login and saves the current path.
|
||||
*/
|
||||
export async function authenticatedFetch(
|
||||
url: string,
|
||||
options?: RequestInit & { skipAuthRedirect?: boolean; skipRefresh?: boolean }
|
||||
): Promise<Response> {
|
||||
const { skipAuthRedirect = false, skipRefresh = false, ...fetchOptions } = options || {};
|
||||
|
||||
const headers = getAuthHeaders(fetchOptions.headers as Record<string, string>);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (response.status === 401 && !skipAuthRedirect) {
|
||||
// Try to refresh the token (unless skipRefresh is set to prevent infinite loops)
|
||||
if (!skipRefresh) {
|
||||
const newToken = await refreshAccessToken();
|
||||
if (newToken) {
|
||||
// Retry the original request with the new token
|
||||
const retryHeaders = {
|
||||
...(fetchOptions.headers as Record<string, string>),
|
||||
Authorization: `Bearer ${newToken}`,
|
||||
};
|
||||
return fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: retryHeaders,
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh failed or was skipped, redirect to login
|
||||
handleUnauthorized();
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
3
surfsense_web/types/window.d.ts
vendored
3
surfsense_web/types/window.d.ts
vendored
|
|
@ -141,12 +141,11 @@ interface ElectronAPI {
|
|||
searchSpaceId?: number | null
|
||||
) => Promise<LocalTextFileResult>;
|
||||
// Auth token sync across windows
|
||||
getAuthTokens: () => Promise<{ bearer: string; refresh: string } | null>;
|
||||
setAuthTokens: (bearer: string, refresh: string) => Promise<void>;
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
logout: () => Promise<void>;
|
||||
startGoogleOAuth: () => Promise<{ ok: true }>;
|
||||
loginPassword: (email: string, password: string) => Promise<{ ok: true }>;
|
||||
onAuthChanged: (
|
||||
callback: (payload: { authed: boolean; accessToken: string | null }) => void
|
||||
) => () => void;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue