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;
}) {
const { isAuthReady, isLoading } = useAuthReady();
const { isLoading } = useAuthReady();
// Show spinner while auth is loading
if (isLoading || !isAuthReady) {
if (isLoading) {
return (
<Flex
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) => {
currentAuthToken = token;
authConfigured = true;

View File

@@ -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,
},
);

View File

@@ -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;
},
},
};

View File

@@ -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",
};
}

View File

@@ -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 (
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<ApiAuthProvider>
<AuthProvider>
<ChakraProvider value={system}>
<WherebyProvider>
{children}
<Toaster />
</WherebyProvider>
</ChakraProvider>
</ApiAuthProvider>
</AuthProvider>
</QueryClientProvider>
</NuqsAdapter>
);