diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx index 2d82877b3..cc4330203 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx @@ -22,6 +22,7 @@ import { type SearchSourceConnector, useSearchSourceConnectors, } from "@/hooks/use-search-source-connectors"; +import { authenticatedFetch } from "@/lib/auth-utils"; export default function AirtableConnectorPage() { const router = useRouter(); @@ -46,14 +47,9 @@ export default function AirtableConnectorPage() { const handleConnectAirtable = async () => { setIsConnecting(true); try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/airtable/connector/add/?space_id=${searchSpaceId}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx index 90a02a5f2..833d716a8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx @@ -40,6 +40,7 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; // Assuming useSearchSourceConnectors hook exists and works similarly import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; +import { authenticatedFetch, redirectToLogin } from "@/lib/auth-utils"; // Define the form schema with Zod for GitHub PAT entry step const githubPatFormSchema = z.object({ @@ -101,19 +102,11 @@ export default function GithubConnectorPage() { setConnectorName(values.name); // Store the name setValidatedPat(values.github_pat); // Store the PAT temporarily try { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) { - throw new Error("No authentication token found"); - } - - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ github_pat: values.github_pat }), } ); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx index 2fdc95671..8179fbabc 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx @@ -24,6 +24,7 @@ import { type SearchSourceConnector, useSearchSourceConnectors, } from "@/hooks/use-search-source-connectors"; +import { authenticatedFetch } from "@/lib/auth-utils"; export default function GoogleCalendarConnectorPage() { const router = useRouter(); @@ -51,14 +52,9 @@ export default function GoogleCalendarConnectorPage() { try { setIsConnecting(true); // Call backend to initiate authorization flow - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/calendar/connector/add/?space_id=${searchSpaceId}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx index c1354d03e..8659d937c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx @@ -24,6 +24,7 @@ import { type SearchSourceConnector, useSearchSourceConnectors, } from "@/hooks/use-search-source-connectors"; +import { authenticatedFetch } from "@/lib/auth-utils"; export default function GoogleGmailConnectorPage() { const router = useRouter(); @@ -50,14 +51,9 @@ export default function GoogleGmailConnectorPage() { try { setIsConnecting(true); // Call backend to initiate authorization flow - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/gmail/connector/add/?space_id=${searchSpaceId}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index 71ce2fa2f..a7e0d6861 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -9,6 +9,7 @@ import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface EditorContent { document_id: number; @@ -29,28 +30,21 @@ export default function EditorPage() { const [error, setError] = useState(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - // Get auth token - const token = - typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") : null; - // Fetch document content - DIRECT CALL TO FASTAPI useEffect(() => { async function fetchDocument() { + const token = getBearerToken(); if (!token) { console.error("No auth token found"); - setError("Please login to access the editor"); - setLoading(false); + // Redirect to login with current path saved + redirectToLogin(); return; } try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/editor-content`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } + { method: "GET" } ); if (!response.ok) { @@ -84,10 +78,10 @@ export default function EditorPage() { } } - if (documentId && token) { + if (documentId) { fetchDocument(); } - }, [documentId, token]); + }, [documentId, params.search_space_id]); // Track changes to mark as unsaved useEffect(() => { @@ -100,8 +94,10 @@ export default function EditorPage() { // Save and exit - DIRECT CALL TO FASTAPI const handleSave = async () => { + const token = getBearerToken(); if (!token) { toast.error("Please login to save"); + redirectToLogin(); return; } @@ -113,14 +109,11 @@ export default function EditorPage() { setSaving(true); try { // Save blocknote_document and trigger reindexing in background - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/save`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ blocknote_document: editorContent }), } ); diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 150cd772c..7382429d2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -13,6 +13,7 @@ import { OnboardLLMSetup } from "@/components/onboard/onboard-llm-setup"; import { OnboardLoading } from "@/components/onboard/onboard-loading"; import { OnboardStats } from "@/components/onboard/onboard-stats"; import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; +import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; const OnboardPage = () => { const t = useTranslations("onboard"); @@ -44,12 +45,13 @@ const OnboardPage = () => { // Check if user is authenticated useEffect(() => { - const token = localStorage.getItem("surfsense_bearer_token"); + const token = getBearerToken(); if (!token) { - router.push("/login"); + // Save current path and redirect to login + redirectToLogin(); return; } - }, [router]); + }, []); // Capture onboarding state on first load useEffect(() => { diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 01436aff9..8763a622f 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -1,28 +1,28 @@ "use client"; import { Loader2 } from "lucide-react"; -import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { AnnouncementBanner } from "@/components/announcement-banner"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface DashboardLayoutProps { children: React.ReactNode; } export default function DashboardLayout({ children }: DashboardLayoutProps) { - const router = useRouter(); const [isCheckingAuth, setIsCheckingAuth] = useState(true); useEffect(() => { // Check if user is authenticated - const token = localStorage.getItem("surfsense_bearer_token"); + const token = getBearerToken(); if (!token) { - router.push("/login"); + // Save current path and redirect to login + redirectToLogin(); return; } setIsCheckingAuth(false); - }, [router]); + }, []); // Show loading screen while checking authentication if (isCheckingAuth) { diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 0910d0b44..0b3450d20 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -36,6 +36,7 @@ import { Spotlight } from "@/components/ui/spotlight"; import { Tilt } from "@/components/ui/tilt"; import { useUser } from "@/hooks"; import { useSearchSpaces } from "@/hooks/use-search-spaces"; +import { authenticatedFetch } from "@/lib/auth-utils"; /** * Formats a date string into a readable format @@ -173,14 +174,9 @@ const DashboardPage = () => { const handleDeleteSearchSpace = async (id: number) => { // Send DELETE request to the API try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "DELETE" } ); if (!response.ok) { diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx index 5d18195af..520c4358e 100644 --- a/surfsense_web/app/dashboard/searchspaces/page.tsx +++ b/surfsense_web/app/dashboard/searchspaces/page.tsx @@ -4,18 +4,17 @@ import { motion } from "motion/react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { SearchSpaceForm } from "@/components/search-space-form"; +import { authenticatedFetch } from "@/lib/auth-utils"; + export default function SearchSpacesPage() { const router = useRouter(); const handleCreateSearchSpace = async (data: { name: string; description?: string }) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: data.name, description: data.description || "", diff --git a/surfsense_web/app/invite/[invite_code]/page.tsx b/surfsense_web/app/invite/[invite_code]/page.tsx index 1a4600483..4ff78ac91 100644 --- a/surfsense_web/app/invite/[invite_code]/page.tsx +++ b/surfsense_web/app/invite/[invite_code]/page.tsx @@ -27,6 +27,7 @@ import { CardTitle, } from "@/components/ui/card"; import { useInviteInfo } from "@/hooks/use-rbac"; +import { getBearerToken } from "@/lib/auth-utils"; export default function InviteAcceptPage() { const params = useParams(); @@ -47,7 +48,7 @@ export default function InviteAcceptPage() { // Check if user is logged in useEffect(() => { if (typeof window !== "undefined") { - const token = localStorage.getItem("surfsense_bearer_token"); + const token = getBearerToken(); setIsLoggedIn(!!token); } }, []); @@ -71,7 +72,10 @@ export default function InviteAcceptPage() { const handleLoginRedirect = () => { // Store the invite code to redirect back after login localStorage.setItem("pending_invite_code", inviteCode); - router.push("/auth"); + // Save the current invite page URL so we can return after authentication + localStorage.setItem("surfsense_redirect_path", `/invite/${inviteCode}`); + // Redirect to login (we manually set the path above since invite pages need special handling) + window.location.href = "/login"; }; // Check for pending invite after login diff --git a/surfsense_web/atoms/chats/chat-mutation.atoms.ts b/surfsense_web/atoms/chats/chat-mutation.atoms.ts index f7ad5cf4b..6105ef615 100644 --- a/surfsense_web/atoms/chats/chat-mutation.atoms.ts +++ b/surfsense_web/atoms/chats/chat-mutation.atoms.ts @@ -7,6 +7,7 @@ import type { UpdateChatRequest, } from "@/contracts/types/chat.types"; import { chatsApiService } from "@/lib/apis/chats-api.service"; +import { getBearerToken } from "@/lib/auth-utils"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queryClient } from "@/lib/query-client/client"; import { activeSearchSpaceIdAtom } from "../seach-spaces/seach-space-queries.atom"; @@ -14,7 +15,7 @@ import { globalChatsQueryParamsAtom } from "./ui.atoms"; export const deleteChatMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const chatsQueryParams = get(globalChatsQueryParamsAtom); return { @@ -38,7 +39,7 @@ export const deleteChatMutationAtom = atomWithMutation((get) => { export const createChatMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const chatsQueryParams = get(globalChatsQueryParamsAtom); return { @@ -58,7 +59,7 @@ export const createChatMutationAtom = atomWithMutation((get) => { export const updateChatMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const chatsQueryParams = get(globalChatsQueryParamsAtom); return { diff --git a/surfsense_web/atoms/chats/chat-query.atoms.ts b/surfsense_web/atoms/chats/chat-query.atoms.ts index 26b2b1057..36871dbd0 100644 --- a/surfsense_web/atoms/chats/chat-query.atoms.ts +++ b/surfsense_web/atoms/chats/chat-query.atoms.ts @@ -2,12 +2,13 @@ import { atomWithQuery } from "jotai-tanstack-query"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; import { chatsApiService } from "@/lib/apis/chats-api.service"; import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; +import { getBearerToken } from "@/lib/auth-utils"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { activeChatIdAtom, globalChatsQueryParamsAtom } from "./ui.atoms"; export const activeChatAtom = atomWithQuery((get) => { const activeChatId = get(activeChatIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); return { queryKey: cacheKeys.chats.activeChat(activeChatId ?? ""), @@ -32,7 +33,7 @@ export const activeChatAtom = atomWithQuery((get) => { export const chatsAtom = atomWithQuery((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const queryParams = get(globalChatsQueryParamsAtom); return { diff --git a/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts b/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts index 3a2403af7..7e51891be 100644 --- a/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts +++ b/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts @@ -7,13 +7,14 @@ import type { Podcast, } from "@/contracts/types/podcast.types"; import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; +import { getBearerToken } from "@/lib/auth-utils"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queryClient } from "@/lib/query-client/client"; import { globalPodcastsQueryParamsAtom } from "./ui.atoms"; export const deletePodcastMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const podcastsQueryParams = get(globalPodcastsQueryParamsAtom); return { @@ -37,7 +38,7 @@ export const deletePodcastMutationAtom = atomWithMutation((get) => { export const generatePodcastMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const authToken = getBearerToken(); const podcastsQueryParams = get(globalPodcastsQueryParamsAtom); return { diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx index 3b80994a8..70119dfe4 100644 --- a/surfsense_web/components/TokenHandler.tsx +++ b/surfsense_web/components/TokenHandler.tsx @@ -2,22 +2,25 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect } from "react"; +import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; interface TokenHandlerProps { - redirectPath?: string; // Path to redirect after storing token + redirectPath?: string; // Default path to redirect after storing token (if no saved path) tokenParamName?: string; // Name of the URL parameter containing the token - storageKey?: string; // Key to use when storing in localStorage + storageKey?: string; // Key to use when storing in localStorage (kept for backwards compatibility) } /** * Client component that extracts a token from URL parameters and stores it in localStorage + * After storing the token, it redirects the user back to the page they were on before + * being redirected to login (if available), or to the default redirectPath. * - * @param redirectPath - Path to redirect after storing token (default: '/') + * @param redirectPath - Default path to redirect after storing token (default: '/dashboard') * @param tokenParamName - Name of the URL parameter containing the token (default: 'token') - * @param storageKey - Key to use when storing in localStorage (default: 'auth_token') + * @param storageKey - Key to use when storing in localStorage (default: 'surfsense_bearer_token') */ const TokenHandler = ({ - redirectPath = "/", + redirectPath = "/dashboard", tokenParamName = "token", storageKey = "surfsense_bearer_token", }: TokenHandlerProps) => { @@ -33,14 +36,22 @@ const TokenHandler = ({ if (token) { try { - // Store token in localStorage + // Store token in localStorage using both methods for compatibility localStorage.setItem(storageKey, token); - // console.log(`Token stored in localStorage with key: ${storageKey}`); + setBearerToken(token); - // Redirect to specified path - router.push(redirectPath); + // Check if there's a saved redirect path from before the auth flow + const savedRedirectPath = getAndClearRedirectPath(); + + // Use the saved path if available, otherwise use the default redirectPath + const finalRedirectPath = savedRedirectPath || redirectPath; + + // Redirect to the appropriate path + router.push(finalRedirectPath); } catch (error) { console.error("Error storing token in localStorage:", error); + // Even if there's an error, try to redirect to the default path + router.push(redirectPath); } } }, [searchParams, tokenParamName, storageKey, redirectPath, router]); diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index 76abe3817..c845403a6 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -14,6 +14,7 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { useSearchSpace } from "@/hooks/use-search-space"; +import { authenticatedFetch, getBearerToken } from "@/lib/auth-utils"; interface BreadcrumbItemInterface { label: string; @@ -41,17 +42,12 @@ export function DashboardBreadcrumb() { useEffect(() => { if (segments[2] === "editor" && segments[3] && searchSpaceId) { const documentId = segments[3]; - const token = - typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") : null; + const token = getBearerToken(); if (token) { - fetch( + authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } + { method: "GET" } ) .then((res) => res.json()) .then((data) => { diff --git a/surfsense_web/components/onboard/setup-prompt-step.tsx b/surfsense_web/components/onboard/setup-prompt-step.tsx index 9a190e6c7..899d856fa 100644 --- a/surfsense_web/components/onboard/setup-prompt-step.tsx +++ b/surfsense_web/components/onboard/setup-prompt-step.tsx @@ -13,6 +13,7 @@ import { Switch } from "@/components/ui/switch"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { type CommunityPrompt, useCommunityPrompts } from "@/hooks/use-community-prompts"; +import { authenticatedFetch } from "@/lib/auth-utils"; interface SetupPromptStepProps { searchSpaceId: number; @@ -74,14 +75,11 @@ export function SetupPromptStep({ searchSpaceId, onComplete }: SetupPromptStepPr // Only send update if there's something to update if (Object.keys(payload).length > 0) { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index 0de1ca286..a1199c10d 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -25,6 +25,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { type CommunityPrompt, useCommunityPrompts } from "@/hooks/use-community-prompts"; import { useSearchSpace } from "@/hooks/use-search-space"; +import { authenticatedFetch } from "@/lib/auth-utils"; interface PromptConfigManagerProps { searchSpaceId: number; @@ -78,14 +79,11 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) // Only send request if we have something to update if (Object.keys(payload).length > 0) { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index c9976bb64..c51522a74 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; +import { getAuthHeaders } from "@/lib/auth-utils"; import { GridPattern } from "./GridPattern"; interface DocumentUploadTabProps { @@ -168,9 +169,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) { `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`, { method: "POST", - headers: { - Authorization: `Bearer ${window.localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: getAuthHeaders(), body: formData, } ); diff --git a/surfsense_web/components/sources/YouTubeTab.tsx b/surfsense_web/components/sources/YouTubeTab.tsx index 717a4266d..8301f51cc 100644 --- a/surfsense_web/components/sources/YouTubeTab.tsx +++ b/surfsense_web/components/sources/YouTubeTab.tsx @@ -19,6 +19,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; +import { authenticatedFetch } from "@/lib/auth-utils"; const youtubeRegex = /^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/; @@ -66,14 +67,11 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) { const videoUrls = videoTags.map((tag) => tag.text); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ document_type: "YOUTUBE_VIDEO", content: videoUrls, diff --git a/surfsense_web/hooks/use-api-key.ts b/surfsense_web/hooks/use-api-key.ts index 229a8de3e..6f955adc3 100644 --- a/surfsense_web/hooks/use-api-key.ts +++ b/surfsense_web/hooks/use-api-key.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; +import { getBearerToken } from "@/lib/auth-utils"; interface UseApiKeyReturn { apiKey: string | null; @@ -17,7 +18,7 @@ export function useApiKey(): UseApiKeyReturn { // Load API key from localStorage const loadApiKey = () => { try { - const token = localStorage.getItem("surfsense_bearer_token"); + const token = getBearerToken(); setApiKey(token); } catch (error) { console.error("Error loading API key:", error); diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index 7424c7aea..b006401d1 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/hooks/use-documents"; +import { getBearerToken } from "@/lib/auth-utils"; interface UseChatStateProps { search_space_id: string; @@ -22,7 +23,7 @@ export function useChatState({ chat_id }: UseChatStateProps) { const [topK, setTopK] = useState(5); useEffect(() => { - const bearerToken = localStorage.getItem("surfsense_bearer_token"); + const bearerToken = getBearerToken(); setToken(bearerToken); }, []); diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 00be9ef17..899cbb961 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -15,6 +15,7 @@ import { type SearchSourceConnector, useSearchSourceConnectors, } from "@/hooks/use-search-source-connectors"; +import { authenticatedFetch } from "@/lib/auth-utils"; const normalizeListInput = (value: unknown): string[] => { if (Array.isArray(value)) { @@ -178,16 +179,11 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) setIsFetchingRepos(true); setFetchedRepos(null); try { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) throw new Error("No auth token"); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ github_pat: values.github_pat }), } ); diff --git a/surfsense_web/hooks/use-connectors.ts b/surfsense_web/hooks/use-connectors.ts index db0a2618e..830677ea5 100644 --- a/surfsense_web/hooks/use-connectors.ts +++ b/surfsense_web/hooks/use-connectors.ts @@ -1,3 +1,5 @@ +import { authenticatedFetch } from "@/lib/auth-utils"; + // Types for connector API export interface ConnectorConfig { [key: string]: string; @@ -32,14 +34,11 @@ export const getConnectorTypeDisplay = (type: string): string => { export const ConnectorService = { // Create a new connector async createConnector(data: CreateConnectorRequest): Promise { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), } ); @@ -54,13 +53,9 @@ export const ConnectorService = { // Get all connectors async getConnectors(skip = 0, limit = 100): Promise { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors?skip=${skip}&limit=${limit}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "GET" } ); if (!response.ok) { @@ -73,13 +68,9 @@ export const ConnectorService = { // Get a specific connector async getConnector(connectorId: number): Promise { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "GET" } ); if (!response.ok) { @@ -92,14 +83,11 @@ export const ConnectorService = { // Update a connector async updateConnector(connectorId: number, data: CreateConnectorRequest): Promise { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), } ); @@ -114,14 +102,9 @@ export const ConnectorService = { // Delete a connector async deleteConnector(connectorId: number): Promise { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "DELETE" } ); if (!response.ok) { diff --git a/surfsense_web/hooks/use-document-by-chunk.ts b/surfsense_web/hooks/use-document-by-chunk.ts index dd36fcab1..630e810a2 100644 --- a/surfsense_web/hooks/use-document-by-chunk.ts +++ b/surfsense_web/hooks/use-document-by-chunk.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; export interface Chunk { id: number; @@ -49,13 +50,10 @@ export function useDocumentByChunk() { setError(null); setDocument(null); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/by-chunk/${chunkId}`, { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, method: "GET", } ); diff --git a/surfsense_web/hooks/use-document-types.ts b/surfsense_web/hooks/use-document-types.ts index 415e42e90..21c9eb6fe 100644 --- a/surfsense_web/hooks/use-document-types.ts +++ b/surfsense_web/hooks/use-document-types.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { authenticatedFetch } from "@/lib/auth-utils"; export interface DocumentTypeCount { type: string; @@ -23,11 +24,6 @@ export const useDocumentTypes = (searchSpaceId?: number, lazy: boolean = false) try { setIsLoading(true); setError(null); - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } // Build URL with optional search_space_id query parameter const url = new URL( @@ -37,12 +33,9 @@ export const useDocumentTypes = (searchSpaceId?: number, lazy: boolean = false) url.searchParams.append("search_space_id", spaceId.toString()); } - const response = await fetch(url.toString(), { + const response = await authenticatedFetch(url.toString(), { method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, }); if (!response.ok) { diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 21ee959b8..b5c349091 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; import { normalizeListResponse } from "@/lib/pagination"; export interface Document { @@ -78,14 +79,9 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption params.append("document_types", effectiveDocumentTypes.join(",")); } - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -159,14 +155,9 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption params.append("document_types", effectiveDocumentTypes.join(",")); } - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -193,14 +184,9 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption const deleteDocument = useCallback( async (documentId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -228,14 +214,9 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption search_space_id: searchSpaceId.toString(), }); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/hooks/use-llm-configs.ts b/surfsense_web/hooks/use-llm-configs.ts index 0755211c4..7619cc3e4 100644 --- a/surfsense_web/hooks/use-llm-configs.ts +++ b/surfsense_web/hooks/use-llm-configs.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; export interface LLMConfig { id: number; @@ -61,14 +62,9 @@ export function useLLMConfigs(searchSpaceId: number | null) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs?search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -92,14 +88,11 @@ export function useLLMConfigs(searchSpaceId: number | null) { const createLLMConfig = async (config: CreateLLMConfig): Promise => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(config), } ); @@ -122,14 +115,9 @@ export function useLLMConfigs(searchSpaceId: number | null) { const deleteLLMConfig = async (id: number): Promise => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + { method: "DELETE" } ); if (!response.ok) { @@ -151,14 +139,11 @@ export function useLLMConfigs(searchSpaceId: number | null) { config: UpdateLLMConfig ): Promise => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(config), } ); @@ -203,14 +188,9 @@ export function useLLMPreferences(searchSpaceId: number | null) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -239,14 +219,11 @@ export function useLLMPreferences(searchSpaceId: number | null) { } try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(newPreferences), } ); @@ -293,14 +270,9 @@ export function useGlobalLLMConfigs() { const fetchGlobalConfigs = async () => { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/global-llm-configs`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/hooks/use-logs.ts b/surfsense_web/hooks/use-logs.ts index 7defd8345..6ce025e89 100644 --- a/surfsense_web/hooks/use-logs.ts +++ b/surfsense_web/hooks/use-logs.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL"; export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED"; @@ -95,14 +96,9 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { if (options.skip !== undefined) params.append("skip", options.skip.toString()); if (options.limit !== undefined) params.append("limit", options.limit.toString()); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs?${params}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -147,14 +143,14 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { // Function to create a new log const createLog = useCallback(async (logData: Omit) => { try { - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "POST", - body: JSON.stringify(logData), - }); + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs`, + { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify(logData), + } + ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -179,13 +175,10 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { updateData: Partial> ) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "PUT", body: JSON.stringify(updateData), } @@ -212,14 +205,9 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { // Function to delete a log const deleteLog = useCallback(async (logId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -240,14 +228,9 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { // Function to get a single log const getLog = useCallback(async (logId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -287,14 +270,9 @@ export function useLogsSummary(searchSpaceId: number, hours: number = 24) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/search-space/${searchSpaceId}/summary?hours=${hours}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/hooks/use-rbac.ts b/surfsense_web/hooks/use-rbac.ts index 6033f887f..ee3450746 100644 --- a/surfsense_web/hooks/use-rbac.ts +++ b/surfsense_web/hooks/use-rbac.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch, getBearerToken, handleUnauthorized } from "@/lib/auth-utils"; // ============ Types ============ @@ -105,22 +106,11 @@ export function useMembers(searchSpaceId: number) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/members`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); - if (response.status === 401) { - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized"); - } - if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || "Failed to fetch members"); @@ -145,13 +135,10 @@ export function useMembers(searchSpaceId: number) { const updateMemberRole = useCallback( async (membershipId: number, roleId: number | null) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/members/${membershipId}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "PUT", body: JSON.stringify({ role_id: roleId }), } @@ -177,14 +164,9 @@ export function useMembers(searchSpaceId: number) { const removeMember = useCallback( async (membershipId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/members/${membershipId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -205,14 +187,9 @@ export function useMembers(searchSpaceId: number) { const leaveSearchSpace = useCallback(async () => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/members/me`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -251,22 +228,11 @@ export function useRoles(searchSpaceId: number) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); - if (response.status === 401) { - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized"); - } - if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || "Failed to fetch roles"); @@ -291,13 +257,10 @@ export function useRoles(searchSpaceId: number) { const createRole = useCallback( async (roleData: RoleCreate) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "POST", body: JSON.stringify(roleData), } @@ -323,13 +286,10 @@ export function useRoles(searchSpaceId: number) { const updateRole = useCallback( async (roleId: number, roleData: RoleUpdate) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles/${roleId}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "PUT", body: JSON.stringify(roleData), } @@ -355,14 +315,9 @@ export function useRoles(searchSpaceId: number) { const deleteRole = useCallback( async (roleId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles/${roleId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -404,22 +359,11 @@ export function useInvites(searchSpaceId: number) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/invites`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); - if (response.status === 401) { - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized"); - } - if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || "Failed to fetch invites"); @@ -444,13 +388,10 @@ export function useInvites(searchSpaceId: number) { const createInvite = useCallback( async (inviteData: InviteCreate) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/invites`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "POST", body: JSON.stringify(inviteData), } @@ -476,13 +417,10 @@ export function useInvites(searchSpaceId: number) { const updateInvite = useCallback( async (inviteId: number, inviteData: InviteUpdate) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/invites/${inviteId}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "PUT", body: JSON.stringify(inviteData), } @@ -508,14 +446,9 @@ export function useInvites(searchSpaceId: number) { const revokeInvite = useCallback( async (inviteId: number) => { try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/invites/${inviteId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "DELETE", - } + { method: "DELETE" } ); if (!response.ok) { @@ -555,14 +488,9 @@ export function usePermissions() { const fetchPermissions = useCallback(async () => { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/permissions`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { @@ -619,22 +547,11 @@ export function useUserAccess(searchSpaceId: number) { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/my-access`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); - if (response.status === 401) { - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized"); - } - if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || "Failed to fetch access info"); @@ -737,13 +654,10 @@ export function useInviteInfo(inviteCode: string | null) { } try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/invites/accept`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, + headers: { "Content-Type": "application/json" }, method: "POST", body: JSON.stringify({ invite_code: inviteCode }), } diff --git a/surfsense_web/hooks/use-search-source-connectors.ts b/surfsense_web/hooks/use-search-source-connectors.ts index 41b5f5115..22c5b3553 100644 --- a/surfsense_web/hooks/use-search-source-connectors.ts +++ b/surfsense_web/hooks/use-search-source-connectors.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { authenticatedFetch, getBearerToken, handleUnauthorized } from "@/lib/auth-utils"; export interface SearchSourceConnector { id: number; @@ -66,11 +67,6 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: try { setIsLoading(true); setError(null); - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } // Build URL with optional search_space_id query parameter const url = new URL( @@ -80,12 +76,9 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: url.searchParams.append("search_space_id", spaceId.toString()); } - const response = await fetch(url.toString(), { + const response = await authenticatedFetch(url.toString(), { method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, }); if (!response.ok) { @@ -176,24 +169,15 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: spaceId: number ) => { try { - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } - // Add search_space_id as a query parameter const url = new URL( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors` ); url.searchParams.append("search_space_id", spaceId.toString()); - const response = await fetch(url.toString(), { + const response = await authenticatedFetch(url.toString(), { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(connectorData), }); @@ -222,20 +206,11 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: > ) => { try { - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } - - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(connectorData), } ); @@ -262,20 +237,11 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: */ const deleteConnector = async (connectorId: number) => { try { - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } - - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, { method: "DELETE", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, } ); @@ -302,12 +268,6 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: endDate?: string ) => { try { - const token = localStorage.getItem("surfsense_bearer_token"); - - if (!token) { - throw new Error("No authentication token found"); - } - // Build query parameters const params = new URLSearchParams({ search_space_id: searchSpaceId.toString(), @@ -319,16 +279,13 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: params.append("end_date", endDate); } - const response = await fetch( + const response = await authenticatedFetch( `${ process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL }/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, } ); diff --git a/surfsense_web/hooks/use-search-space.ts b/surfsense_web/hooks/use-search-space.ts index 46ff64187..849aad413 100644 --- a/surfsense_web/hooks/use-search-space.ts +++ b/surfsense_web/hooks/use-search-space.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; interface SearchSpace { created_at: string; @@ -29,23 +30,11 @@ export function useSearchSpace({ searchSpaceId, autoFetch = true }: UseSearchSpa if (typeof window === "undefined") return; setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); - if (response.status === 401) { - // Clear token and redirect to home - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized: Redirecting to login page"); - } - if (!response.ok) { throw new Error(`Failed to fetch search space: ${response.status}`); } diff --git a/surfsense_web/hooks/use-search-spaces.ts b/surfsense_web/hooks/use-search-spaces.ts index f69144081..03a87881c 100644 --- a/surfsense_web/hooks/use-search-spaces.ts +++ b/surfsense_web/hooks/use-search-spaces.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; interface SearchSpace { id: number; @@ -23,19 +24,14 @@ export function useSearchSpaces() { const fetchSearchSpaces = async () => { try { setLoading(true); - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { - toast.error("Not authenticated"); - throw new Error("Not authenticated"); + toast.error("Failed to fetch search spaces"); + throw new Error("Failed to fetch search spaces"); } const data = await response.json(); @@ -56,19 +52,14 @@ export function useSearchSpaces() { const refreshSearchSpaces = async () => { setLoading(true); try { - const response = await fetch( + const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } + { method: "GET" } ); if (!response.ok) { - toast.error("Not authenticated"); - throw new Error("Not authenticated"); + toast.error("Failed to fetch search spaces"); + throw new Error("Failed to fetch search spaces"); } const data = await response.json(); diff --git a/surfsense_web/hooks/use-user.ts b/surfsense_web/hooks/use-user.ts index 23a23237b..e81ac350b 100644 --- a/surfsense_web/hooks/use-user.ts +++ b/surfsense_web/hooks/use-user.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { authenticatedFetch } from "@/lib/auth-utils"; interface User { id: string; @@ -25,19 +26,10 @@ export function useUser() { if (typeof window === "undefined") return; setLoading(true); - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/users/me`, { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - }); - - if (response.status === 401) { - // Clear token and redirect to home - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized: Redirecting to login page"); - } + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/users/me`, + { method: "GET" } + ); if (!response.ok) { throw new Error(`Failed to fetch user: ${response.status}`); diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index 51b1f69fb..3013be70a 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -1,5 +1,5 @@ -import { th } from "date-fns/locale"; import type z from "zod"; +import { getBearerToken, handleUnauthorized } from "../auth-utils"; import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error"; enum ResponseType { @@ -132,6 +132,8 @@ class BaseApiService { switch (response.status) { case 401: + // Use centralized auth handler for 401 responses + handleUnauthorized(); throw new AuthenticationError( "You are not authenticated. Please login again.", response.status, @@ -261,6 +263,6 @@ class BaseApiService { } export const baseApiService = new BaseApiService( - typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "", + typeof window !== "undefined" ? getBearerToken() || "" : "", process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "" ); diff --git a/surfsense_web/lib/auth-utils.ts b/surfsense_web/lib/auth-utils.ts new file mode 100644 index 000000000..c1dc7194b --- /dev/null +++ b/surfsense_web/lib/auth-utils.ts @@ -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): Record { + 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 { + const { skipAuthRedirect = false, ...fetchOptions } = options || {}; + + const headers = getAuthHeaders(fetchOptions.headers as Record); + + 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 = + | { 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( + url: string, + options?: RequestInit & { skipAuthRedirect?: boolean } +): Promise> { + 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", + }; + } +}