tyndale-ai-frontend/server.js

106 lines
3.0 KiB
JavaScript

const express = require('express');
const path = require('path');
const { GoogleAuth } = require('google-auth-library');
const app = express();
const port = process.env.PORT || 8080;
const BACKEND_URL = process.env.BACKEND_URL;
// Validate required environment variable at startup
if (!BACKEND_URL) {
console.error('ERROR: BACKEND_URL environment variable is required');
process.exit(1);
}
// Initialize Google Auth client (auto-detects credentials on Cloud Run)
const auth = new GoogleAuth();
app.use(express.json());
// Serve static React build
app.use(express.static(path.join(__dirname, 'dist')));
// Proxy endpoint - generates ID token and forwards to backend
app.post('/api/chat/stream', async (req, res) => {
console.log('Proxy request received for /api/chat/stream');
try {
// Get ID token client with backend URL as audience
const client = await auth.getIdTokenClient(BACKEND_URL);
const headers = await client.getRequestHeaders();
console.log('Generated ID token, forwarding to backend...');
// Forward request to backend with auth
const backendResponse = await fetch(`${BACKEND_URL}/chat/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(req.body),
});
// Check if backend returned an error
if (!backendResponse.ok) {
const errorText = await backendResponse.text();
console.error(`Backend error: ${backendResponse.status} - ${errorText}`);
res.status(backendResponse.status).json({
error: 'Backend error',
status: backendResponse.status,
message: errorText,
});
return;
}
// Set SSE headers and flush immediately
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
// Pipe raw SSE stream from backend to browser (no parsing)
const reader = backendResponse.body.getReader();
// Handle client disconnect
req.on('close', () => {
console.log('Client disconnected');
reader.cancel();
});
// Stream loop
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream complete');
res.end();
return;
}
res.write(value);
}
} catch (error) {
console.error('Proxy error:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Proxy error', message: error.message });
} else {
res.end();
}
}
});
// Health check endpoint for Cloud Run
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
// SPA fallback - all other routes serve index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// Listen on 0.0.0.0 for Cloud Run
app.listen(port, '0.0.0.0', () => {
console.log(`Server listening on ${port}`);
console.log(`Backend URL: ${BACKEND_URL}`);
});