From 9ce5a8e5e262032c8949dceec674b46e9d1a6b8c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 18 Feb 2026 13:16:49 +0530 Subject: [PATCH] fix: Fixes #139 --- ui/src/app/api/auth/oss/route.ts | 2 +- ui/src/app/files/DocumentList.tsx | 13 +- ui/src/app/files/DocumentUpload.tsx | 9 +- ui/src/app/files/page.tsx | 21 +-- ui/src/app/reports/page.tsx | 23 +-- ui/src/app/superadmin/runs/page.tsx | 33 ++--- ui/src/app/usage/page.tsx | 40 ++--- .../workflow/[workflowId]/RenderWorkflow.tsx | 19 +-- .../[workflowId]/components/EmbedDialog.tsx | 17 +-- .../components/PhoneCallDialog.tsx | 11 +- .../components/WorkflowEditorHeader.tsx | 1 - .../components/WorkflowExecutions.tsx | 17 +-- .../[workflowId]/hooks/useWorkflowState.ts | 36 +---- ui/src/app/workflow/[workflowId]/page.tsx | 11 +- .../[workflowId]/run/[runId]/BrowserCall.tsx | 20 ++- .../[workflowId]/run/[runId]/page.tsx | 23 +-- ui/src/app/workflow/page.tsx | 2 - ui/src/components/MediaPreviewDialog.tsx | 22 ++- ui/src/components/VoiceSelector.tsx | 9 +- .../components/http/credential-selector.tsx | 9 +- ui/src/components/layout/AppLayout.tsx | 69 ++++----- ui/src/components/layout/AppSidebar.tsx | 87 +++++++---- .../components/looptalk/ConversationsList.tsx | 8 +- .../looptalk/CreateTestSessionButton.tsx | 6 +- .../components/workflow-runs/CampaignRuns.tsx | 24 +-- .../workflow-runs/WorkflowRunsTable.tsx | 4 +- ui/src/components/workflow/WorkflowTable.tsx | 11 -- ui/src/context/UserConfigContext.tsx | 35 ++--- ui/src/lib/apiClient.ts | 25 ++++ ui/src/lib/auth/hooks/useAuth.ts | 7 - ui/src/lib/auth/index.ts | 3 +- ui/src/lib/auth/providers/AuthProvider.tsx | 67 +++------ .../auth/providers/LocalProviderWrapper.tsx | 74 ++++++++++ .../auth/providers/StackProviderWrapper.tsx | 8 +- ui/src/lib/auth/services/index.ts | 45 ------ ui/src/lib/auth/services/interface.ts | 23 --- ui/src/lib/auth/services/localAuthService.ts | 109 -------------- ui/src/lib/auth/services/stackAuthService.ts | 139 ------------------ ui/src/lib/files.ts | 14 +- 39 files changed, 338 insertions(+), 758 deletions(-) delete mode 100644 ui/src/lib/auth/hooks/useAuth.ts create mode 100644 ui/src/lib/auth/providers/LocalProviderWrapper.tsx delete mode 100644 ui/src/lib/auth/services/index.ts delete mode 100644 ui/src/lib/auth/services/interface.ts delete mode 100644 ui/src/lib/auth/services/localAuthService.ts delete mode 100644 ui/src/lib/auth/services/stackAuthService.ts diff --git a/ui/src/app/api/auth/oss/route.ts b/ui/src/app/api/auth/oss/route.ts index 3b21806..6679fb4 100644 --- a/ui/src/app/api/auth/oss/route.ts +++ b/ui/src/app/api/auth/oss/route.ts @@ -1,5 +1,5 @@ /* - Helps provide authentication token to LocalAuthService once its loaded + Provides authentication token to LocalProviderWrapper once loaded in the browser */ import { cookies } from 'next/headers'; diff --git a/ui/src/app/files/DocumentList.tsx b/ui/src/app/files/DocumentList.tsx index fd66c32..a6830a7 100644 --- a/ui/src/app/files/DocumentList.tsx +++ b/ui/src/app/files/DocumentList.tsx @@ -16,27 +16,21 @@ import { Skeleton } from '@/components/ui/skeleton'; import logger from '@/lib/logger'; interface DocumentListProps { - accessToken: string; refreshTrigger: number; } -export default function DocumentList({ accessToken, refreshTrigger }: DocumentListProps) { +export default function DocumentList({ refreshTrigger }: DocumentListProps) { const [documents, setDocuments] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [error, setError] = useState(null); const fetchDocuments = useCallback(async () => { - if (!accessToken) return; - try { setIsLoading(true); setError(null); const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({ - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, query: { limit: 100, offset: 0, @@ -54,7 +48,7 @@ export default function DocumentList({ accessToken, refreshTrigger }: DocumentLi } finally { setIsLoading(false); } - }, [accessToken]); + }, []); // Fetch documents on mount and when refreshTrigger changes useEffect(() => { @@ -85,9 +79,6 @@ export default function DocumentList({ accessToken, refreshTrigger }: DocumentLi path: { document_uuid: documentUuid, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (response.error) { diff --git a/ui/src/app/files/DocumentUpload.tsx b/ui/src/app/files/DocumentUpload.tsx index 3117bfd..e9367c3 100644 --- a/ui/src/app/files/DocumentUpload.tsx +++ b/ui/src/app/files/DocumentUpload.tsx @@ -14,14 +14,13 @@ import { Progress } from '@/components/ui/progress'; import logger from '@/lib/logger'; interface DocumentUploadProps { - accessToken: string; onUploadSuccess: () => void; } const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB const ACCEPTED_FILE_TYPES = ['.pdf', '.docx', '.doc', '.txt']; -export default function DocumentUpload({ accessToken, onUploadSuccess }: DocumentUploadProps) { +export default function DocumentUpload({ onUploadSuccess }: DocumentUploadProps) { const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [dragActive, setDragActive] = useState(false); @@ -62,9 +61,6 @@ export default function DocumentUpload({ accessToken, onUploadSuccess }: Documen uploaded_at: new Date().toISOString(), }, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (uploadUrlResponse.error || !uploadUrlResponse.data) { @@ -98,9 +94,6 @@ export default function DocumentUpload({ accessToken, onUploadSuccess }: Documen document_uuid: uploadData.document_uuid, s3_key: uploadData.s3_key, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (processResponse.error) { diff --git a/ui/src/app/files/page.tsx b/ui/src/app/files/page.tsx index dd389e1..249613b 100644 --- a/ui/src/app/files/page.tsx +++ b/ui/src/app/files/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -11,9 +11,8 @@ import DocumentList from "./DocumentList"; import DocumentUpload from "./DocumentUpload"; export default function FilesPage() { - const { user, getAccessToken, redirectToLogin, loading } = useAuth(); + const { user, redirectToLogin, loading } = useAuth(); const [refreshKey, setRefreshKey] = useState(0); - const [accessToken, setAccessToken] = useState(''); // Redirect if not authenticated useEffect(() => { @@ -22,24 +21,12 @@ export default function FilesPage() { } }, [loading, user, redirectToLogin]); - // Get access token - const fetchAccessToken = useCallback(async () => { - if (user) { - const token = await getAccessToken(); - setAccessToken(token); - } - }, [user, getAccessToken]); - - useEffect(() => { - fetchAccessToken(); - }, [fetchAccessToken]); - const handleUploadSuccess = () => { // Trigger refresh of document list setRefreshKey(prev => prev + 1); }; - if (loading || !user || !accessToken) { + if (loading || !user) { return (
@@ -75,7 +62,6 @@ export default function FilesPage() { @@ -92,7 +78,6 @@ export default function FilesPage() { diff --git a/ui/src/app/reports/page.tsx b/ui/src/app/reports/page.tsx index 4d4123c..b65b923 100644 --- a/ui/src/app/reports/page.tsx +++ b/ui/src/app/reports/page.tsx @@ -16,6 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { useUserConfig } from '@/context/UserConfigContext'; +import { useAuth } from '@/lib/auth'; import { DispositionChart } from './components/DispositionChart'; import { DurationChart } from './components/DurationChart'; @@ -55,20 +56,18 @@ export default function ReportsPage() { const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { userConfig, accessToken } = useUserConfig(); + const { userConfig } = useUserConfig(); + const auth = useAuth(); const timezone = userConfig?.timezone || 'America/New_York'; // Fetch workflows on mount useEffect(() => { const fetchWorkflows = async () => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; try { const response = await getWorkflowOptionsApiV1OrganizationsReportsWorkflowsGet({ - headers: { - Authorization: `Bearer ${accessToken}` - } }); if (response.data) { setWorkflows(response.data); @@ -78,12 +77,12 @@ export default function ReportsPage() { } }; fetchWorkflows(); - }, [accessToken]); + }, [auth.isAuthenticated]); // Fetch report data when date or workflow changes useEffect(() => { const fetchReport = async () => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; setLoading(true); setError(null); @@ -98,9 +97,6 @@ export default function ReportsPage() { timezone, ...(workflowId && { workflow_id: workflowId }) }, - headers: { - Authorization: `Bearer ${accessToken}` - } }); if (response.data) { @@ -115,7 +111,7 @@ export default function ReportsPage() { }; fetchReport(); - }, [selectedDate, selectedWorkflow, timezone, accessToken]); + }, [selectedDate, selectedWorkflow, timezone, auth.isAuthenticated]); const handlePreviousDay = () => { setSelectedDate(subDays(selectedDate, 1)); @@ -126,7 +122,7 @@ export default function ReportsPage() { }; const handleDownloadCSV = async () => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; try { const dateStr = format(selectedDate, 'yyyy-MM-dd'); @@ -139,9 +135,6 @@ export default function ReportsPage() { timezone, ...(workflowId && { workflow_id: workflowId }) }, - headers: { - Authorization: `Bearer ${accessToken}` - } }); if (response.data && response.data.length > 0) { diff --git a/ui/src/app/superadmin/runs/page.tsx b/ui/src/app/superadmin/runs/page.tsx index c58961f..4f0f4ee 100644 --- a/ui/src/app/superadmin/runs/page.tsx +++ b/ui/src/app/superadmin/runs/page.tsx @@ -30,7 +30,7 @@ import { } from "@/components/ui/table"; import { Textarea } from '@/components/ui/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useUserConfig } from '@/context/UserConfigContext'; +import { useAuth } from '@/lib/auth'; import{ superadminFilterAttributes } from "@/lib/filterAttributes"; import { decodeFiltersFromURL, encodeFiltersToURL } from '@/lib/filters'; import { impersonateAsSuperadmin } from '@/lib/utils'; @@ -107,10 +107,10 @@ export default function RunsPage() { const [commentText, setCommentText] = useState(''); const [selectedRowId, setSelectedRowId] = useState(null); - const { accessToken } = useUserConfig(); + const auth = useAuth(); // Media preview dialog - const mediaPreview = MediaPreviewDialog({ accessToken }); + const mediaPreview = MediaPreviewDialog(); const fetchRuns = useCallback(async ( page: number, @@ -119,7 +119,7 @@ export default function RunsPage() { sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc' ) => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; // Don't show loading state for auto-refresh to prevent UI flicker if (!isAutoRefresh) { @@ -148,9 +148,6 @@ export default function RunsPage() { ...(sortByParam && { sort_by: sortByParam }), ...(sortOrderParam && { sort_order: sortOrderParam }), }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } }); if (response.data) { @@ -170,7 +167,7 @@ export default function RunsPage() { setIsAutoRefreshing(false); } } - }, [limit, accessToken]); + }, [limit, auth.isAuthenticated]); const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => { const params = new URLSearchParams(); @@ -195,11 +192,11 @@ export default function RunsPage() { }, [router]); useEffect(() => { - // Fetch runs when token is available and when page/sort changes - if (accessToken) { + // Fetch runs when auth is available and when page/sort changes + if (auth.isAuthenticated) { fetchRuns(currentPage, appliedFilters, false, sortBy, sortOrder); } - }, [currentPage, accessToken, appliedFilters, fetchRuns, sortBy, sortOrder]); + }, [currentPage, auth.isAuthenticated, appliedFilters, fetchRuns, sortBy, sortOrder]); // Auto-refresh every 5 seconds when enabled and filters are active useEffect(() => { @@ -262,7 +259,7 @@ export default function RunsPage() { // Save comment function declared outside JSX (requirement #2) const saveAdminComment = useCallback(async () => { - if (commentRunId === null || !accessToken) return; + if (commentRunId === null || !auth.isAuthenticated) return; try { await setAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPost({ path: { @@ -271,9 +268,6 @@ export default function RunsPage() { body: { admin_comment: commentText, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); // Optimistically update UI @@ -284,7 +278,7 @@ export default function RunsPage() { console.error('Failed to set admin comment', err); alert('Failed to save comment. Please try again.'); } - }, [commentRunId, commentText, accessToken]); + }, [commentRunId, commentText, auth.isAuthenticated]); /** * ---------------------------------------------------------------------------------- @@ -308,10 +302,11 @@ export default function RunsPage() { */ const impersonateAndMaybeRedirect = useCallback( async (targetUserId: number | undefined, redirectPath?: string) => { - if (!targetUserId || !accessToken) return; + if (!targetUserId || !auth.isAuthenticated) return; try { + const token = await auth.getAccessToken(); await impersonateAsSuperadmin({ - accessToken: accessToken, + accessToken: token, userId: targetUserId, redirectPath, openInNewTab: true, @@ -321,7 +316,7 @@ export default function RunsPage() { alert('Failed to impersonate the user. Please try again.'); } }, - [accessToken], + [auth], ); if (isLoading && runs.length === 0) { diff --git a/ui/src/app/usage/page.tsx b/ui/src/app/usage/page.tsx index 74d159c..23dac1d 100644 --- a/ui/src/app/usage/page.tsx +++ b/ui/src/app/usage/page.tsx @@ -23,6 +23,7 @@ import { TableRow, } from '@/components/ui/table'; import { useUserConfig } from '@/context/UserConfigContext'; +import { useAuth } from '@/lib/auth'; import { usageFilterAttributes } from '@/lib/filterAttributes'; import { decodeFiltersFromURL, encodeFiltersToURL } from '@/lib/filters'; import { ActiveFilter, DateRangeValue } from '@/types/filters'; @@ -33,7 +34,8 @@ const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone; export default function UsagePage() { const router = useRouter(); const searchParams = useSearchParams(); - const { userConfig, saveUserConfig, loading: userConfigLoading, accessToken, organizationPricing } = useUserConfig(); + const { userConfig, saveUserConfig, loading: userConfigLoading, organizationPricing } = useUserConfig(); + const auth = useAuth(); // Current usage state const [currentUsage, setCurrentUsage] = useState(null); @@ -58,7 +60,7 @@ export default function UsagePage() { }); // Media preview dialog - const mediaPreview = MediaPreviewDialog({ accessToken }); + const mediaPreview = MediaPreviewDialog(); // Timezone state - initialize with empty string to avoid hydration mismatch const localTimezone = getLocalTimezone(); @@ -68,13 +70,9 @@ export default function UsagePage() { // Fetch current usage const fetchCurrentUsage = useCallback(async () => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; try { - const response = await getCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGet({ - headers: { - 'Authorization': `Bearer ${accessToken}`, - } - }); + const response = await getCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGet(); if (response.data) { setCurrentUsage(response.data); @@ -84,11 +82,11 @@ export default function UsagePage() { } finally { setIsLoadingCurrent(false); } - }, [accessToken]); + }, [auth.isAuthenticated]); // Fetch usage history const fetchUsageHistory = useCallback(async (page: number, filters?: ActiveFilter[]) => { - if (!accessToken) return; + if (!auth.isAuthenticated) return; setIsLoadingHistory(true); try { let filterParam = undefined; @@ -132,9 +130,6 @@ export default function UsagePage() { ...(endDate && { end_date: endDate }), ...(filterParam && { filters: filterParam }) }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } }); if (response.data) { @@ -145,19 +140,16 @@ export default function UsagePage() { } finally { setIsLoadingHistory(false); } - }, [accessToken]); + }, [auth.isAuthenticated]); // Fetch daily usage breakdown const fetchDailyUsage = useCallback(async () => { - if (!accessToken || !organizationPricing?.price_per_second_usd) return; + if (!auth.isAuthenticated || !organizationPricing?.price_per_second_usd) return; setIsLoadingDaily(true); try { const response = await getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet({ query: { days: 7 }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } }); if (response.data) { @@ -168,7 +160,7 @@ export default function UsagePage() { } finally { setIsLoadingDaily(false); } - }, [accessToken, organizationPricing]); + }, [auth.isAuthenticated, organizationPricing]); // Handle timezone change const handleTimezoneChange = async (timezone: ITimezoneOption | string) => { @@ -200,20 +192,20 @@ export default function UsagePage() { } }, [userConfig, userConfigLoading, localTimezone]); - // Initial load - fetch when accessToken becomes available + // Initial load - fetch when auth becomes available useEffect(() => { - if (accessToken) { + if (auth.isAuthenticated) { fetchCurrentUsage(); fetchUsageHistory(currentPage, activeFilters); } - }, [accessToken, currentPage, activeFilters, fetchUsageHistory, fetchCurrentUsage]); + }, [auth.isAuthenticated, currentPage, activeFilters, fetchUsageHistory, fetchCurrentUsage]); // Fetch daily usage when organizationPricing becomes available useEffect(() => { - if (accessToken && organizationPricing?.price_per_second_usd) { + if (auth.isAuthenticated && organizationPricing?.price_per_second_usd) { fetchDailyUsage(); } - }, [accessToken, organizationPricing, fetchDailyUsage]); + }, [auth.isAuthenticated, organizationPricing, fetchDailyUsage]); // Update URL with query parameters const updateUrlParams = useCallback((params: { page?: number; filters?: ActiveFilter[] }) => { diff --git a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx index d720d0d..d24112c 100644 --- a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx +++ b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx @@ -58,10 +58,9 @@ interface RenderWorkflowProps { initialTemplateContextVariables?: Record; initialWorkflowConfigurations?: WorkflowConfigurations; user: { id: string; email?: string }; - getAccessToken: () => Promise; } -function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialTemplateContextVariables, initialWorkflowConfigurations, user, getAccessToken }: RenderWorkflowProps) { +function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialTemplateContextVariables, initialWorkflowConfigurations, user }: RenderWorkflowProps) { const [isContextVarsDialogOpen, setIsContextVarsDialogOpen] = useState(false); const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false); const [isDictionaryDialogOpen, setIsDictionaryDialogOpen] = useState(false); @@ -100,18 +99,14 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT initialTemplateContextVariables, initialWorkflowConfigurations, user, - getAccessToken }); // Fetch documents and tools once for the entire workflow useEffect(() => { const fetchData = async () => { try { - const accessToken = await getAccessToken(); - // Fetch documents const documentsResponse = await listDocumentsApiV1KnowledgeBaseDocumentsGet({ - headers: { Authorization: `Bearer ${accessToken}` }, query: { limit: 100 }, }); if (documentsResponse.data) { @@ -119,9 +114,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT } // Fetch tools - const toolsResponse = await listToolsApiV1ToolsGet({ - headers: { Authorization: `Bearer ${accessToken}` }, - }); + const toolsResponse = await listToolsApiV1ToolsGet({}); if (toolsResponse.data) { setTools(toolsResponse.data); } @@ -131,7 +124,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT }; fetchData(); - }, [getAccessToken]); + }, []); // Memoize defaultEdgeOptions to prevent unnecessary re-renders const defaultEdgeOptions = useMemo(() => ({ @@ -159,7 +152,6 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT workflowId={workflowId} saveWorkflow={saveWorkflow} user={user} - getAccessToken={getAccessToken} onPhoneCallClick={() => setIsPhoneCallDialogOpen(true)} /> @@ -388,14 +380,12 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT onOpenChange={setIsEmbedDialogOpen} workflowId={workflowId} workflowName={workflowName} - getAccessToken={getAccessToken} />
@@ -409,8 +399,7 @@ export default React.memo(RenderWorkflow, (prevProps, nextProps) => { return ( prevProps.workflowId === nextProps.workflowId && prevProps.initialWorkflowName === nextProps.initialWorkflowName && - prevProps.user.id === nextProps.user.id && - prevProps.getAccessToken === nextProps.getAccessToken + prevProps.user.id === nextProps.user.id // Note: We intentionally don't compare initialFlow, initialTemplateContextVariables, // or initialWorkflowConfigurations because they're only used for initialization ); diff --git a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx index 7b720b1..afd83a8 100644 --- a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx @@ -1,7 +1,6 @@ import { Check, Copy, Loader2, Plus, Rocket, Trash2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; -import { client } from "@/client/client.gen"; import { createOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPost, deactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDelete, @@ -32,7 +31,6 @@ interface EmbedDialogProps { onOpenChange: (open: boolean) => void; workflowId: number; workflowName: string; - getAccessToken: () => Promise; } interface EmbedToken { @@ -53,7 +51,6 @@ export function EmbedDialog({ onOpenChange, workflowId, workflowName, - getAccessToken, }: EmbedDialogProps) { const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -72,12 +69,6 @@ export function EmbedDialog({ const loadEmbedToken = useCallback(async () => { setLoading(true); try { - const token = await getAccessToken(); - client.setConfig({ - baseUrl: window.location.origin.replace(/:\d+$/, ':8000'), - headers: { Authorization: `Bearer ${token}` }, - }); - const response = await getEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGet({ path: { workflow_id: workflowId }, }); @@ -105,7 +96,7 @@ export function EmbedDialog({ } finally { setLoading(false); } - }, [workflowId, getAccessToken]); + }, [workflowId]); useEffect(() => { if (open) { @@ -116,12 +107,6 @@ export function EmbedDialog({ const handleSave = async () => { setSaving(true); try { - const token = await getAccessToken(); - client.setConfig({ - baseUrl: window.location.origin.replace(/:\d+$/, ':8000'), - headers: { Authorization: `Bearer ${token}` }, - }); - if (!isEnabled && embedToken) { // Deactivate token await deactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDelete({ diff --git a/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx b/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx index e7632c6..2fd73c6 100644 --- a/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx @@ -28,7 +28,6 @@ interface PhoneCallDialogProps { open: boolean; onOpenChange: (open: boolean) => void; workflowId: number; - getAccessToken: () => Promise; user: { id: string; email?: string }; } @@ -36,7 +35,6 @@ export const PhoneCallDialog = ({ open, onOpenChange, workflowId, - getAccessToken, user, }: PhoneCallDialogProps) => { const router = useRouter(); @@ -57,10 +55,7 @@ export const PhoneCallDialog = ({ setCheckingConfig(true); try { - const accessToken = await getAccessToken(); - const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({ - headers: { 'Authorization': `Bearer ${accessToken}` }, - }); + const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({}); if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix && !configResponse.data?.ari)) { setNeedsConfiguration(true); @@ -76,7 +71,7 @@ export const PhoneCallDialog = ({ }; checkConfig(); - }, [open, getAccessToken]); + }, [open]); // Reset state when dialog closes useEffect(() => { @@ -119,7 +114,6 @@ export const PhoneCallDialog = ({ setCallSuccessMsg(null); try { if (!user || !userConfig) return; - const accessToken = await getAccessToken(); // Save phone number if it has changed if (phoneChanged) { @@ -132,7 +126,6 @@ export const PhoneCallDialog = ({ workflow_id: workflowId, phone_number: phoneNumber }, - headers: { 'Authorization': `Bearer ${accessToken}` }, }); if (response.error) { diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx index be59591..9c1ec09 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx @@ -30,7 +30,6 @@ interface WorkflowEditorHeaderProps { workflowId: number; saveWorkflow: (updateWorkflowDefinition?: boolean) => Promise; user: { id: string; email?: string }; - getAccessToken: () => Promise; onPhoneCallClick: () => void; } diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx index d05702a..acbe6cc 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx @@ -7,7 +7,7 @@ import { getWorkflowApiV1WorkflowFetchWorkflowIdGet, getWorkflowRunsApiV1Workflo import { WorkflowRunResponseSchema } from "@/client/types.gen"; import { WorkflowRunsTable } from "@/components/workflow-runs"; import { DISPOSITION_CODES } from "@/constants/dispositionCodes"; -import { useUserConfig } from '@/context/UserConfigContext'; +import { useAuth } from '@/lib/auth'; import { decodeFiltersFromURL, encodeFiltersToURL } from "@/lib/filters"; import { ActiveFilter, availableAttributes, FilterAttribute } from "@/types/filters"; @@ -39,7 +39,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti return order === 'asc' ? 'asc' : 'desc'; }); - const { accessToken } = useUserConfig(); + const { isAuthenticated } = useAuth(); // Initialize filters from URL const [activeFilters, setActiveFilters] = useState(() => { @@ -53,11 +53,10 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti // Load disposition codes from workflow configuration const loadDispositionCodes = useCallback(async () => { - if (!accessToken) return; + if (!isAuthenticated) return; try { const response = await getWorkflowApiV1WorkflowFetchWorkflowIdGet({ path: { workflow_id: Number(workflowId) }, - headers: { 'Authorization': `Bearer ${accessToken}` } }); const workflow = response.data; @@ -81,7 +80,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti } catch (err) { console.error("Failed to load disposition codes:", err); } - }, [workflowId, accessToken]); + }, [workflowId, isAuthenticated]); useEffect(() => { loadDispositionCodes(); @@ -93,7 +92,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc' ) => { - if (!accessToken) return; + if (!isAuthenticated) return; try { setLoading(true); // Prepare filter data for API @@ -116,9 +115,6 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti ...(sortByParam && { sort_by: sortByParam }), ...(sortOrderParam && { sort_order: sortOrderParam }), }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } }); if (response.error) { @@ -138,7 +134,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti } finally { setLoading(false); } - }, [workflowId, accessToken]); + }, [workflowId, isAuthenticated]); const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => { const params = new URLSearchParams(); @@ -234,7 +230,6 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti sortOrder={sortOrder} onSort={handleSort} workflowId={workflowId} - accessToken={accessToken} onReload={handleReload} />
diff --git a/ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts b/ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts index 8d283b3..6184674 100644 --- a/ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts +++ b/ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts @@ -107,7 +107,6 @@ interface UseWorkflowStateProps { initialTemplateContextVariables?: Record; initialWorkflowConfigurations?: WorkflowConfigurations; user: { id: string; email?: string }; // Minimal user type needed - getAccessToken: () => Promise; } export const useWorkflowState = ({ @@ -117,7 +116,6 @@ export const useWorkflowState = ({ initialTemplateContextVariables, initialWorkflowConfigurations, user, - getAccessToken }: UseWorkflowStateProps) => { const router = useRouter(); const rfInstance = useRef | null>(null); @@ -245,14 +243,10 @@ export const useWorkflowState = ({ const validateWorkflow = useCallback(async () => { if (!user) return; try { - const accessToken = await getAccessToken(); const response = await validateWorkflowApiV1WorkflowWorkflowIdValidatePost({ path: { workflow_id: workflowId, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); // Clear validation errors first @@ -305,13 +299,12 @@ export const useWorkflowState = ({ } catch (error) { logger.error(`Unexpected validation error: ${error}`); } - }, [workflowId, user, getAccessToken, clearValidationErrors, markNodeAsInvalid, markEdgeAsInvalid, setWorkflowValidationErrors]); + }, [workflowId, user, clearValidationErrors, markNodeAsInvalid, markEdgeAsInvalid, setWorkflowValidationErrors]); // Save workflow function const saveWorkflow = useCallback(async (updateWorkflowDefinition: boolean = true) => { if (!user || !rfInstance.current) return; const flow = rfInstance.current.toObject(); - const accessToken = await getAccessToken(); try { await updateWorkflowApiV1WorkflowWorkflowIdPut({ path: { @@ -321,9 +314,6 @@ export const useWorkflowState = ({ name: workflowName, workflow_definition: updateWorkflowDefinition ? flow : null, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); setIsDirty(false); } catch (error) { @@ -332,7 +322,7 @@ export const useWorkflowState = ({ // Validate after saving await validateWorkflow(); - }, [workflowId, workflowName, setIsDirty, user, getAccessToken, validateWorkflow]); + }, [workflowId, workflowName, setIsDirty, user, validateWorkflow]); // Set up keyboard shortcut for save (Cmd/Ctrl + S) useEffect(() => { @@ -386,7 +376,6 @@ export const useWorkflowState = ({ const onRun = async (mode: string) => { if (!user) return; const workflowRunName = `WR-${getRandomId()}`; - const accessToken = await getAccessToken(); const response = await createWorkflowRunApiV1WorkflowWorkflowIdRunsPost({ path: { workflow_id: workflowId, @@ -395,9 +384,6 @@ export const useWorkflowState = ({ mode, name: workflowRunName }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); router.push(`/workflow/${workflowId}/run/${response.data?.id}`); }; @@ -405,7 +391,6 @@ export const useWorkflowState = ({ // Save template context variables const saveTemplateContextVariables = useCallback(async (variables: Record) => { if (!user) return; - const accessToken = await getAccessToken(); try { await updateWorkflowApiV1WorkflowWorkflowIdPut({ path: { @@ -416,9 +401,6 @@ export const useWorkflowState = ({ workflow_definition: null, template_context_variables: variables, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); setTemplateContextVariables(variables); logger.info('Template context variables saved successfully'); @@ -426,12 +408,11 @@ export const useWorkflowState = ({ logger.error(`Error saving template context variables: ${error}`); throw error; } - }, [workflowId, workflowName, user, getAccessToken, setTemplateContextVariables]); + }, [workflowId, workflowName, user, setTemplateContextVariables]); // Save workflow configurations const saveWorkflowConfigurations = useCallback(async (configurations: WorkflowConfigurations, newWorkflowName: string) => { if (!user) return; - const accessToken = await getAccessToken(); // Preserve the current dictionary when saving other configurations const currentDictionary = useWorkflowStore.getState().dictionary; const configurationsWithDictionary: WorkflowConfigurations = { ...configurations, dictionary: currentDictionary }; @@ -445,9 +426,6 @@ export const useWorkflowState = ({ workflow_definition: null, workflow_configurations: configurationsWithDictionary as Record, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); setWorkflowConfigurations(configurationsWithDictionary); setWorkflowName(newWorkflowName); @@ -456,12 +434,11 @@ export const useWorkflowState = ({ logger.error(`Error saving workflow configurations: ${error}`); throw error; } - }, [workflowId, user, getAccessToken, setWorkflowConfigurations, setWorkflowName]); + }, [workflowId, user, setWorkflowConfigurations, setWorkflowName]); // Save dictionary const saveDictionary = useCallback(async (newDictionary: string) => { if (!user) return; - const accessToken = await getAccessToken(); const currentConfigurations = useWorkflowStore.getState().workflowConfigurations ?? DEFAULT_WORKFLOW_CONFIGURATIONS; const updatedConfigurations: WorkflowConfigurations = { ...currentConfigurations, dictionary: newDictionary }; try { @@ -474,9 +451,6 @@ export const useWorkflowState = ({ workflow_definition: null, workflow_configurations: updatedConfigurations as Record, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); setDictionary(newDictionary); setWorkflowConfigurations(updatedConfigurations); @@ -484,7 +458,7 @@ export const useWorkflowState = ({ logger.error(`Error saving dictionary: ${error}`); throw error; } - }, [workflowId, workflowName, user, getAccessToken, setDictionary, setWorkflowConfigurations]); + }, [workflowId, workflowName, user, setDictionary, setWorkflowConfigurations]); // Update rfInstance when it changes useEffect(() => { diff --git a/ui/src/app/workflow/[workflowId]/page.tsx b/ui/src/app/workflow/[workflowId]/page.tsx index b960f75..9c80631 100644 --- a/ui/src/app/workflow/[workflowId]/page.tsx +++ b/ui/src/app/workflow/[workflowId]/page.tsx @@ -19,7 +19,7 @@ export default function WorkflowDetailPage() { const [workflow, setWorkflow] = useState(undefined); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { user, getAccessToken, redirectToLogin, loading: authLoading } = useAuth(); + const { user, redirectToLogin, loading: authLoading } = useAuth(); // Redirect if not authenticated useEffect(() => { @@ -32,14 +32,10 @@ export default function WorkflowDetailPage() { const fetchWorkflow = async () => { if (!user) return; try { - const accessToken = await getAccessToken(); const response = await getWorkflowApiV1WorkflowFetchWorkflowIdGet({ path: { workflow_id: Number(params.workflowId) }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); const workflow = response.data; setWorkflow(workflow); @@ -54,11 +50,9 @@ export default function WorkflowDetailPage() { if (user) { fetchWorkflow(); } - }, [params.workflowId, user, getAccessToken]); + }, [params.workflowId, user]); - // Memoize user and getAccessToken to prevent unnecessary re-renders const stableUser = useMemo(() => user, [user]); - const stableGetAccessToken = useMemo(() => getAccessToken, [getAccessToken]); if (loading) { return ( @@ -89,7 +83,6 @@ export default function WorkflowDetailPage() { initialTemplateContextVariables={workflow.template_context_variables as Record || {}} initialWorkflowConfigurations={(workflow.workflow_configurations as WorkflowConfigurations) || DEFAULT_WORKFLOW_CONFIGURATIONS} user={stableUser} - getAccessToken={stableGetAccessToken} /> ) : null; } diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx index 4d9c0f0..eae1ea6 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet } from "@/client/sdk.gen"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useAuth } from "@/lib/auth"; import { ApiKeyErrorDialog, @@ -14,15 +15,23 @@ import { } from "./components"; import { useWebSocketRTC } from "./hooks"; -const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVariables }: { +const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: { workflowId: number, workflowRunId: number, - accessToken: string | null, initialContextVariables?: Record | null }) => { const router = useRouter(); + const auth = useAuth(); + const [accessToken, setAccessToken] = useState(null); const [checkingForRecording, setCheckingForRecording] = useState(false); + // Get access token for WebSocket connection (non-SDK usage) + useEffect(() => { + if (auth.isAuthenticated && !auth.loading) { + auth.getAccessToken().then(setAccessToken); + } + }, [auth]); + const { audioRef, audioInputs, @@ -47,7 +56,7 @@ const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVar // Poll for recording availability after call ends useEffect(() => { - if (!isCompleted || !accessToken) return; + if (!isCompleted || !auth.isAuthenticated) return; setCheckingForRecording(true); const intervalId = setInterval(async () => { @@ -57,9 +66,6 @@ const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVar workflow_id: workflowId, run_id: workflowRunId, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (response.data?.transcript_url || response.data?.recording_url) { @@ -83,7 +89,7 @@ const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVar clearInterval(intervalId); clearTimeout(timeoutId); }; - }, [isCompleted, accessToken, workflowId, workflowRunId]); + }, [isCompleted, auth.isAuthenticated, workflowId, workflowRunId]); const navigateToApiKeys = () => { router.push('/api-keys'); diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx index 4a2dde5..3902e85 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx @@ -74,7 +74,6 @@ export default function WorkflowRunPage() { const [isLoading, setIsLoading] = useState(true); const auth = useAuth(); const [workflowRun, setWorkflowRun] = useState(null); - const [accessToken, setAccessToken] = useState(null); const { hasSeenTooltip, markTooltipSeen } = useOnboarding(); const customizeButtonRef = useRef(null); @@ -85,21 +84,13 @@ export default function WorkflowRunPage() { } }, [auth]); - // Get access token - useEffect(() => { - if (auth.isAuthenticated && !auth.loading) { - auth.getAccessToken().then(setAccessToken); - } - }, [auth]); - - const { openPreview, dialog } = MediaPreviewDialog({ accessToken }); + const { openPreview, dialog } = MediaPreviewDialog(); useEffect(() => { const fetchWorkflowRun = async () => { if (!auth.isAuthenticated || auth.loading) return; setIsLoading(true); - const token = await auth.getAccessToken(); const workflowId = params.workflowId; const runId = params.runId; const response = await getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet({ @@ -107,9 +98,6 @@ export default function WorkflowRunPage() { workflow_id: Number(workflowId), run_id: Number(runId), }, - headers: { - 'Authorization': `Bearer ${token}`, - }, }); setIsLoading(false); setWorkflowRun({ @@ -197,8 +185,8 @@ export default function WorkflowRunPage() {
Download:
- {recordingKey && accessToken && ( - )} - {transcriptKey && accessToken && ( - )} diff --git a/ui/src/components/VoiceSelector.tsx b/ui/src/components/VoiceSelector.tsx index 45044ad..d47fc06 100644 --- a/ui/src/components/VoiceSelector.tsx +++ b/ui/src/components/VoiceSelector.tsx @@ -10,7 +10,6 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { useUserConfig } from "@/context/UserConfigContext"; import { cn } from "@/lib/utils"; // Providers that have MPS voice endpoints @@ -30,7 +29,6 @@ export const VoiceSelector: React.FC = ({ onChange, className, }) => { - const { accessToken } = useUserConfig(); const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [isManualInput, setIsManualInput] = useState(false); @@ -60,7 +58,7 @@ export const VoiceSelector: React.FC = ({ const fetchVoices = useCallback(async () => { const providerKey = getProviderKey(provider); - if (!providerKey || !accessToken) { + if (!providerKey) { setVoices([]); return; } @@ -71,9 +69,6 @@ export const VoiceSelector: React.FC = ({ try { const response = await getVoicesApiV1UserConfigurationsVoicesProviderGet({ path: { provider: providerKey }, - headers: { - Authorization: `Bearer ${accessToken}`, - }, }); if (response.data?.voices) { @@ -86,7 +81,7 @@ export const VoiceSelector: React.FC = ({ } finally { setIsLoading(false); } - }, [provider, getProviderKey, accessToken]); + }, [provider, getProviderKey]); useEffect(() => { if (provider) { diff --git a/ui/src/components/http/credential-selector.tsx b/ui/src/components/http/credential-selector.tsx index ad5b2b7..73fa2ec 100644 --- a/ui/src/components/http/credential-selector.tsx +++ b/ui/src/components/http/credential-selector.tsx @@ -36,7 +36,7 @@ export function CredentialSelector({ description = "Select a credential for authentication, or leave empty for no auth.", showLabel = true, }: CredentialSelectorProps) { - const { getAccessToken } = useAuth(); + useAuth(); const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(false); @@ -45,10 +45,7 @@ export function CredentialSelector({ const fetchCredentials = useCallback(async () => { setLoading(true); try { - const accessToken = await getAccessToken(); - const response = await listCredentialsApiV1CredentialsGet({ - headers: { Authorization: `Bearer ${accessToken}` }, - }); + const response = await listCredentialsApiV1CredentialsGet({}); if (response.error) { console.error("Failed to fetch credentials:", response.error); setCredentials([]); @@ -63,7 +60,7 @@ export function CredentialSelector({ } finally { setLoading(false); } - }, [getAccessToken]); + }, []); useEffect(() => { fetchCredentials(); diff --git a/ui/src/components/layout/AppLayout.tsx b/ui/src/components/layout/AppLayout.tsx index 98b571e..efe61b4 100644 --- a/ui/src/components/layout/AppLayout.tsx +++ b/ui/src/components/layout/AppLayout.tsx @@ -28,44 +28,47 @@ const AppLayout: React.FC = ({ const isWorkflowEditor = /^\/workflow\/\d+/.test(pathname); const isSuperadmin = pathname.startsWith("/superadmin"); - // If no sidebar needed, just return children - if (!shouldShowSidebar) { - return <>{children}; - } - + // Always render SidebarProvider to keep the component tree shape consistent + // across route changes (avoids React hooks ordering violations during navigation). return ( -
- - - {/* Optional header area for specific pages */} - {headerActions && ( -
-
-
- {headerActions} + {shouldShowSidebar ? ( +
+ + + {/* Optional header area for specific pages */} + {headerActions && ( +
+
+
+ {headerActions} +
+
+
+ )} + + {/* Optional sticky tabs */} + {stickyTabs && ( +
+
+
+ {stickyTabs} +
-
- )} + )} - {/* Optional sticky tabs */} - {stickyTabs && ( -
-
-
- {stickyTabs} -
-
-
- )} - - {/* Main content area */} -
- {children} -
-
-
+ {/* Main content area */} +
+ {children} +
+ +
+ ) : ( +
+ {children} +
+ )} ); }; diff --git a/ui/src/components/layout/AppSidebar.tsx b/ui/src/components/layout/AppSidebar.tsx index d7adcfc..ccad13d 100644 --- a/ui/src/components/layout/AppSidebar.tsx +++ b/ui/src/components/layout/AppSidebar.tsx @@ -11,9 +11,11 @@ import { HelpCircle, Home, Key, + LogOut, Megaphone, MessageSquare, Phone, + Settings, Star, TrendingUp, Workflow, @@ -26,6 +28,14 @@ import React from "react"; import ThemeToggle from "@/components/ThemeSwitcher"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Sidebar, SidebarContent, @@ -50,11 +60,6 @@ import { useAppConfig } from "@/context/AppConfigContext"; import { useAuth } from "@/lib/auth"; import { cn } from "@/lib/utils"; -// Conditionally load Stack components only when using Stack auth -const StackUserButton = React.lazy(() => - import("@stackframe/stack").then((mod) => ({ default: mod.UserButton })) -); - // Lazy load SelectedTeamSwitcher - we'll pass selectedTeam from our context const StackTeamSwitcher = React.lazy(() => import("@stackframe/stack").then((mod) => ({ @@ -66,7 +71,7 @@ export function AppSidebar() { const pathname = usePathname(); const router = useRouter(); const { state } = useSidebar(); - const { provider, getSelectedTeam } = useAuth(); + const { provider, getSelectedTeam, logout, user } = useAuth(); const { config } = useAppConfig(); // Get selected team for Stack auth (cast to Team type from Stack) @@ -400,31 +405,53 @@ export function AppSidebar() { )} - {/* User Button for Stack Auth - at the bottom */} + {/* User Button - at the bottom */} {provider === "stack" && ( - - } - > -
- , - onClick: () => router.push("/usage"), - }, - ]} - /> -
-
+
+ + + + + + +
+ {user?.displayName && ( +

{user.displayName}

+ )} + {(user as { primaryEmail?: string })?.primaryEmail && ( +

{(user as { primaryEmail?: string }).primaryEmail}

+ )} +
+
+ + router.push("/handler/account-settings")} className="cursor-pointer"> + + Account settings + + router.push("/usage")} className="cursor-pointer"> + + Usage + + logout()} className="cursor-pointer"> + + Sign out + +
+
+
)} {/* Theme Toggle - at the very bottom */} diff --git a/ui/src/components/looptalk/ConversationsList.tsx b/ui/src/components/looptalk/ConversationsList.tsx index c6e0bae..29a7edd 100644 --- a/ui/src/components/looptalk/ConversationsList.tsx +++ b/ui/src/components/looptalk/ConversationsList.tsx @@ -19,20 +19,16 @@ export function ConversationsList({ testSessionId }: ConversationsListProps) { const [conversations, setConversations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { user, getAccessToken } = useAuth(); + const { user } = useAuth(); useEffect(() => { const fetchConversations = async () => { if (!user) return; try { - const accessToken = await getAccessToken(); const response = await getTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGet({ path: { test_session_id: testSessionId }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); // API returns { conversation: Conversation | null } @@ -56,7 +52,7 @@ export function ConversationsList({ testSessionId }: ConversationsListProps) { // Poll for updates every 5 seconds const interval = setInterval(fetchConversations, 5000); return () => clearInterval(interval); - }, [testSessionId, user, getAccessToken]); + }, [testSessionId, user]); if (loading && conversations.length === 0) { return ( diff --git a/ui/src/components/looptalk/CreateTestSessionButton.tsx b/ui/src/components/looptalk/CreateTestSessionButton.tsx index 0c067a9..99eceb2 100644 --- a/ui/src/components/looptalk/CreateTestSessionButton.tsx +++ b/ui/src/components/looptalk/CreateTestSessionButton.tsx @@ -33,7 +33,7 @@ export function CreateTestSessionButton() { const router = useRouter(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const { user, getAccessToken } = useAuth(); + const { user } = useAuth(); const [formData, setFormData] = useState({ name: '', description: '', @@ -49,7 +49,6 @@ export function CreateTestSessionButton() { try { if (!user) return; - const accessToken = await getAccessToken(); const response = await createTestSessionApiV1LooptalkTestSessionsPost({ body: { name: formData.name, @@ -61,9 +60,6 @@ export function CreateTestSessionButton() { concurrent_pairs: formData.test_type === 'load_test' ? formData.concurrent_pairs : undefined } }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); toast.success('Test session created successfully'); diff --git a/ui/src/components/workflow-runs/CampaignRuns.tsx b/ui/src/components/workflow-runs/CampaignRuns.tsx index 7880f86..459015d 100644 --- a/ui/src/components/workflow-runs/CampaignRuns.tsx +++ b/ui/src/components/workflow-runs/CampaignRuns.tsx @@ -18,7 +18,7 @@ interface CampaignRunsProps { export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignRunsProps) { const router = useRouter(); - const { getAccessToken } = useAuth(); + const { isAuthenticated } = useAuth(); const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -29,7 +29,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); const [isExecutingFilters, setIsExecutingFilters] = useState(false); - const [accessToken, setAccessToken] = useState(null); // Sort state (initialized from URL) const [sortBy, setSortBy] = useState(() => { @@ -50,22 +49,13 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : []; }); - // Get access token on mount - useEffect(() => { - const fetchToken = async () => { - const token = await getAccessToken(); - setAccessToken(token); - }; - fetchToken(); - }, [getAccessToken]); - const fetchCampaignRuns = useCallback(async ( page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc' ) => { - if (!accessToken) return; + if (!isAuthenticated) return; try { setLoading(true); // Prepare filter data for API @@ -88,9 +78,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR ...(sortByParam && { sort_by: sortByParam }), ...(sortOrderParam && { sort_order: sortOrderParam }), }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } }); if (response.error) { @@ -111,7 +98,7 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR } finally { setLoading(false); } - }, [campaignId, accessToken]); + }, [campaignId, isAuthenticated]); const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => { const params = new URLSearchParams(); @@ -136,10 +123,10 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR }, [router, campaignId]); useEffect(() => { - if (accessToken) { + if (isAuthenticated) { fetchCampaignRuns(currentPage, appliedFilters, sortBy, sortOrder); } - }, [currentPage, appliedFilters, fetchCampaignRuns, accessToken, sortBy, sortOrder]); + }, [currentPage, appliedFilters, fetchCampaignRuns, isAuthenticated, sortBy, sortOrder]); const handleApplyFilters = useCallback(async () => { setIsExecutingFilters(true); @@ -213,7 +200,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR sortOrder={sortOrder} onSort={handleSort} workflowId={workflowId} - accessToken={accessToken} onReload={handleReload} title="Campaign Workflow Runs" emptyMessage="No workflow runs found for this campaign" diff --git a/ui/src/components/workflow-runs/WorkflowRunsTable.tsx b/ui/src/components/workflow-runs/WorkflowRunsTable.tsx index 7f5b752..ef51e68 100644 --- a/ui/src/components/workflow-runs/WorkflowRunsTable.tsx +++ b/ui/src/components/workflow-runs/WorkflowRunsTable.tsx @@ -47,7 +47,6 @@ export interface WorkflowRunsTableProps { // Navigation & Actions workflowId: number; - accessToken: string | null; // Reload onReload?: () => void; @@ -78,7 +77,6 @@ export function WorkflowRunsTable({ sortOrder = 'desc', onSort, workflowId, - accessToken, onReload, title = "Workflow Run History", subtitle, @@ -88,7 +86,7 @@ export function WorkflowRunsTable({ const [selectedRowId, setSelectedRowId] = useState(null); // Media preview dialog - const mediaPreview = MediaPreviewDialog({ accessToken }); + const mediaPreview = MediaPreviewDialog(); const formatDate = (dateString: string) => new Date(dateString).toLocaleString(); diff --git a/ui/src/components/workflow/WorkflowTable.tsx b/ui/src/components/workflow/WorkflowTable.tsx index f0c31ae..30c8081 100644 --- a/ui/src/components/workflow/WorkflowTable.tsx +++ b/ui/src/components/workflow/WorkflowTable.tsx @@ -15,8 +15,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useUserConfig } from '@/context/UserConfigContext'; - interface Workflow { id: number; name: string; @@ -32,7 +30,6 @@ interface WorkflowTableProps { export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) { const router = useRouter(); - const { accessToken } = useUserConfig(); const [isPending, startTransition] = useTransition(); const [loadingWorkflowId, setLoadingWorkflowId] = useState(null); @@ -41,11 +38,6 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) { }; const handleArchiveToggle = async (id: number, currentStatus: string) => { - if (!accessToken) { - toast.error('Authentication required'); - return; - } - const newStatus = currentStatus === 'active' ? 'archived' : 'active'; const action = currentStatus === 'active' ? 'Archive' : 'Restore'; @@ -59,9 +51,6 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) { body: { status: newStatus, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (response.data) { diff --git a/ui/src/context/UserConfigContext.tsx b/ui/src/context/UserConfigContext.tsx index feb4654..7ad7000 100644 --- a/ui/src/context/UserConfigContext.tsx +++ b/ui/src/context/UserConfigContext.tsx @@ -4,6 +4,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, u import { getUserConfigurationsApiV1UserConfigurationsUserGet, updateUserConfigurationsApiV1UserConfigurationsUserPut } from '@/client/sdk.gen'; import type { UserConfigurationRequestResponseSchema } from '@/client/types.gen'; +import { setupAuthInterceptor } from '@/lib/apiClient'; import type { AuthUser } from '@/lib/auth'; import { useAuth } from '@/lib/auth'; @@ -43,7 +44,6 @@ interface UserConfigContextType { error: Error | null; refreshConfig: () => Promise; permissions: TeamPermission[]; - accessToken: string | null; user: AuthUser | null; organizationPricing: OrganizationPricing | null; } @@ -54,7 +54,6 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { const [userConfig, setUserConfig] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [accessToken, setAccessToken] = useState(null); const [organizationPricing, setOrganizationPricing] = useState(null); const [permissions, setPermissions] = useState([]); @@ -68,6 +67,13 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { const hasFetchedConfig = useRef(false); const hasFetchedPermissions = useRef(false); + // Register the auth interceptor synchronously during render (not in useEffect) + // so it's in place before any child effects fire API calls. + // setupAuthInterceptor is idempotent — safe for strict mode double-renders. + if (!auth.loading && auth.isAuthenticated) { + setupAuthInterceptor(auth.getAccessToken); + } + // Fetch permissions once when auth is ready useEffect(() => { if (auth.loading || hasFetchedPermissions.current) { @@ -107,14 +113,7 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { const fetchUserConfig = async () => { setLoading(true); try { - const token = await authRef.current.getAccessToken(); - setAccessToken(token); - - const response = await getUserConfigurationsApiV1UserConfigurationsUserGet({ - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); + const response = await getUserConfigurationsApiV1UserConfigurationsUserGet(); if (response.data) { setUserConfig(response.data); @@ -131,7 +130,6 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { setError(null); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch user configuration')); - setAccessToken(null); } finally { setLoading(false); } @@ -141,13 +139,12 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { }, [auth.loading, auth.isAuthenticated]); const saveUserConfig = useCallback(async (userConfigRequest: SaveUserConfigFunctionParams) => { - if (!accessToken) throw new Error('No authentication token available'); + if (!authRef.current.isAuthenticated) throw new Error('No authentication available'); const response = await updateUserConfigurationsApiV1UserConfigurationsUserPut({ body: { ...userConfig, ...userConfigRequest } as UserConfigurationRequestResponseSchema, - headers: { 'Authorization': `Bearer ${accessToken}` }, }); if (response.error) { let msg = 'Failed to save user configuration'; @@ -168,7 +165,7 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { billing_enabled: response.data.organization_pricing.billing_enabled as boolean || false }); } - }, [accessToken, userConfig]); + }, [userConfig]); const refreshConfig = useCallback(async () => { const currentAuth = authRef.current; @@ -176,14 +173,7 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { setLoading(true); try { - const token = await currentAuth.getAccessToken(); - setAccessToken(token); - - const response = await getUserConfigurationsApiV1UserConfigurationsUserGet({ - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); + const response = await getUserConfigurationsApiV1UserConfigurationsUserGet(); if (response.data) { setUserConfig(response.data); @@ -212,7 +202,6 @@ export function UserConfigProvider({ children }: { children: ReactNode }) { error, refreshConfig, permissions, - accessToken, user: auth.user, organizationPricing, }} diff --git a/ui/src/lib/apiClient.ts b/ui/src/lib/apiClient.ts index 7605ef1..63474c3 100644 --- a/ui/src/lib/apiClient.ts +++ b/ui/src/lib/apiClient.ts @@ -1,4 +1,5 @@ import type { CreateClientConfig } from '@/client/client.gen'; +import { client } from '@/client/client.gen'; export const createClientConfig: CreateClientConfig = (config) => { // Use different URLs for server-side vs client-side @@ -18,3 +19,27 @@ export const createClientConfig: CreateClientConfig = (config) => { baseUrl, }; }; + +let interceptorRegistered = false; + +/** + * Register a request interceptor that attaches a fresh access token + * to every outgoing SDK request. Idempotent — safe for React strict mode. + */ +export function setupAuthInterceptor(getAccessToken: () => Promise) { + if (interceptorRegistered) return; + interceptorRegistered = true; + + client.interceptors.request.use(async (request) => { + if (request.headers.get('Authorization')) { + return request; + } + try { + const token = await getAccessToken(); + request.headers.set('Authorization', `Bearer ${token}`); + } catch { + // If token retrieval fails, let the request proceed without auth + } + return request; + }); +} diff --git a/ui/src/lib/auth/hooks/useAuth.ts b/ui/src/lib/auth/hooks/useAuth.ts deleted file mode 100644 index 0377f4a..0000000 --- a/ui/src/lib/auth/hooks/useAuth.ts +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import { useAuthContext } from '../providers/AuthProvider'; - -export function useAuth() { - return useAuthContext(); -} diff --git a/ui/src/lib/auth/index.ts b/ui/src/lib/auth/index.ts index 13e02a1..72d1d8d 100644 --- a/ui/src/lib/auth/index.ts +++ b/ui/src/lib/auth/index.ts @@ -1,5 +1,4 @@ -export { useAuth } from './hooks/useAuth'; -export { AuthProvider } from './providers/AuthProvider'; +export { AuthProvider, useAuth } from './providers/AuthProvider'; export type { AuthProvider as AuthProviderType, AuthToken, diff --git a/ui/src/lib/auth/providers/AuthProvider.tsx b/ui/src/lib/auth/providers/AuthProvider.tsx index 38fc73b..28877aa 100644 --- a/ui/src/lib/auth/providers/AuthProvider.tsx +++ b/ui/src/lib/auth/providers/AuthProvider.tsx @@ -1,9 +1,8 @@ 'use client'; import { Loader2 } from 'lucide-react'; -import React, { createContext, lazy, Suspense, useContext, useEffect, useMemo, useState } from 'react'; +import React, { createContext, lazy, Suspense, useContext } from 'react'; -import { createAuthService } from '../services'; import type { AuthUser } from '../types'; // Shared context type for both Stack and Local providers @@ -22,54 +21,18 @@ export interface AuthContextType { export const AuthContext = createContext(null); -// Lazy load Stack components only when needed +// Lazy load provider wrappers only when needed const StackProviderWrapper = lazy(() => import('./StackProviderWrapper').then(module => ({ default: module.StackProviderWrapper })) ); -// Generic context provider for non-Stack providers (local/OSS) -function LocalAuthContextProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const service = useMemo(() => createAuthService('local'), []); - - useEffect(() => { - const fetchUser = async () => { - try { - const currentUser = await service.getCurrentUser(); - setUser(currentUser); - } catch (error) { - console.error('Failed to fetch user:', error); - setUser(null); - } finally { - setLoading(false); - } - }; - fetchUser(); - }, [service]); - - const getAccessToken = React.useCallback(() => service.getAccessToken(), [service]); - const redirectToLogin = React.useCallback(() => service.redirectToLogin(), [service]); - const logout = React.useCallback(() => service.logout(), [service]); - - const contextValue: AuthContextType = useMemo(() => ({ - user, - isAuthenticated: !!user, - loading, - getAccessToken, - redirectToLogin, - logout, - provider: 'local', - }), [user, loading, getAccessToken, redirectToLogin, logout]); - - return ( - - {children} - - ); -} +const LocalProviderWrapper = lazy(() => + import('./LocalProviderWrapper').then(module => ({ + default: module.LocalProviderWrapper + })) +); export function AuthProvider({ children }: { children: React.ReactNode }) { const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack'; @@ -91,16 +54,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // For local/OSS provider return ( - - {children} - + + +
+ }> + + {children} + + ); } -export function useAuthContext() { +export function useAuth() { const context = useContext(AuthContext); if (!context) { - throw new Error('useAuthContext must be used within AuthProvider'); + throw new Error('useAuth must be used within AuthProvider'); } return context; } diff --git a/ui/src/lib/auth/providers/LocalProviderWrapper.tsx b/ui/src/lib/auth/providers/LocalProviderWrapper.tsx new file mode 100644 index 0000000..1ce5b2c --- /dev/null +++ b/ui/src/lib/auth/providers/LocalProviderWrapper.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import logger from '@/lib/logger'; + +import type { AuthUser, LocalUser } from '../types'; +import { AuthContext } from './AuthProvider'; + +export function LocalProviderWrapper({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const tokenRef = useRef(null); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const initializeAuth = async () => { + try { + const response = await fetch('/api/auth/oss'); + if (response.ok) { + const data = await response.json(); + tokenRef.current = data.token; + setUser(data.user); + logger.info('OSS auth initialized', { user: data.user }); + } else { + logger.error('Failed to initialize OSS auth'); + } + } catch (error) { + logger.error('Error initializing OSS auth', error); + } finally { + setLoading(false); + } + }; + + initializeAuth(); + }, []); + + const getAccessToken = React.useCallback(async () => { + if (typeof window === 'undefined') { + return 'ssr-placeholder-token'; + } + if (!tokenRef.current) { + logger.warn('No OSS token available after initialization'); + return ''; + } + return tokenRef.current; + }, []); + + const redirectToLogin = React.useCallback(() => { + logger.info('Login redirect not needed in local mode'); + }, []); + + const logout = React.useCallback(async () => { + setUser(null); + logger.info('Logout requested in OSS mode - server cookies need to be cleared'); + }, []); + + const contextValue = useMemo(() => ({ + user: user as AuthUser, + isAuthenticated: !loading, + loading, + getAccessToken, + redirectToLogin, + logout, + provider: 'local' as const, + }), [user, loading, getAccessToken, redirectToLogin, logout]); + + return ( + + {children} + + ); +} diff --git a/ui/src/lib/auth/providers/StackProviderWrapper.tsx b/ui/src/lib/auth/providers/StackProviderWrapper.tsx index 731869f..688066f 100644 --- a/ui/src/lib/auth/providers/StackProviderWrapper.tsx +++ b/ui/src/lib/auth/providers/StackProviderWrapper.tsx @@ -56,9 +56,11 @@ function StackAuthContextProvider({ children }: { children: React.ReactNode }) { }, []); const logout = React.useCallback(async () => { - const user = userRef.current; - if (user?.signOut) { - await user.signOut(); + // Redirect to Stack's server-side sign-out handler instead of calling + // signOut() client-side. Client-side signOut triggers an internal + // re-render that causes a hooks ordering violation in Stack's components. + if (typeof window !== 'undefined') { + window.location.href = '/handler/sign-out'; } }, []); diff --git a/ui/src/lib/auth/services/index.ts b/ui/src/lib/auth/services/index.ts deleted file mode 100644 index ee0fdcd..0000000 --- a/ui/src/lib/auth/services/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import logger from '@/lib/logger'; - -import type { AuthProvider } from '../types'; -import type { IAuthService } from './interface'; -import { LocalAuthService } from './localAuthService'; -import { StackAuthService } from './stackAuthService'; - -// Singleton instances for auth services -let stackServiceInstance: StackAuthService | null = null; -let localServiceInstance: LocalAuthService | null = null; - -export function createAuthService(provider?: AuthProvider | string): IAuthService { - const authProvider = provider || process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack'; - - switch (authProvider) { - case 'stack': - if (!stackServiceInstance) { - logger.debug('[createAuthService] Creating singleton StackAuthService instance'); - stackServiceInstance = new StackAuthService(); - } - return stackServiceInstance; - case 'local': - if (!localServiceInstance) { - logger.debug('[createAuthService] Creating singleton LocalAuthService instance'); - localServiceInstance = new LocalAuthService(); - } - return localServiceInstance; - // Future providers can be added here - // case 'auth0': - // return new Auth0Service(); - // case 'supabase': - // return new SupabaseService(); - default: - console.warn(`Unknown auth provider: ${authProvider}, falling back to local`); - if (!localServiceInstance) { - localServiceInstance = new LocalAuthService(); - } - return localServiceInstance; - } -} - -export type { IAuthService } from './interface'; -export { LocalAuthService } from './localAuthService'; -export { StackAuthService } from './stackAuthService'; - diff --git a/ui/src/lib/auth/services/interface.ts b/ui/src/lib/auth/services/interface.ts deleted file mode 100644 index e26caa6..0000000 --- a/ui/src/lib/auth/services/interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AuthUser } from '../types'; - -export interface IAuthService { - // Token management - getAccessToken(): Promise; - refreshToken(): Promise; - - // User management - getCurrentUser(): Promise; - isAuthenticated(): boolean; - - // Navigation - redirectToLogin(): void; - logout(): Promise; - - // Team/Organization management (optional for some providers) - getSelectedTeam?(): unknown; - listPermissions?(team?: unknown): Promise>; - - // Provider-specific - getProviderName(): string; -} - diff --git a/ui/src/lib/auth/services/localAuthService.ts b/ui/src/lib/auth/services/localAuthService.ts deleted file mode 100644 index dc85f7a..0000000 --- a/ui/src/lib/auth/services/localAuthService.ts +++ /dev/null @@ -1,109 +0,0 @@ -'use client'; - -import logger from '@/lib/logger'; - -import type { LocalUser } from '../types'; -import type { IAuthService } from './interface'; - -export class LocalAuthService implements IAuthService { - private currentUser: LocalUser | null = null; - private currentToken: string | null = null; - private authPromise: Promise | null = null; - private static instance: LocalAuthService | null = null; - - constructor() { - // Singleton pattern to ensure single initialization - if (LocalAuthService.instance) { - return LocalAuthService.instance; - } - LocalAuthService.instance = this; - - // Initialize auth on creation - if (typeof window !== 'undefined') { - this.authPromise = this.initializeAuth(); - } - } - - private async initializeAuth(): Promise { - try { - const response = await fetch('/api/auth/oss'); - if (response.ok) { - const data = await response.json(); - this.currentToken = data.token; - this.currentUser = data.user; - logger.info('OSS auth initialized', { user: data.user }); - } else { - logger.error('Failed to initialize OSS auth'); - } - } catch (error) { - logger.error('Error initializing OSS auth', error); - } - } - - private async ensureAuth(): Promise { - if (this.authPromise) { - await this.authPromise; - } else if (!this.currentToken && typeof window !== 'undefined') { - this.authPromise = this.initializeAuth(); - await this.authPromise; - } - } - - async getAccessToken(): Promise { - if (typeof window === 'undefined') { - // SSR: Server will handle this - return 'ssr-placeholder-token'; - } - - await this.ensureAuth(); - - if (!this.currentToken) { - logger.warn('No OSS token available after initialization'); - return ''; - } - return this.currentToken; - } - - async refreshToken(): Promise { - // For local mode, just return the same token - return this.getAccessToken(); - } - - async getCurrentUser(): Promise { - if (typeof window === 'undefined') { - // SSR: Server will handle this - return null; - } - - await this.ensureAuth(); - - if (!this.currentUser) { - logger.warn('No OSS user available after initialization'); - return null; - } - - return this.currentUser; - } - - isAuthenticated(): boolean { - // In local mode, always authenticated - return true; - } - - redirectToLogin(): void { - // No-op for local mode - logger.info('Login redirect not needed in local mode'); - } - - async logout(): Promise { - // In OSS mode, logout would require server-side cookie clearing - // For now, just clear the cached user - this.currentUser = null; - logger.info('Logout requested in OSS mode - server cookies need to be cleared'); - } - - getProviderName(): string { - return 'local'; - } -} - diff --git a/ui/src/lib/auth/services/stackAuthService.ts b/ui/src/lib/auth/services/stackAuthService.ts deleted file mode 100644 index 1eafbe7..0000000 --- a/ui/src/lib/auth/services/stackAuthService.ts +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import type { CurrentUser } from '@stackframe/stack'; - -import logger from '@/lib/logger'; - -import type { IAuthService } from './interface'; - -export class StackAuthService implements IAuthService { - private userInstance: CurrentUser | null = null; - private callCount = { - setUserInstance: 0, - getAccessToken: 0, - refreshToken: 0, - getCurrentUser: 0, - isAuthenticated: 0 - }; - - // Set the user instance from the Stack useUser hook - setUserInstance(user: CurrentUser) { - this.callCount.setUserInstance++; - logger.debug('[StackAuthService] setUserInstance called', { - callCount: this.callCount.setUserInstance, - userId: user?.id, - hadPreviousUser: !!this.userInstance, - previousUserId: this.userInstance?.id, - timestamp: new Date().toISOString() - }); - this.userInstance = user; - logger.debug('[StackAuthService] setUserInstance completed - user is now set'); - } - - async getAccessToken(): Promise { - this.callCount.getAccessToken++; - logger.debug('[StackAuthService] getAccessToken called', { - callCount: this.callCount.getAccessToken, - hasUser: !!this.userInstance, - userId: this.userInstance?.id - }); - - if (!this.userInstance) { - logger.error('[StackAuthService] getAccessToken - User not initialized'); - throw new Error('User not initialized'); - } - - logger.debug('[StackAuthService] Calling user.getAuthJson()'); - const authJson = await this.userInstance.getAuthJson(); - logger.debug('[StackAuthService] getAuthJson returned', { - hasToken: !!authJson.accessToken, - tokenLength: authJson.accessToken?.length - }); - - if (!authJson.accessToken) { - logger.error('[StackAuthService] No access token available'); - throw new Error('No access token available'); - } - return authJson.accessToken; - } - - async refreshToken(): Promise { - this.callCount.refreshToken++; - logger.debug('[StackAuthService] refreshToken called', { - callCount: this.callCount.refreshToken, - hasUser: !!this.userInstance - }); - - if (!this.userInstance) { - throw new Error('User not initialized'); - } - // Stack handles token refresh internally - const authJson = await this.userInstance.getAuthJson(); - if (!authJson.accessToken) { - throw new Error('No access token available'); - } - return authJson.accessToken; - } - - async getCurrentUser(): Promise { - this.callCount.getCurrentUser++; - logger.debug('[StackAuthService] getCurrentUser called', { - callCount: this.callCount.getCurrentUser, - hasUser: !!this.userInstance, - userId: this.userInstance?.id - }); - // Return the actual Stack user instance - return this.userInstance; - } - - isAuthenticated(): boolean { - this.callCount.isAuthenticated++; - const isAuth = !!this.userInstance; - logger.debug('[StackAuthService] isAuthenticated called', { - callCount: this.callCount.isAuthenticated, - result: isAuth, - hasUserInstance: !!this.userInstance, - userId: this.userInstance?.id, - timestamp: new Date().toISOString() - }); - return isAuth; - } - - redirectToLogin(): void { - if (typeof window !== 'undefined') { - window.location.href = '/handler/sign-in'; - } - } - - async logout(): Promise { - if (this.userInstance && this.userInstance.signOut) { - await this.userInstance.signOut(); - } - } - - getSelectedTeam(): unknown { - return this.userInstance?.selectedTeam; - } - - async listPermissions(team?: unknown): Promise> { - if (!this.userInstance || !this.userInstance.listPermissions) { - return []; - } - const targetTeam = team || this.userInstance.selectedTeam; - if (!targetTeam) { - return []; - } - try { - const perms = await this.userInstance.listPermissions(targetTeam); - return Array.isArray(perms) ? perms : []; - } catch (error) { - logger.error('Error listing permissions:', error); - return []; - } - } - - getProviderName(): string { - return 'stack'; - } -} - diff --git a/ui/src/lib/files.ts b/ui/src/lib/files.ts index 8798956..1dd6067 100644 --- a/ui/src/lib/files.ts +++ b/ui/src/lib/files.ts @@ -3,17 +3,14 @@ import { getSignedUrlApiV1S3SignedUrlGet } from "@/client/sdk.gen"; /** * Get a signed URL and download a file */ -export async function downloadFile(url: string | null, accessToken: string) { - if (!url || !accessToken) return; +export async function downloadFile(url: string | null) { + if (!url) return; try { const response = await getSignedUrlApiV1S3SignedUrlGet({ query: { key: url }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (response.data?.url) { @@ -28,8 +25,8 @@ export async function downloadFile(url: string | null, accessToken: string) { * Return a signed URL for a given S3 key without triggering a download. * Useful for previewing media (audio or transcript) in-browser first. */ -export async function getSignedUrl(url: string | null, accessToken: string, inline: boolean = false): Promise { - if (!url || !accessToken) return null; +export async function getSignedUrl(url: string | null, inline: boolean = false): Promise { + if (!url) return null; try { const response = await getSignedUrlApiV1S3SignedUrlGet({ @@ -37,9 +34,6 @@ export async function getSignedUrl(url: string | null, accessToken: string, inli key: url, inline: inline, }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, }); if (response.data?.url) {