diff --git a/www/app/(app)/AuthWrapper.tsx b/www/app/(app)/AuthWrapper.tsx index cdd85cca..ac9f1bbd 100644 --- a/www/app/(app)/AuthWrapper.tsx +++ b/www/app/(app)/AuthWrapper.tsx @@ -8,10 +8,9 @@ export default function AuthWrapper({ }: { children: React.ReactNode; }) { - const { isAuthReady, isLoading } = useAuthReady(); + const { isLoading } = useAuthReady(); - // Show spinner while auth is loading - if (isLoading || !isAuthReady) { + if (isLoading) { return ( (undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const { data: session, status } = useNextAuthSession(); + const customSession = session as CustomSession; + + if (session) { + debugger; + } + + const contextValue: AuthContextType = + status === "loading" + ? { status: "loading" as const } + : customSession?.accessToken + ? { + status: "authenticated" as const, + accessToken: customSession.accessToken, + accessTokenExpires: customSession.accessTokenExpires, + } + : { status: "unauthenticated" as const }; + + // not useEffect, we need it ASAP + configureApiAuth( + contextValue.status === "authenticated" ? contextValue.accessToken : null, + ); + + return ( + {children} + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/www/app/lib/apiClient.tsx b/www/app/lib/apiClient.tsx index ad41012e..fe2a271b 100644 --- a/www/app/lib/apiClient.tsx +++ b/www/app/lib/apiClient.tsx @@ -41,6 +41,7 @@ client.use({ }, }); +// the function contract: lightweight, idempotent export const configureApiAuth = (token: string | null | undefined) => { currentAuthToken = token; authConfigured = true; diff --git a/www/app/lib/apiHooks.ts b/www/app/lib/apiHooks.ts index 519512a8..58f600ce 100644 --- a/www/app/lib/apiHooks.ts +++ b/www/app/lib/apiHooks.ts @@ -23,7 +23,7 @@ import useAuthReady from "./useAuthReady"; const STALE_TIME = 500; export function useRoomsList(page: number = 1) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -34,7 +34,7 @@ export function useRoomsList(page: number = 1) { }, }, { - enabled: isAuthReady, + enabled: isAuthenticated, staleTime: STALE_TIME, }, ); @@ -51,7 +51,7 @@ export function useTranscriptsSearch( source_kind?: SourceKind; } = {}, ) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -68,7 +68,7 @@ export function useTranscriptsSearch( }, }, { - enabled: isAuthReady, + enabled: isAuthenticated, staleTime: STALE_TIME, }, ); @@ -101,7 +101,7 @@ export function useTranscriptProcess() { } export function useTranscriptGet(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -114,7 +114,7 @@ export function useTranscriptGet(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); @@ -169,22 +169,22 @@ export function useRoomDelete() { } export function useZulipStreams() { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", "/v1/zulip/streams", {}, { - enabled: isAuthReady, + enabled: isAuthenticated, staleTime: STALE_TIME, }, ); } export function useZulipTopics(streamId: number | null) { - const { isAuthReady } = useAuthReady(); - const enabled = !!streamId && isAuthReady; + const { isAuthenticated } = useAuthReady(); + const enabled = !!streamId && isAuthenticated; return $api.useQuery( "get", "/v1/zulip/streams/{stream_id}/topics", @@ -262,7 +262,7 @@ export function useTranscriptUploadAudio() { } export function useTranscriptWaveform(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -273,14 +273,14 @@ export function useTranscriptWaveform(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); } export function useTranscriptMP3(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -291,14 +291,14 @@ export function useTranscriptMP3(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); } export function useTranscriptTopics(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -309,14 +309,14 @@ export function useTranscriptTopics(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); } export function useTranscriptTopicsWithWords(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -327,7 +327,7 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); @@ -337,7 +337,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( transcriptId: string | null, topicId: string | null, ) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -351,14 +351,14 @@ export function useTranscriptTopicsWithWordsPerSpeaker( }, }, { - enabled: !!transcriptId && !!topicId && isAuthReady, + enabled: !!transcriptId && !!topicId && isAuthenticated, staleTime: STALE_TIME, }, ); } export function useTranscriptParticipants(transcriptId: string | null) { - const { isAuthReady } = useAuthReady(); + const { isAuthenticated } = useAuthReady(); return $api.useQuery( "get", @@ -369,7 +369,7 @@ export function useTranscriptParticipants(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && isAuthReady, + enabled: !!transcriptId && isAuthenticated, staleTime: STALE_TIME, }, ); diff --git a/www/app/lib/auth.ts b/www/app/lib/auth.ts index 54c8c4ef..7a93b105 100644 --- a/www/app/lib/auth.ts +++ b/www/app/lib/auth.ts @@ -80,17 +80,19 @@ export const authOptions: AuthOptions = { return await lockedRefreshAccessToken(token); }, async session({ session, token }) { + // TODO no as const extendedToken = token as JWTWithAccessToken; - const customSession = session as CustomSession; - customSession.accessToken = extendedToken.accessToken; - customSession.accessTokenExpires = extendedToken.accessTokenExpires; - customSession.error = extendedToken.error; - customSession.user = { - id: extendedToken.sub, - name: extendedToken.name, - email: extendedToken.email, - }; - return customSession; + return { + ...session, + accessToken: extendedToken.accessToken, + accessTokenExpires: extendedToken.accessTokenExpires, + error: extendedToken.error, + user: { + id: extendedToken.sub, + name: extendedToken.name, + email: extendedToken.email, + }, + } satisfies CustomSession; }, }, }; diff --git a/www/app/lib/useAuthReady.ts b/www/app/lib/useAuthReady.ts index fc3493b0..f267b338 100644 --- a/www/app/lib/useAuthReady.ts +++ b/www/app/lib/useAuthReady.ts @@ -1,53 +1,13 @@ "use client"; -import { useState, useEffect } from "react"; -import useSessionStatus from "./useSessionStatus"; -import { isAuthConfigured } from "./apiClient"; +import { useAuth } from "./AuthProvider"; -/** - * Hook to check if authentication is fully ready. - * This ensures both the session is authenticated AND the API client token is configured. - * Prevents race conditions where React Query fires requests before the token is set. - */ +// TODO export default function useAuthReady() { - const status = useSessionStatus(); - const isAuthenticated = status === "authenticated"; - const [authReady, setAuthReady] = useState(false); - - useEffect(() => { - let ready_ = false; - // Check if both session is authenticated and token is configured - const checkAuthReady = () => { - const ready = isAuthenticated && isAuthConfigured(); - ready_ = ready; - setAuthReady(ready); - }; - - // Check immediately - checkAuthReady(); - - // Also check periodically for a short time to catch async updates - const interval = setInterval(checkAuthReady, 100); - - // Stop checking after 2 seconds (auth should be ready by then) - const timeout = setTimeout(() => { - if (ready_) { - clearInterval(interval); - return; - } else { - console.warn("Auth not ready after 2 seconds"); - } - }, 2000); - - return () => { - clearInterval(interval); - clearTimeout(timeout); - }; - }, [isAuthenticated]); + const auth = useAuth(); return { - isAuthReady: authReady, - isLoading: status === "loading", - isAuthenticated, + isAuthenticated: auth.status === "authenticated", + isLoading: auth.status === "loading", }; } diff --git a/www/app/providers.tsx b/www/app/providers.tsx index 8149ec6f..090ad161 100644 --- a/www/app/providers.tsx +++ b/www/app/providers.tsx @@ -8,20 +8,20 @@ import { Toaster } from "./components/ui/toaster"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { QueryClientProvider } from "@tanstack/react-query"; import { queryClient } from "./lib/queryClient"; -import { ApiAuthProvider } from "./lib/ApiAuthProvider"; +import { AuthProvider } from "./lib/AuthProvider"; export function Providers({ children }: { children: React.ReactNode }) { return ( - + {children} - + );