diff --git a/api/services/mps_service_key_client.py b/api/services/mps_service_key_client.py index 3bbc3f4..8f49ad5 100644 --- a/api/services/mps_service_key_client.py +++ b/api/services/mps_service_key_client.py @@ -65,12 +65,12 @@ class MPSServiceKeyClient: data = response.json() # Transform the response to match our expected format return { - "id": data.get("id"), - "name": data.get("name"), - "service_key": data.get("service_key"), # Only returned on creation - "key_prefix": data.get("service_key", "")[:8] + "id": data.get("id"), + "name": data.get("name") or name, + "service_key": data.get("service_key"), + "key_prefix": data.get("key_prefix") or (data.get("service_key", "")[:8] if data.get("service_key") - else "", + else ""), "expires_at": data.get("expires_at"), "created_at": data.get("created_at"), "is_active": data.get("is_active", True), diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 8a714cc..23db1fa 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -15,7 +15,7 @@ from pipecat.services.openai.llm import OpenAILLMContext from pipecat.transports.base_transport import BaseTransport from pipecat.utils.enums import EndTaskReason -from api.constants import VOICEMAIL_RECORDING_DURATION +from api.constants import DEPLOYMENT_MODE, ENABLE_TRACING, VOICEMAIL_RECORDING_DURATION from api.services.gender.gender_service import GenderService from api.services.workflow.disposition_mapper import ( apply_disposition_mapping, @@ -480,7 +480,9 @@ class PipecatEngine: async def _handle_start_node(self, node: Node) -> None: """Handle start node execution.""" # Handle voicemail detection setup (before any returns) - if node.detect_voicemail: + # Lets check ENABLE_TRACING to make sure we have prompt access from + # langfuse + if node.detect_voicemail and DEPLOYMENT_MODE == 'saas' and ENABLE_TRACING: if not self._audio_buffer: logger.warning( "Voicemail detection enabled but no audio buffer available - skipping detection" diff --git a/docker-compose.yaml b/docker-compose.yaml index e66c522..223b6be 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -169,6 +169,9 @@ services: # Server-side URL (SSR, internal Docker network) BACKEND_URL: "http://api:8000" + # Controls some feature flags like voicemail detection + NEXT_PUBLIC_DEPLOYMENT_MODE: "oss" + # Posthog NEXT_PUBLIC_ENABLE_POSTHOG: "true" NEXT_PUBLIC_POSTHOG_KEY: "phc_st6dverimoydkpM5m0a9aeAr8znUYWznEaQa8v80E2D" diff --git a/ui/src/app/after-sign-in/page.tsx b/ui/src/app/after-sign-in/page.tsx index 10ce7ea..eabad50 100644 --- a/ui/src/app/after-sign-in/page.tsx +++ b/ui/src/app/after-sign-in/page.tsx @@ -1,20 +1,32 @@ import { redirect } from "next/navigation"; import { getServerAuthProvider, getServerUser } from "@/lib/auth/server"; +import logger from '@/lib/logger'; import { getRedirectUrl } from "@/lib/utils"; +export const dynamic = 'force-dynamic'; + export default async function AfterSignInPage() { + logger.debug('[AfterSignInPage] Starting after-sign-in page'); const authProvider = getServerAuthProvider(); + logger.debug('[AfterSignInPage] Auth provider:', authProvider); + logger.debug('[AfterSignInPage] Getting server user...'); const user = await getServerUser(); + logger.debug('[AfterSignInPage] Got user:', { hasUser: !!user, userId: user?.id }); if (authProvider === 'stack' && user && 'getAuthJson' in user) { + logger.debug('[AfterSignInPage] Stack user detected, getting auth token...'); const token = await user.getAuthJson(); + logger.debug('[AfterSignInPage] Got token:', { hasToken: !!token?.accessToken }); const permissions = 'listPermissions' in user && 'selectedTeam' in user ? await user.listPermissions(user.selectedTeam!) ?? [] : []; + logger.debug('[AfterSignInPage] Got permissions:', { count: permissions.length }); const redirectUrl = await getRedirectUrl(token?.accessToken ?? "", permissions); + logger.debug('[AfterSignInPage] Redirecting to:', redirectUrl); redirect(redirectUrl); } // For local provider or if user is not available, redirect to create-workflow + logger.debug('[AfterSignInPage] Fallback redirect to /create-workflow'); redirect('/create-workflow'); } diff --git a/ui/src/app/api-keys/page.tsx b/ui/src/app/api-keys/page.tsx index 9991d12..80d4d17 100644 --- a/ui/src/app/api-keys/page.tsx +++ b/ui/src/app/api-keys/page.tsx @@ -21,10 +21,18 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/lib/auth'; +import logger from '@/lib/logger'; export default function APIKeysPage() { const { user, getAccessToken, redirectToLogin, loading } = useAuth(); + logger.debug('[APIKeysPage] Component render', { + loading, + hasUser: !!user, + userId: user?.id, + timestamp: new Date().toISOString() + }); + const [apiKeys, setApiKeys] = useState([]); const [serviceKeys, setServiceKeys] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -49,12 +57,24 @@ export default function APIKeysPage() { }, [loading, user, redirectToLogin]); const fetchApiKeys = useCallback(async () => { - if (!user) return; + logger.debug('[APIKeysPage] fetchApiKeys called', { + loading, + hasUser: !!user, + userId: user?.id + }); + + // Follow the pattern from UserConfigContext - check both loading and user + if (loading || !user) { + logger.debug('[APIKeysPage] fetchApiKeys - skipping due to loading or no user'); + return; + } try { setIsLoading(true); setError(null); + logger.debug('[APIKeysPage] fetchApiKeys - calling getAccessToken...'); const accessToken = await getAccessToken(); + logger.debug('[APIKeysPage] fetchApiKeys - got access token'); const response = await getApiKeysApiV1UserApiKeysGet({ query: { @@ -76,15 +96,27 @@ export default function APIKeysPage() { } finally { setIsLoading(false); } - }, [user, getAccessToken, showArchived]); + }, [loading, user, getAccessToken, showArchived]); const fetchServiceKeys = useCallback(async () => { - if (!user) return; + logger.debug('[APIKeysPage] fetchServiceKeys called', { + loading, + hasUser: !!user, + userId: user?.id + }); + + // Follow the pattern from UserConfigContext - check both loading and user + if (loading || !user) { + logger.debug('[APIKeysPage] fetchServiceKeys - skipping due to loading or no user'); + return; + } try { setIsServiceKeysLoading(true); setError(null); + logger.debug('[APIKeysPage] fetchServiceKeys - calling getAccessToken...'); const accessToken = await getAccessToken(); + logger.debug('[APIKeysPage] fetchServiceKeys - got access token'); const response = await getServiceKeysApiV1UserServiceKeysGet({ query: { @@ -104,13 +136,15 @@ export default function APIKeysPage() { } finally { setIsServiceKeysLoading(false); } - }, [user, getAccessToken, showServiceArchived]); + }, [loading, user, getAccessToken, showServiceArchived]); useEffect(() => { + logger.debug('[APIKeysPage] useEffect for fetchApiKeys triggered'); fetchApiKeys(); }, [fetchApiKeys]); useEffect(() => { + logger.debug('[APIKeysPage] useEffect for fetchServiceKeys triggered'); fetchServiceKeys(); }, [fetchServiceKeys]); diff --git a/ui/src/app/integrations/page.tsx b/ui/src/app/integrations/page.tsx index 2f35244..349bb1b 100644 --- a/ui/src/app/integrations/page.tsx +++ b/ui/src/app/integrations/page.tsx @@ -6,6 +6,8 @@ import logger from '@/lib/logger'; import CreateIntegrationButton from "./CreateIntegrationButton"; +export const dynamic = 'force-dynamic'; + // Server component for integration list async function IntegrationList() { const authProvider = getServerAuthProvider(); diff --git a/ui/src/app/looptalk/page.tsx b/ui/src/app/looptalk/page.tsx index d4bc3aa..b5d143b 100644 --- a/ui/src/app/looptalk/page.tsx +++ b/ui/src/app/looptalk/page.tsx @@ -6,6 +6,8 @@ import { getServerAuthProvider, isServerAuthenticated } from '@/lib/auth/server' import LoopTalkLayout from "./LoopTalkLayout"; +export const dynamic = 'force-dynamic'; + async function PageContent() { const authProvider = getServerAuthProvider(); const isAuthenticated = await isServerAuthenticated(); diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index bd59e22..c7338f9 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -5,29 +5,42 @@ import { getServerAuthProvider,getServerUser } from "@/lib/auth/server"; import logger from '@/lib/logger'; import { getRedirectUrl } from "@/lib/utils"; +export const dynamic = 'force-dynamic'; + export default async function Home() { + logger.debug('[HomePage] Starting Home page render'); const authProvider = getServerAuthProvider(); + logger.debug('[HomePage] Auth provider:', authProvider); // For local/OSS provider, always redirect to workflow page if (authProvider === 'local') { - logger.debug('Redirecting to workflow page for local provider'); + logger.debug('[HomePage] Redirecting to workflow page for local provider'); redirect('/create-workflow'); } + logger.debug('[HomePage] Getting server user...'); const user = await getServerUser(); - logger.debug(`authProvider: ${authProvider}, user: ${JSON.stringify(user)}`); + logger.debug('[HomePage] Server user result:', { + hasUser: !!user, + userId: user?.id, + authProvider + }); if (user) { try { // For Stack provider, get the token and permissions if (authProvider === 'stack' && 'getAuthJson' in user) { + logger.debug('[HomePage] Getting auth token from Stack user...'); const token = await user.getAuthJson(); + logger.debug('[HomePage] Got auth token:', { hasToken: !!token?.accessToken }); const permissions = 'listPermissions' in user && 'selectedTeam' in user ? await user.listPermissions(user.selectedTeam!) ?? [] : []; + logger.debug('[HomePage] Got permissions:', { count: permissions.length }); + logger.debug('[HomePage] Getting redirect URL...'); const redirectUrl = await getRedirectUrl(token?.accessToken ?? "", permissions); - logger.debug(`redirectUrl: ${redirectUrl}`); + logger.debug('[HomePage] Redirecting to:', redirectUrl); redirect(redirectUrl); } } catch (error) { diff --git a/ui/src/app/workflow/page.tsx b/ui/src/app/workflow/page.tsx index 46a1787..fa4d466 100644 --- a/ui/src/app/workflow/page.tsx +++ b/ui/src/app/workflow/page.tsx @@ -13,6 +13,8 @@ import logger from '@/lib/logger'; import WorkflowLayout from "./WorkflowLayout"; +export const dynamic = 'force-dynamic'; + // Server component for workflow templates async function WorkflowTemplatesList() { try { diff --git a/ui/src/components/flow/nodes/StartCall.tsx b/ui/src/components/flow/nodes/StartCall.tsx index e112ed3..7053fed 100644 --- a/ui/src/components/flow/nodes/StartCall.tsx +++ b/ui/src/components/flow/nodes/StartCall.tsx @@ -170,6 +170,7 @@ const StartCallEditForm = ({ delayedStartDuration, setDelayedStartDuration }: StartCallEditFormProps) => { + const isOSS = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE === 'oss'; return (
@@ -240,19 +241,21 @@ const StartCallEditForm = ({
-
- - - -
+ {!isOSS && ( +
+ + + +
+ )}
context.service.logout(), [context.service]); const redirectToLogin = React.useCallback(() => context.service.redirectToLogin(), [context.service]); diff --git a/ui/src/lib/auth/providers/StackProviderWrapper.tsx b/ui/src/lib/auth/providers/StackProviderWrapper.tsx index 4daab52..b8b61c8 100644 --- a/ui/src/lib/auth/providers/StackProviderWrapper.tsx +++ b/ui/src/lib/auth/providers/StackProviderWrapper.tsx @@ -3,10 +3,28 @@ import { StackClientApp,StackProvider, StackTheme, useUser as useStackUser } from '@stackframe/stack'; import React, { useEffect, useState } from 'react'; +import logger from '@/lib/logger'; + import { StackAuthService } from '../services'; import type { AuthUser } from '../types'; import { AuthContext } from './AuthProvider'; +// Create a singleton StackClientApp instance to prevent multiple initializations +let stackClientAppInstance: StackClientApp | null = null; + +function getStackClientApp(): StackClientApp { + if (!stackClientAppInstance) { + logger.debug('[StackProviderWrapper] Creating singleton StackClientApp instance'); + stackClientAppInstance = new StackClientApp({ + tokenStore: "nextjs-cookie", + urls: { + afterSignIn: "/after-sign-in" + } + }); + } + return stackClientAppInstance; +} + interface StackProviderWrapperProps { service: StackAuthService; children: React.ReactNode; @@ -14,29 +32,84 @@ interface StackProviderWrapperProps { // Stack-specific context provider that uses the useUser hook function StackAuthContextProvider({ service, children }: { service: StackAuthService; children: React.ReactNode }) { - const [loading, setLoading] = useState(true); + const renderCount = React.useRef(0); + const lastUserId = React.useRef(undefined); + renderCount.current++; + + logger.debug(`[StackAuthContextProvider] Render #${renderCount.current} - Starting`); + const stackUser = useStackUser(); // Always call the hook + const [isInitialized, setIsInitialized] = useState(false); + + // Track if user actually changed + const userChanged = lastUserId.current !== stackUser?.id; + if (userChanged) { + lastUserId.current = stackUser?.id; + } + + logger.debug(`[StackAuthContextProvider] Render #${renderCount.current} - stackUser:`, { + hasUser: !!stackUser, + userId: stackUser?.id, + isInitialized, + userChanged + }); useEffect(() => { - // Set the user instance in the service - if (service instanceof StackAuthService && stackUser) { - service.setUserInstance(stackUser); - setLoading(false); - } else if (!stackUser) { - setLoading(false); + // Only log and update if user actually changed + if (!userChanged && isInitialized) { + return; } - }, [service, stackUser]); - const getAccessToken = React.useCallback(() => service.getAccessToken(), [service]); + logger.debug('[StackAuthContextProvider] useEffect triggered (user changed)', { + hasUser: !!stackUser, + userId: stackUser?.id, + isInitialized, + isStackAuthService: service instanceof StackAuthService + }); - const contextValue = React.useMemo(() => ({ - service, - user: stackUser as AuthUser, // Pass the actual Stack CurrentUser - isAuthenticated: service.isAuthenticated(), - loading, - getAccessToken, - provider: service.getProviderName(), - }), [service, stackUser, loading, getAccessToken]); + // Only update the service once when user becomes available + if (!isInitialized && service instanceof StackAuthService && stackUser) { + logger.debug('[StackAuthContextProvider] Setting user instance in service', { + userId: stackUser.id + }); + service.setUserInstance(stackUser); + setIsInitialized(true); + } + }, [service, stackUser, isInitialized, userChanged]); + + const getAccessToken = React.useCallback(() => { + logger.debug('[StackAuthContextProvider] getAccessToken called'); + return service.getAccessToken(); + }, [service]); + + // Stabilize the context value to prevent unnecessary re-renders + const contextValue = React.useMemo(() => { + const isAuth = service.isAuthenticated(); + // IMPORTANT: Stay in loading state until service is initialized (has user set) + // Even if stackUser exists, we're still loading until setUserInstance is called + const loadingState = !isInitialized; + + const value = { + service, + user: stackUser as AuthUser, // Pass the actual Stack CurrentUser + isAuthenticated: isAuth, + loading: loadingState, // Loading until service is initialized + getAccessToken, + provider: service.getProviderName(), + }; + + logger.debug('[StackAuthContextProvider] Context value created', { + isAuthenticated: isAuth, + loading: loadingState, + hasUser: !!value.user, + userId: stackUser?.id, + isInitialized, + provider: value.provider, + serviceHasUser: isAuth + }); + + return value; + }, [service, stackUser, isInitialized, getAccessToken]); return ( @@ -46,13 +119,10 @@ function StackAuthContextProvider({ service, children }: { service: StackAuthSer } export function StackProviderWrapper({ service, children }: StackProviderWrapperProps) { - // Create the Stack client app here, only when actually needed - const stackClientApp = new StackClientApp({ - tokenStore: "nextjs-cookie", - urls: { - afterSignIn: "/after-sign-in" - } - }); + logger.debug('[StackProviderWrapper] Rendering wrapper'); + + // Use the singleton instance + const stackClientApp = getStackClientApp(); return ( diff --git a/ui/src/lib/auth/services/index.ts b/ui/src/lib/auth/services/index.ts index 187dc1a..ee0fdcd 100644 --- a/ui/src/lib/auth/services/index.ts +++ b/ui/src/lib/auth/services/index.ts @@ -1,16 +1,30 @@ +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': - return new StackAuthService(); + if (!stackServiceInstance) { + logger.debug('[createAuthService] Creating singleton StackAuthService instance'); + stackServiceInstance = new StackAuthService(); + } + return stackServiceInstance; case 'local': - return new LocalAuthService(); + 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(); @@ -18,7 +32,10 @@ export function createAuthService(provider?: AuthProvider | string): IAuthServic // return new SupabaseService(); default: console.warn(`Unknown auth provider: ${authProvider}, falling back to local`); - return new LocalAuthService(); + if (!localServiceInstance) { + localServiceInstance = new LocalAuthService(); + } + return localServiceInstance; } } diff --git a/ui/src/lib/auth/services/stackAuthService.ts b/ui/src/lib/auth/services/stackAuthService.ts index 407bbc6..1eafbe7 100644 --- a/ui/src/lib/auth/services/stackAuthService.ts +++ b/ui/src/lib/auth/services/stackAuthService.ts @@ -8,24 +8,62 @@ 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'); } @@ -38,12 +76,27 @@ export class StackAuthService implements IAuthService { } 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 { - return !!this.userInstance; + 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 { diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 5de715e..0bb755e 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -27,27 +27,43 @@ export function debounce unknown>(func: T, wai } export async function getRedirectUrl(token: string, permissions: { id: string }[] = []) { + console.log('[getRedirectUrl] Called with:', { + hasToken: !!token, + tokenLength: token?.length, + permissionsCount: permissions.length, + permissions: permissions.map(p => p.id) + }); try { + console.log('[getRedirectUrl] Calling getAuthUserApiV1UserAuthUserGet...'); const authUser = await getAuthUserApiV1UserAuthUserGet({ headers: { Authorization: `Bearer ${token}`, }, }); + console.log('[getRedirectUrl] Auth user response:', { + hasData: !!authUser.data, + isSuperuser: authUser.data?.is_superuser, + userId: authUser.data?.id + }); if (authUser.data?.is_superuser) { + console.log('[getRedirectUrl] User is superuser, redirecting to /superadmin'); return "/superadmin"; } const hasAdminPermission = permissions.some(p => p.id === 'admin'); + console.log('[getRedirectUrl] Admin permission check:', { hasAdminPermission }); // If the user doesn't have admin permissions, redirect them to // usage page if (!hasAdminPermission) { + console.log('[getRedirectUrl] No admin permission, redirecting to /usage'); return "/usage"; } + console.log('[getRedirectUrl] Has admin permission, redirecting to /create-workflow'); return "/create-workflow"; } catch (error) { - console.error("Failed to fetch auth user:", error); + console.error("[getRedirectUrl] Failed to fetch auth user:", error); // Re-throw the error so the caller can handle it throw error; }