diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index b1525a9db..d3d88a47a 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -35,10 +35,10 @@ import { } from "@/components/ui/card"; import { Spotlight } from "@/components/ui/spotlight"; import { Tilt } from "@/components/ui/tilt"; -import { useUser } from "@/hooks"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; /** * Formats a date string into a readable format @@ -159,8 +159,7 @@ const DashboardPage = () => { const { data: searchSpaces = [], isLoading: loading, error, refetch: refreshSearchSpaces } = useAtomValue(searchSpacesAtom); const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom); - // Fetch user details - const { user, loading: isLoadingUser, error: userError } = useUser(); + const { data: user, isPending: isLoadingUser, error: userError } = useAtomValue(currentUserAtom); // Create user object for UserDropdown const customUser = { diff --git a/surfsense_web/atoms/user/user-query.atoms.ts b/surfsense_web/atoms/user/user-query.atoms.ts new file mode 100644 index 000000000..ea3e7ec49 --- /dev/null +++ b/surfsense_web/atoms/user/user-query.atoms.ts @@ -0,0 +1,13 @@ +import { atomWithQuery } from "jotai-tanstack-query"; +import { userApiService } from "@/lib/apis/user-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; + +export const currentUserAtom = atomWithQuery(() => { + return { + queryKey: cacheKeys.user.current(), + staleTime: 5 * 60 * 1000, // 5 minutes + queryFn: async () => { + return userApiService.getMe(); + }, + }; +}); diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index fce8697f7..55bc8331c 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -17,10 +17,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useUser } from "@/hooks"; import { useQuery } from "@tanstack/react-query"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; interface AppSidebarProviderProps { searchSpaceId: string; @@ -68,7 +68,7 @@ export function AppSidebarProvider({ enabled: !!searchSpaceId, }); - const { user } = useUser(); + const { data: user } = useAtomValue(currentUserAtom); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null); diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index cd42e6fe8..6f86d3808 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -38,7 +38,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { useUser } from "@/hooks/use-user"; +import { useAtomValue } from "jotai"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; /** * Generates a consistent color based on a string (email) @@ -262,7 +263,7 @@ export const AppSidebar = memo(function AppSidebar({ }: AppSidebarProps) { const router = useRouter(); const { theme, setTheme } = useTheme(); - const { user, loading: isLoadingUser } = useUser(); + const { data: user, isPending: isLoadingUser } = useAtomValue(currentUserAtom); const [isClient, setIsClient] = useState(false); useEffect(() => { diff --git a/surfsense_web/contracts/types/user.types.ts b/surfsense_web/contracts/types/user.types.ts new file mode 100644 index 000000000..f5df17694 --- /dev/null +++ b/surfsense_web/contracts/types/user.types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const user = z.object({ + id: z.string().uuid(), + email: z.string().email(), + is_active: z.boolean(), + is_superuser: z.boolean(), + is_verified: z.boolean(), + pages_limit: z.number(), + pages_used: z.number(), +}); + +/** + * Get current user + */ +export const getMeResponse = user; + +export type User = z.infer; +export type GetMeResponse = z.infer; diff --git a/surfsense_web/hooks/index.ts b/surfsense_web/hooks/index.ts index f7ef22534..d2a4ff6bf 100644 --- a/surfsense_web/hooks/index.ts +++ b/surfsense_web/hooks/index.ts @@ -1,4 +1,3 @@ export * from "./use-logs"; export * from "./use-rbac"; export * from "./use-search-source-connectors"; -export * from "./use-user"; diff --git a/surfsense_web/hooks/use-user.ts b/surfsense_web/hooks/use-user.ts deleted file mode 100644 index e81ac350b..000000000 --- a/surfsense_web/hooks/use-user.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { authenticatedFetch } from "@/lib/auth-utils"; - -interface User { - id: string; - email: string; - is_active: boolean; - is_superuser: boolean; - is_verified: boolean; - pages_limit: number; - pages_used: number; -} - -export function useUser() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchUser = async () => { - try { - // Only run on client-side - if (typeof window === "undefined") return; - - setLoading(true); - 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}`); - } - - const data = await response.json(); - setUser(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch user"); - console.error("Error fetching user:", err); - } finally { - setLoading(false); - } - }; - - fetchUser(); - }, []); - - return { user, loading, error }; -} diff --git a/surfsense_web/lib/apis/user-api.service.ts b/surfsense_web/lib/apis/user-api.service.ts new file mode 100644 index 000000000..ea46ac116 --- /dev/null +++ b/surfsense_web/lib/apis/user-api.service.ts @@ -0,0 +1,13 @@ +import { getMeResponse } from "@/contracts/types/user.types"; +import { baseApiService } from "./base-api.service"; + +class UserApiService { + /** + * Get current authenticated user + */ + getMe = async () => { + return baseApiService.get(`/users/me`, getMeResponse); + }; +} + +export const userApiService = new UserApiService(); diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index 797c40b65..eb2c4972a 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -40,5 +40,8 @@ export const cacheKeys = { ["search-spaces", ...(queries ? Object.values(queries) : [])] as const, detail: (searchSpaceId: string) => ["search-spaces", searchSpaceId] as const, communityPrompts: ["search-spaces", "community-prompts"] as const, - } -}; + }, + user: { + current: () => ["user", "me"] as const, + }, +}; \ No newline at end of file