fix: ssl error when using self signed certificate (#238)

fix: ssl error when using self signed certificate with remote deployment
This commit is contained in:
Abhishek 2026-04-14 18:58:27 +05:30 committed by GitHub
parent 7fab959e26
commit 50a59164e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 285 additions and 292 deletions

View file

@ -6,19 +6,10 @@ import type { HealthResponse } from "@/client/types.gen";
// Import version from package.json at build time
import packageJson from "../../../../../package.json";
// Internal/local URLs that are not reachable from the browser
const INTERNAL_HOST_RE = /^https?:\/\/(localhost|127\.0\.0\.1|api)(:\d+)?(\/|$)/;
function isInternalUrl(url: string | undefined | null): boolean {
return !url || INTERNAL_HOST_RE.test(url);
}
export async function GET() {
const uiVersion = packageJson.version || "dev";
// Fetch backend version and config from health endpoint
let apiVersion = "unknown";
let backendApiEndpoint: string | null = null;
let deploymentMode = "oss";
let authProvider = "local";
@ -27,28 +18,16 @@ export async function GET() {
if (response.data) {
const data = response.data as HealthResponse;
apiVersion = data.version;
// Pass through the backend's own endpoint for display purposes
backendApiEndpoint = data.backend_api_endpoint;
deploymentMode = data.deployment_mode;
authProvider = data.auth_provider;
}
} catch {
// Backend might not be reachable during build or in some deployments
apiVersion = "unavailable";
}
// For the API client base URL: prefer BACKEND_URL env, fall back to
// health endpoint value. Skip internal/Docker-only URLs (e.g. http://api:8000)
// that aren't reachable from the browser — the client will keep using
// window.location.origin via the Next.js proxy instead.
const clientCandidate = process.env.BACKEND_URL || backendApiEndpoint;
const clientApiBaseUrl = isInternalUrl(clientCandidate) ? 'http://localhost:8000' : clientCandidate;
return NextResponse.json({
ui: uiVersion,
api: apiVersion,
backendApiEndpoint,
clientApiBaseUrl,
deploymentMode,
authProvider,
});

View file

@ -1,140 +1,15 @@
'use client';
import Link from 'next/link';
import { Suspense } from 'react';
import { useEffect, useRef, useState } from 'react';
import { getIntegrationsApiV1IntegrationGet } from "@/client/sdk.gen";
import { getServerAccessToken,getServerAuthProvider } from '@/lib/auth/server';
import type { IntegrationResponse } from '@/client/types.gen';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import CreateIntegrationButton from "./CreateIntegrationButton";
export const dynamic = 'force-dynamic';
// Server component for integration list
async function IntegrationList() {
const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
const { redirect } = await import('next/navigation');
if (authProvider === 'stack') {
redirect('/');
} else {
// For OSS mode, this shouldn't happen as token is auto-generated
return (
<div className="text-red-500">
Authentication required. Please refresh the page.
</div>
);
}
}
try {
const response = await getIntegrationsApiV1IntegrationGet({
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const integrationData = response.data ? (Array.isArray(response.data) ? response.data : [response.data]) : [];
const integrations = [...integrationData].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
if (integrations.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
No integrations found. Create your first integration to get started.
</div>
);
}
return (
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="min-w-full bg-card border border-border">
<thead className="bg-muted">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Channel
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Created At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{integrations.map((integration) => (
<tr key={integration.id} className="hover:bg-muted/50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{integration.provider}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'slack' && integration.provider_data ? (integration.provider_data.channel as string) || '-' : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.action}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{new Date(integration.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'google-mail' && (
<Link
href={`/integrations/${integration.id}/gmail`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Search
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
} catch (err) {
logger.error(`Error fetching integrations: ${err}`);
return (
<div className="text-red-500 text-center py-8">
Failed to load Integrations. Please Try Again Later.
</div>
);
}
}
async function PageContent() {
const integrationList = await IntegrationList();
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Your Integrations</h1>
<CreateIntegrationButton />
</div>
{integrationList}
</div>
</div>
);
}
function IntegrationsLoading() {
return (
<div className="container mx-auto px-4 py-8">
@ -187,9 +62,113 @@ function IntegrationsLoading() {
}
export default function IntegrationsPage() {
const { user, loading: authLoading } = useAuth();
const hasFetched = useRef(false);
const [integrations, setIntegrations] = useState<IntegrationResponse[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (authLoading || !user || hasFetched.current) return;
hasFetched.current = true;
const fetchIntegrations = async () => {
try {
const response = await getIntegrationsApiV1IntegrationGet({});
const integrationData = response.data ? (Array.isArray(response.data) ? response.data : [response.data]) : [];
const sorted = [...integrationData].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
setIntegrations(sorted);
} catch (err) {
logger.error(`Error fetching integrations: ${err}`);
setError('Failed to load Integrations. Please Try Again Later.');
}
};
fetchIntegrations();
}, [authLoading, user]);
if (authLoading || (integrations === null && !error)) {
return <IntegrationsLoading />;
}
return (
<Suspense fallback={<IntegrationsLoading />}>
<PageContent />
</Suspense>
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Your Integrations</h1>
<CreateIntegrationButton />
</div>
{error ? (
<div className="text-red-500 text-center py-8">{error}</div>
) : !integrations || integrations.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No integrations found. Create your first integration to get started.
</div>
) : (
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="min-w-full bg-card border border-border">
<thead className="bg-muted">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Channel
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Created At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{integrations.map((integration) => (
<tr key={integration.id} className="hover:bg-muted/50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{integration.provider}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'slack' && integration.provider_data ? (integration.provider_data.channel as string) || '-' : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.action}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{new Date(integration.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'google-mail' && (
<Link
href={`/integrations/${integration.id}/gmail`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Search
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,99 +1,22 @@
'use client';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import { useParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet } from '@/client/sdk.gen';
import type { TestSessionResponse } from '@/client/types.gen';
import { ConversationsList } from '@/components/looptalk/ConversationsList';
import { LiveAudioPlayer } from '@/components/looptalk/LiveAudioPlayer';
import { TestSessionControls } from '@/components/looptalk/TestSessionControls';
import { TestSessionDetails } from '@/components/looptalk/TestSessionDetails';
import { Button } from '@/components/ui/button';
import { getServerAccessToken,getServerAuthProvider } from '@/lib/auth/server';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import LoopTalkLayout from "../LoopTalkLayout";
interface PageProps {
params: Promise<{
id: string;
}>;
}
async function PageContent({ params }: PageProps) {
const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
const { redirect } = await import('next/navigation');
if (authProvider === 'stack') {
redirect('/');
} else {
// For OSS mode, this shouldn't happen as token is auto-generated
return (
<div className="text-red-500">
Authentication required. Please refresh the page.
</div>
);
}
}
try {
const resolvedParams = await params;
const testSessionId = parseInt(resolvedParams.id);
const response = await getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet({
path: {
test_session_id: testSessionId
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const testSession = response.data;
if (!testSession) {
notFound();
}
// Transform the API response to match our UI types
const sessionForUI = {
id: testSession.id,
name: testSession.name,
description: '', // API doesn't return description
test_type: testSession.test_index !== null ? 'load_test' : 'single',
status: testSession.status,
actor_workflow_name: `Workflow ${testSession.actor_workflow_id}`, // We'll need to fetch actual names
adversary_workflow_name: `Workflow ${testSession.adversary_workflow_id}`,
created_at: testSession.created_at,
updated_at: testSession.created_at, // API doesn't have updated_at
test_metadata: testSession.config
};
return (
<div className="container mx-auto px-4 py-8">
<TestSessionDetails session={sessionForUI} />
<TestSessionControls session={sessionForUI} />
{/* Persistent Audio Player */}
<div className="mt-6">
<LiveAudioPlayer
testSessionId={testSessionId}
sessionStatus={testSession.status as 'pending' | 'running' | 'completed' | 'failed'}
autoStart={true}
/>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Conversations</h2>
<ConversationsList testSessionId={testSessionId} />
</div>
</div>
);
} catch (err) {
logger.error(`Error fetching test session: ${err}`);
notFound();
}
}
function TestSessionLoading() {
return (
<div className="container mx-auto px-4 py-8">
@ -106,7 +29,87 @@ function TestSessionLoading() {
);
}
export default function TestSessionPage({ params }: PageProps) {
function TestSessionPageContent() {
const params = useParams();
const testSessionId = parseInt(params.id as string);
const { user, loading: authLoading } = useAuth();
const hasFetched = useRef(false);
const [testSession, setTestSession] = useState<TestSessionResponse | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (authLoading || !user || hasFetched.current) return;
hasFetched.current = true;
const fetchTestSession = async () => {
try {
const response = await getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet({
path: {
test_session_id: testSessionId
},
});
if (!response.data) {
setError('Test session not found');
return;
}
setTestSession(response.data);
} catch (err) {
logger.error(`Error fetching test session: ${err}`);
setError('Failed to load test session');
}
};
fetchTestSession();
}, [authLoading, user, testSessionId]);
if (authLoading || (testSession === null && !error)) {
return <TestSessionLoading />;
}
if (error || !testSession) {
return (
<div className="container mx-auto px-4 py-8">
<div className="text-red-500 text-center py-8">
{error || 'Test session not found'}
</div>
</div>
);
}
const sessionForUI = {
id: testSession.id,
name: testSession.name,
description: '',
test_type: testSession.test_index !== null ? 'load_test' : 'single',
status: testSession.status,
actor_workflow_name: `Workflow ${testSession.actor_workflow_id}`,
adversary_workflow_name: `Workflow ${testSession.adversary_workflow_id}`,
created_at: testSession.created_at,
updated_at: testSession.created_at,
test_metadata: testSession.config
};
return (
<div className="container mx-auto px-4 py-8">
<TestSessionDetails session={sessionForUI} />
<TestSessionControls session={sessionForUI} />
<div className="mt-6">
<LiveAudioPlayer
testSessionId={testSessionId}
sessionStatus={testSession.status as 'pending' | 'running' | 'completed' | 'failed'}
autoStart={true}
/>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Conversations</h2>
<ConversationsList testSessionId={testSessionId} />
</div>
</div>
);
}
export default function TestSessionPage() {
const backButton = (
<Link href="/looptalk">
<Button variant="ghost" size="sm">
@ -118,9 +121,7 @@ export default function TestSessionPage({ params }: PageProps) {
return (
<LoopTalkLayout backButton={backButton}>
<Suspense fallback={<TestSessionLoading />}>
<PageContent params={params} />
</Suspense>
<TestSessionPageContent />
</LoopTalkLayout>
);
}

View file

@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { NODE_DOCUMENTATION_URLS } from "@/constants/documentation";
import { useAppConfig } from "@/context/AppConfigContext";
import { NodeContent } from "./common/NodeContent";
import { NodeEditDialog } from "./common/NodeEditDialog";
@ -28,7 +27,6 @@ interface TriggerNodeProps extends NodeProps {
export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id });
const { saveWorkflow } = useWorkflow();
const { config } = useAppConfig();
// Form state
const [name, setName] = useState(data.name || "API Trigger");
@ -36,8 +34,9 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
// Generate trigger_path if not present (should be done on node creation)
const [triggerPath] = useState(() => data.trigger_path ?? crypto.randomUUID());
// Get backend URL from app config (fetched from backend health endpoint)
const backendUrl = config?.backendApiEndpoint || "http://localhost:8000";
const backendUrl =
process.env.NEXT_PUBLIC_BACKEND_URL ||
(typeof window !== 'undefined' ? window.location.origin : '');
const endpoint = `${backendUrl}/api/v1/public/agent/${triggerPath}`;
// Copy state for button feedback

View file

@ -6,7 +6,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useAppConfig } from '@/context/AppConfigContext';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
@ -46,7 +45,6 @@ export function LiveAudioPlayer({
const nextStartTimeRef = useRef(0);
const animationFrameRef = useRef<number | undefined>(undefined);
const isConnectingRef = useRef(false);
const { config } = useAppConfig();
const { user, getAccessToken } = useAuth();
// Auto-start streaming when session starts
@ -99,8 +97,8 @@ export function LiveAudioPlayer({
// Get auth token
const accessToken = await getAccessToken();
// Create WebSocket connection
const baseUrl = (config?.backendApiEndpoint || 'http://localhost:8000').replace(/^http/, 'ws');
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
@ -201,7 +199,7 @@ export function LiveAudioPlayer({
} finally {
isConnectingRef.current = false;
}
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel, config]); // Removed connectionStatus to avoid loops
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel]); // Removed connectionStatus to avoid loops
const disconnect = useCallback(() => {
if (wsRef.current) {

View file

@ -30,7 +30,8 @@ export function SimpleAudioPlayer({ testSessionId }: SimpleAudioPlayerProps) {
const accessToken = await getAccessToken();
// Create WebSocket connection - pass token as query param since WebSocket doesn't support headers
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace('http', 'ws') || 'ws://localhost:8000';
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;

View file

@ -2,12 +2,9 @@
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { client } from '@/client/client.gen';
interface AppConfig {
uiVersion: string;
apiVersion: string;
backendApiEndpoint: string | null;
deploymentMode: string;
authProvider: string;
}
@ -20,7 +17,6 @@ interface AppConfigContextType {
const defaultConfig: AppConfig = {
uiVersion: 'dev',
apiVersion: 'unknown',
backendApiEndpoint: null,
deploymentMode: 'oss',
authProvider: 'local',
};
@ -38,15 +34,9 @@ export function AppConfigProvider({ children }: { children: ReactNode }) {
fetch('/api/config/version')
.then((res) => res.json())
.then((data) => {
// Use clientApiBaseUrl (filtered for browser-reachable URLs)
// to configure the API client; keep backendApiEndpoint for display
if (data.clientApiBaseUrl) {
client.setConfig({ baseUrl: data.clientApiBaseUrl });
}
setConfig({
uiVersion: data.ui || 'dev',
apiVersion: data.api || 'unknown',
backendApiEndpoint: data.backendApiEndpoint || null,
deploymentMode: data.deploymentMode || 'oss',
authProvider: data.authProvider || 'local',
});

View file

@ -7,12 +7,9 @@ export const createClientConfig: CreateClientConfig = (config) => {
let baseUrl: string;
if (isServer) {
// for server-side rendering, still use environment variable as fallback
baseUrl = process.env.BACKEND_URL || 'http://api:8000';
} else {
// Client-side API calls are proxied through Next.js rewrites.
// AppConfigContext may update this later with the fetched backend URL.
baseUrl = window.location.origin;
baseUrl = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
}
return {