diff --git a/www/app/(app)/AuthGuard.tsx b/www/app/(app)/AuthGuard.tsx new file mode 100644 index 00000000..19af12e7 --- /dev/null +++ b/www/app/(app)/AuthGuard.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { signIn } from "next-auth/react"; +import useSessionStatus from "../lib/useSessionStatus"; +import { Flex, Spinner } from "@chakra-ui/react"; + +interface AuthGuardProps { + children: React.ReactNode; + requireAuth?: boolean; +} + +// Routes that should be accessible without authentication +const PUBLIC_ROUTES = ["/transcripts/new"]; + +export default function AuthGuard({ + children, + requireAuth = true, +}: AuthGuardProps) { + const { isAuthenticated, isLoading, status } = useSessionStatus(); + const router = useRouter(); + const pathname = usePathname(); + + // Check if current route is public + const isPublicRoute = PUBLIC_ROUTES.some((route) => + pathname.startsWith(route), + ); + + useEffect(() => { + // Don't require auth for public routes + if (isPublicRoute) return; + + // Only redirect if we're sure the user is not authenticated and auth is required + if (!isLoading && requireAuth && status === "unauthenticated") { + // Instead of redirecting to /login, trigger NextAuth signIn + signIn("authentik"); + } + }, [isLoading, requireAuth, status, isPublicRoute]); + + // For public routes, always show content + if (isPublicRoute) { + return <>{children}; + } + + // Show loading spinner while checking authentication + if ( + isLoading || + (requireAuth && !isAuthenticated && status !== "unauthenticated") + ) { + return ( + + + + ); + } + + // If authentication is not required or user is authenticated, show content + if (!requireAuth || isAuthenticated) { + return <>{children}; + } + + // Don't render anything while redirecting + return null; +} diff --git a/www/app/(app)/layout.tsx b/www/app/(app)/layout.tsx index 053df5a7..749ce189 100644 --- a/www/app/(app)/layout.tsx +++ b/www/app/(app)/layout.tsx @@ -6,6 +6,7 @@ import About from "../(aboutAndPrivacy)/about"; import Privacy from "../(aboutAndPrivacy)/privacy"; import UserInfo from "../(auth)/userInfo"; import { RECORD_A_MEETING_URL } from "../lib/constants"; +import AuthGuard from "./AuthGuard"; export default async function AppLayout({ children, @@ -90,7 +91,7 @@ export default async function AppLayout({ - {children} + {children} ); } diff --git a/www/app/lib/ApiAuthProvider.tsx b/www/app/lib/ApiAuthProvider.tsx index 8e27f6b7..fd8bdc96 100644 --- a/www/app/lib/ApiAuthProvider.tsx +++ b/www/app/lib/ApiAuthProvider.tsx @@ -1,53 +1,13 @@ "use client"; -import { useEffect, useContext, useRef } from "react"; -import { client, configureApiAuth } from "./apiClient"; +import { useEffect } from "react"; +import { configureApiAuth } from "./apiClient"; import useSessionAccessToken from "./useSessionAccessToken"; -import { DomainContext } from "../domainContext"; -// Store the current API URL globally -let currentApiUrl: string | null = null; - -// Set up base URL middleware once -const baseUrlMiddlewareSetup = () => { - client.use({ - onRequest({ request }) { - if (currentApiUrl) { - // Update the base URL for all requests - const url = new URL(request.url); - const apiUrl = new URL(currentApiUrl); - url.protocol = apiUrl.protocol; - url.host = apiUrl.host; - url.port = apiUrl.port; - return new Request(url.toString(), request); - } - return request; - }, - }); -}; - -// Initialize base URL middleware once -if (typeof window !== "undefined") { - baseUrlMiddlewareSetup(); -} +// Note: Base URL is now configured directly in apiClient.tsx export function ApiAuthProvider({ children }: { children: React.ReactNode }) { const { accessToken } = useSessionAccessToken(); - const { api_url } = useContext(DomainContext); - const initialized = useRef(false); - - // Initialize middleware once on client side - useEffect(() => { - if (!initialized.current && typeof window !== "undefined") { - baseUrlMiddlewareSetup(); - initialized.current = true; - } - }, []); - - useEffect(() => { - // Update the global API URL - currentApiUrl = api_url; - }, [api_url]); useEffect(() => { // Configure authentication diff --git a/www/app/lib/api-hooks.ts b/www/app/lib/api-hooks.ts index 02ae66da..caa8e47c 100644 --- a/www/app/lib/api-hooks.ts +++ b/www/app/lib/api-hooks.ts @@ -4,16 +4,26 @@ import { $api } from "./apiClient"; import { useError } from "../(errors)/errorContext"; import { useQueryClient } from "@tanstack/react-query"; import type { paths } from "../reflector-api"; +import useSessionStatus from "./useSessionStatus"; // Rooms hooks export function useRoomsList(page: number = 1) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); - return $api.useQuery("get", "/v1/rooms", { - params: { - query: { page }, + return $api.useQuery( + "get", + "/v1/rooms", + { + params: { + query: { page }, + }, }, - }); + { + // Only fetch when authenticated + enabled: isAuthenticated && !isLoading, + }, + ); } // Transcripts hooks @@ -27,18 +37,27 @@ export function useTranscriptsSearch( } = {}, ) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); - return $api.useQuery("get", "/v1/transcripts/search", { - params: { - query: { - q, - limit: options.limit, - offset: options.offset, - room_id: options.room_id, - source_kind: options.source_kind as any, + return $api.useQuery( + "get", + "/v1/transcripts/search", + { + params: { + query: { + q, + limit: options.limit, + offset: options.offset, + room_id: options.room_id, + source_kind: options.source_kind as any, + }, }, }, - }); + { + // Only fetch when authenticated + enabled: isAuthenticated && !isLoading, + }, + ); } export function useTranscriptDelete() { @@ -72,6 +91,7 @@ export function useTranscriptProcess() { export function useTranscriptGet(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -84,7 +104,8 @@ export function useTranscriptGet(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + // Only fetch when authenticated and transcriptId is provided + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } @@ -141,25 +162,32 @@ export function useRoomDelete() { // Zulip hooks - NOTE: These endpoints are not in the OpenAPI spec yet export function useZulipStreams() { const { setError } = useError(); - - // @ts-ignore - Zulip endpoint not in OpenAPI spec - return $api.useQuery("get", "/v1/zulip/get-streams" as any, {}); -} - -export function useZulipTopics(streamId: number | null) { - const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); // @ts-ignore - Zulip endpoint not in OpenAPI spec return $api.useQuery( "get", - "/v1/zulip/get-topics" as any, + "/v1/zulip/streams" as any, + {}, { - params: { - query: { stream_id: streamId || 0 }, - }, + // Only fetch when authenticated + enabled: isAuthenticated && !isLoading, }, + ); +} + +export function useZulipTopics(streamId: number | null) { + const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); + + // @ts-ignore - Zulip endpoint not in OpenAPI spec + return $api.useQuery( + "get", + streamId ? (`/v1/zulip/streams/${streamId}/topics` as any) : null, + {}, { - enabled: !!streamId, + // Only fetch when authenticated and streamId is provided + enabled: !!streamId && isAuthenticated && !isLoading, }, ); } @@ -233,6 +261,7 @@ export function useTranscriptUploadAudio() { // Transcript queries export function useTranscriptWaveform(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -243,13 +272,14 @@ export function useTranscriptWaveform(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } export function useTranscriptMP3(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -260,13 +290,14 @@ export function useTranscriptMP3(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } export function useTranscriptTopics(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -277,13 +308,14 @@ export function useTranscriptTopics(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } export function useTranscriptTopicsWithWords(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -294,7 +326,7 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } @@ -304,6 +336,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( topicId: string | null, ) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -317,7 +350,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( }, }, { - enabled: !!transcriptId && !!topicId, + enabled: !!transcriptId && !!topicId && isAuthenticated && !isLoading, }, ); } @@ -325,6 +358,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( // Participant operations export function useTranscriptParticipants(transcriptId: string | null) { const { setError } = useError(); + const { isAuthenticated, isLoading } = useSessionStatus(); return $api.useQuery( "get", @@ -335,7 +369,7 @@ export function useTranscriptParticipants(transcriptId: string | null) { }, }, { - enabled: !!transcriptId, + enabled: !!transcriptId && isAuthenticated && !isLoading, }, ); } diff --git a/www/app/lib/apiClient.tsx b/www/app/lib/apiClient.tsx index 2b320267..0a167f68 100644 --- a/www/app/lib/apiClient.tsx +++ b/www/app/lib/apiClient.tsx @@ -10,10 +10,10 @@ import { } from "@tanstack/react-query"; import createFetchClient from "openapi-react-query"; -// Create the base openapi-fetch client +// Create the base openapi-fetch client with a default URL +// The actual URL will be set via middleware in ApiAuthProvider export const client = createClient({ - // Base URL will be set dynamically via middleware - baseUrl: "", + baseUrl: "http://127.0.0.1:1250", headers: { "Content-Type": "application/json", },