mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
refactor: centralize authentication handling
- Replaced direct localStorage token access with a centralized `getBearerToken` function across various components and hooks to improve code maintainability and security. - Updated API calls to use `authenticatedFetch` for consistent authentication handling. - Enhanced user experience by ensuring proper redirection to login when authentication fails. - Cleaned up unused imports and improved overall code structure for better readability.
This commit is contained in:
parent
6cc9e38e1d
commit
b2a97b39ce
35 changed files with 396 additions and 497 deletions
173
surfsense_web/lib/auth-utils.ts
Normal file
173
surfsense_web/lib/auth-utils.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* Authentication utilities for handling token expiration and redirects
|
||||
*/
|
||||
|
||||
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
|
||||
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
||||
|
||||
/**
|
||||
* Saves the current path and redirects to login page
|
||||
* Call this when a 401 response is received
|
||||
*/
|
||||
export function handleUnauthorized(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// Save the current path (including search params and hash) for redirect after login
|
||||
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
||||
|
||||
// Don't save auth-related paths
|
||||
const excludedPaths = ["/auth", "/auth/callback", "/"];
|
||||
if (!excludedPaths.includes(window.location.pathname)) {
|
||||
localStorage.setItem(REDIRECT_PATH_KEY, currentPath);
|
||||
}
|
||||
|
||||
// Clear the token
|
||||
localStorage.removeItem(BEARER_TOKEN_KEY);
|
||||
|
||||
// Redirect to home page (which has login options)
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stored redirect path and clears it from storage
|
||||
* Call this after successful login to redirect the user back
|
||||
*/
|
||||
export function getAndClearRedirectPath(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
const redirectPath = localStorage.getItem(REDIRECT_PATH_KEY);
|
||||
if (redirectPath) {
|
||||
localStorage.removeItem(REDIRECT_PATH_KEY);
|
||||
}
|
||||
return redirectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bearer token from localStorage
|
||||
*/
|
||||
export function getBearerToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(BEARER_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bearer token in localStorage
|
||||
*/
|
||||
export function setBearerToken(token: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(BEARER_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the bearer token from localStorage
|
||||
*/
|
||||
export function clearBearerToken(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(BEARER_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is authenticated (has a token)
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getBearerToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current path and redirects to login page
|
||||
* Use this for client-side auth checks (e.g., in useEffect)
|
||||
* Unlike handleUnauthorized, this doesn't clear the token (user might not have one)
|
||||
*/
|
||||
export function redirectToLogin(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// Save the current path (including search params and hash) for redirect after login
|
||||
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"];
|
||||
if (!excludedPaths.includes(window.location.pathname)) {
|
||||
localStorage.setItem(REDIRECT_PATH_KEY, currentPath);
|
||||
}
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated fetch wrapper that handles 401 responses uniformly
|
||||
* Automatically redirects to login on 401 and saves the current path
|
||||
*/
|
||||
export async function authenticatedFetch(
|
||||
url: string,
|
||||
options?: RequestInit & { skipAuthRedirect?: boolean }
|
||||
): Promise<Response> {
|
||||
const { skipAuthRedirect = false, ...fetchOptions } = options || {};
|
||||
|
||||
const headers = getAuthHeaders(fetchOptions.headers as Record<string, string>);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (response.status === 401 && !skipAuthRedirect) {
|
||||
handleUnauthorized();
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for the result of a fetch operation with built-in error handling
|
||||
*/
|
||||
export type FetchResult<T> =
|
||||
| { success: true; data: T; response: Response }
|
||||
| { success: false; error: string; status?: number };
|
||||
|
||||
/**
|
||||
* Authenticated fetch with JSON response handling
|
||||
* Returns a result object instead of throwing on non-401 errors
|
||||
*/
|
||||
export async function authenticatedFetchJson<T = unknown>(
|
||||
url: string,
|
||||
options?: RequestInit & { skipAuthRedirect?: boolean }
|
||||
): Promise<FetchResult<T>> {
|
||||
try {
|
||||
const response = await authenticatedFetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.detail || `Request failed: ${response.status}`,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { success: true, data, response };
|
||||
} catch (err: any) {
|
||||
// Re-throw if it's the unauthorized redirect
|
||||
if (err.message?.includes("Unauthorized")) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: err.message || "Request failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue