modcomms/frontend/services/geminiService.ts
michael 321a9ca820 Implement Microsoft MSAL SSO with PKCE flow
Frontend:
- Add @azure/msal-browser and @azure/msal-react packages
- Create authConfig.ts with MSAL configuration for PKCE flow
- Create authService.ts for token acquisition and user info
- Wrap App with MsalProvider in index.tsx
- Replace dummy login with real MSAL loginPopup() in Login.tsx
- Update App.tsx to use useIsAuthenticated/useMsal hooks
- Update Profile.tsx to display real user data from claims
- Update geminiService.ts to include access_token in WebSocket messages
- Update WIPReviewer.tsx to pass msalInstance for auth

Backend:
- Add python-jose and httpx dependencies for JWT verification
- Create auth_service.py with Azure AD JWKS fetching and token verification
- Create auth.py FastAPI dependency for protected REST endpoints
- Update main.py to verify tokens on WebSocket and protect /info endpoint
- Add AZURE_TENANT_ID, AZURE_CLIENT_ID, DISABLE_AUTH to config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 08:43:30 -06:00

202 lines
7 KiB
TypeScript

import type { AgentReview, SubReview, AgentName } from '../types';
import { IPublicClientApplication } from '@azure/msal-browser';
import { getAccessToken } from './authService';
// WebSocket URL for backend communication
const WS_URL = import.meta.env.VITE_BACKEND_WS_URL || 'ws://localhost:8000/ws/analyze';
const HTTP_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000';
/**
* Analyze a proof using the backend WebSocket API.
* Provides real-time updates as each agent completes.
* Now requires MSAL instance to acquire access token.
*/
export const analyzeProof = async (
file: File,
onAgentUpdate: (name: AgentName | 'Summary', review?: SubReview) => void,
msalInstance: IPublicClientApplication
): Promise<AgentReview> => {
// Acquire token before connecting
const accessToken = await getAccessToken(msalInstance);
if (!accessToken) {
throw new Error('Failed to acquire access token. Please sign in again.');
}
return new Promise((resolve, reject) => {
const ws = new WebSocket(WS_URL);
let resolved = false;
ws.onopen = () => {
// Convert file to base64 and send
const reader = new FileReader();
reader.onloadend = () => {
const base64Data = (reader.result as string).split(',')[1];
ws.send(JSON.stringify({
type: 'analyze',
file_data: base64Data,
file_type: file.type,
is_wip: false,
access_token: accessToken
}));
};
reader.onerror = () => {
ws.close();
reject(new Error('Failed to read file'));
};
reader.readAsDataURL(file);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
switch (message.type) {
case 'agent_started':
// Agent is starting - can optionally show loading state
break;
case 'agent_completed':
// Agent completed - update UI
onAgentUpdate(message.agent_name as AgentName, message.review);
break;
case 'summary':
// Summary ready
onAgentUpdate('Summary');
break;
case 'complete':
// Analysis complete - resolve with full result
resolved = true;
ws.close();
resolve(message.result as AgentReview);
break;
case 'error':
// Error occurred
resolved = true;
ws.close();
reject(new Error(message.message || 'Analysis failed'));
break;
}
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
reject(new Error('WebSocket connection error. Is the backend running?'));
}
};
ws.onclose = (event) => {
if (!resolved && !event.wasClean) {
resolved = true;
reject(new Error('WebSocket connection closed unexpectedly'));
}
};
});
};
/**
* Analyze a work-in-progress proof.
* Returns a conversational summary for the creative team.
*/
export const analyzeWIPProof = async (
file: File,
onAgentUpdate: (name: AgentName | 'Summary', review?: SubReview) => void,
msalInstance: IPublicClientApplication
): Promise<string> => {
// Acquire token before connecting
const accessToken = await getAccessToken(msalInstance);
if (!accessToken) {
throw new Error('Failed to acquire access token. Please sign in again.');
}
return new Promise((resolve, reject) => {
const ws = new WebSocket(WS_URL);
let resolved = false;
ws.onopen = () => {
const reader = new FileReader();
reader.onloadend = () => {
const base64Data = (reader.result as string).split(',')[1];
ws.send(JSON.stringify({
type: 'analyze',
file_data: base64Data,
file_type: file.type,
is_wip: true,
access_token: accessToken
}));
};
reader.onerror = () => {
ws.close();
reject(new Error('Failed to read file'));
};
reader.readAsDataURL(file);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'agent_completed') {
onAgentUpdate(message.agent_name as AgentName, message.review);
} else if (message.type === 'summary') {
onAgentUpdate('Summary');
} else if (message.type === 'complete') {
resolved = true;
ws.close();
resolve(message.result?.leadAgentSummary || 'Analysis complete.');
} else if (message.type === 'error') {
resolved = true;
ws.close();
reject(new Error(message.message || 'Analysis failed'));
}
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
reject(new Error('WebSocket connection error'));
}
};
ws.onclose = (event) => {
if (!resolved && !event.wasClean) {
resolved = true;
reject(new Error('Connection closed unexpectedly'));
}
};
});
};
/**
* Get a chat response from the WIP Lead Agent.
* Uses HTTP REST endpoint (not WebSocket).
*/
export const getWIPChatResponse = async (prompt: string): Promise<string> => {
try {
const response = await fetch(`${HTTP_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data.response || data.message || 'No response from Lead Agent.';
} catch (error) {
console.error('Error getting WIP chat response:', error);
// Fallback message when backend chat endpoint is not available
return "I'm sorry, the chat feature requires the backend chat endpoint to be implemented. For now, please use the proof analysis feature.";
}
};