authReady callback simplify

This commit is contained in:
Igor Loskutov
2025-09-02 14:00:00 -04:00
parent 11ed585cea
commit 5ffc312d4a
7 changed files with 99 additions and 83 deletions

View File

@@ -8,10 +8,9 @@ export default function AuthWrapper({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { isAuthReady, isLoading } = useAuthReady(); const { isLoading } = useAuthReady();
// Show spinner while auth is loading if (isLoading) {
if (isLoading || !isAuthReady) {
return ( return (
<Flex <Flex
flexDir="column" flexDir="column"

View File

@@ -0,0 +1,54 @@
"use client";
import { createContext, useContext, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { configureApiAuth } from "./apiClient";
import { CustomSession } from "./types";
type AuthContextType =
| { status: "loading" }
| { status: "unauthenticated"; error?: string }
| {
status: "authenticated";
accessToken: string;
accessTokenExpires: number;
};
const AuthContext = createContext<AuthContextType | undefined>(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 (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View File

@@ -41,6 +41,7 @@ client.use({
}, },
}); });
// the function contract: lightweight, idempotent
export const configureApiAuth = (token: string | null | undefined) => { export const configureApiAuth = (token: string | null | undefined) => {
currentAuthToken = token; currentAuthToken = token;
authConfigured = true; authConfigured = true;

View File

@@ -23,7 +23,7 @@ import useAuthReady from "./useAuthReady";
const STALE_TIME = 500; const STALE_TIME = 500;
export function useRoomsList(page: number = 1) { export function useRoomsList(page: number = 1) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -34,7 +34,7 @@ export function useRoomsList(page: number = 1) {
}, },
}, },
{ {
enabled: isAuthReady, enabled: isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
@@ -51,7 +51,7 @@ export function useTranscriptsSearch(
source_kind?: SourceKind; source_kind?: SourceKind;
} = {}, } = {},
) { ) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -68,7 +68,7 @@ export function useTranscriptsSearch(
}, },
}, },
{ {
enabled: isAuthReady, enabled: isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
@@ -101,7 +101,7 @@ export function useTranscriptProcess() {
} }
export function useTranscriptGet(transcriptId: string | null) { export function useTranscriptGet(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -114,7 +114,7 @@ export function useTranscriptGet(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
@@ -169,22 +169,22 @@ export function useRoomDelete() {
} }
export function useZulipStreams() { export function useZulipStreams() {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
"/v1/zulip/streams", "/v1/zulip/streams",
{}, {},
{ {
enabled: isAuthReady, enabled: isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
} }
export function useZulipTopics(streamId: number | null) { export function useZulipTopics(streamId: number | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
const enabled = !!streamId && isAuthReady; const enabled = !!streamId && isAuthenticated;
return $api.useQuery( return $api.useQuery(
"get", "get",
"/v1/zulip/streams/{stream_id}/topics", "/v1/zulip/streams/{stream_id}/topics",
@@ -262,7 +262,7 @@ export function useTranscriptUploadAudio() {
} }
export function useTranscriptWaveform(transcriptId: string | null) { export function useTranscriptWaveform(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -273,14 +273,14 @@ export function useTranscriptWaveform(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
} }
export function useTranscriptMP3(transcriptId: string | null) { export function useTranscriptMP3(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -291,14 +291,14 @@ export function useTranscriptMP3(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
} }
export function useTranscriptTopics(transcriptId: string | null) { export function useTranscriptTopics(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -309,14 +309,14 @@ export function useTranscriptTopics(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
} }
export function useTranscriptTopicsWithWords(transcriptId: string | null) { export function useTranscriptTopicsWithWords(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -327,7 +327,7 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
@@ -337,7 +337,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker(
transcriptId: string | null, transcriptId: string | null,
topicId: string | null, topicId: string | null,
) { ) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -351,14 +351,14 @@ export function useTranscriptTopicsWithWordsPerSpeaker(
}, },
}, },
{ {
enabled: !!transcriptId && !!topicId && isAuthReady, enabled: !!transcriptId && !!topicId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );
} }
export function useTranscriptParticipants(transcriptId: string | null) { export function useTranscriptParticipants(transcriptId: string | null) {
const { isAuthReady } = useAuthReady(); const { isAuthenticated } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
@@ -369,7 +369,7 @@ export function useTranscriptParticipants(transcriptId: string | null) {
}, },
}, },
{ {
enabled: !!transcriptId && isAuthReady, enabled: !!transcriptId && isAuthenticated,
staleTime: STALE_TIME, staleTime: STALE_TIME,
}, },
); );

View File

@@ -80,17 +80,19 @@ export const authOptions: AuthOptions = {
return await lockedRefreshAccessToken(token); return await lockedRefreshAccessToken(token);
}, },
async session({ session, token }) { async session({ session, token }) {
// TODO no as
const extendedToken = token as JWTWithAccessToken; const extendedToken = token as JWTWithAccessToken;
const customSession = session as CustomSession; return {
customSession.accessToken = extendedToken.accessToken; ...session,
customSession.accessTokenExpires = extendedToken.accessTokenExpires; accessToken: extendedToken.accessToken,
customSession.error = extendedToken.error; accessTokenExpires: extendedToken.accessTokenExpires,
customSession.user = { error: extendedToken.error,
id: extendedToken.sub, user: {
name: extendedToken.name, id: extendedToken.sub,
email: extendedToken.email, name: extendedToken.name,
}; email: extendedToken.email,
return customSession; },
} satisfies CustomSession;
}, },
}, },
}; };

View File

@@ -1,53 +1,13 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useAuth } from "./AuthProvider";
import useSessionStatus from "./useSessionStatus";
import { isAuthConfigured } from "./apiClient";
/** // TODO
* 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.
*/
export default function useAuthReady() { export default function useAuthReady() {
const status = useSessionStatus(); const auth = useAuth();
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]);
return { return {
isAuthReady: authReady, isAuthenticated: auth.status === "authenticated",
isLoading: status === "loading", isLoading: auth.status === "loading",
isAuthenticated,
}; };
} }

View File

@@ -8,20 +8,20 @@ import { Toaster } from "./components/ui/toaster";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient"; import { queryClient } from "./lib/queryClient";
import { ApiAuthProvider } from "./lib/ApiAuthProvider"; import { AuthProvider } from "./lib/AuthProvider";
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<NuqsAdapter> <NuqsAdapter>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ApiAuthProvider> <AuthProvider>
<ChakraProvider value={system}> <ChakraProvider value={system}>
<WherebyProvider> <WherebyProvider>
{children} {children}
<Toaster /> <Toaster />
</WherebyProvider> </WherebyProvider>
</ChakraProvider> </ChakraProvider>
</ApiAuthProvider> </AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</NuqsAdapter> </NuqsAdapter>
); );