diff --git a/Dockerfile b/Dockerfile index 28865d3..fd21fa5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# ---------- Build stage (Keep this the same) ---------- +# Build stage FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ @@ -6,18 +6,11 @@ RUN npm ci COPY . . RUN npm run build -# ---------- Diagnostic Production stage ---------- -FROM python:3.11-slim - -# Set the working directory to where the React files live -WORKDIR /usr/share/nginx/html - -# Copy build output from the build stage -COPY --from=build /app/dist . - -# Expose port 80 (Cloud Run expects this) +# Production stage +FROM node:20-alpine +WORKDIR /app +COPY --from=build /app/dist ./dist +COPY server.js . +RUN npm install express EXPOSE 80 - -# Run Python's built-in simple HTTP server -# This server is very "dumb" and will ignore/accept large IAP headers -CMD ["python", "-m", "http.server", "80"] +CMD ["node", "server.js"] diff --git a/server.js b/server.js new file mode 100644 index 0000000..02ada4e --- /dev/null +++ b/server.js @@ -0,0 +1,63 @@ +const express = require('express'); +const path = require('path'); + +const app = express(); +const PORT = process.env.PORT || 80; +const BACKEND_URL = process.env.BACKEND_URL; + +app.use(express.json()); + +// Serve static React build +app.use(express.static(path.join(__dirname, 'dist'))); + +// Proxy endpoint - fetches identity token and forwards to backend +app.post('/api/chat/stream', async (req, res) => { + try { + // Fetch identity token from GCP metadata server + const tokenResponse = await fetch( + `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${BACKEND_URL}`, + { headers: { 'Metadata-Flavor': 'Google' } } + ); + const identityToken = await tokenResponse.text(); + + // Forward request to backend with auth + const backendResponse = await fetch(`${BACKEND_URL}/chat/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${identityToken}`, + }, + body: JSON.stringify(req.body), + }); + + // Stream response back to client + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + const reader = backendResponse.body.getReader(); + const pump = async () => { + const { done, value } = await reader.read(); + if (done) { + res.end(); + return; + } + res.write(value); + pump(); + }; + pump(); + + } catch (error) { + console.error('Proxy error:', error); + res.status(500).json({ error: 'Proxy error' }); + } +}); + +// SPA fallback - all other routes serve index.html +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'dist', 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/src/lib/api.ts b/src/lib/api.ts index fcc7571..804b3f1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,8 @@ import type { ChatMode } from '@/types/chat' -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +// In production, use relative URL to route through the proxy server +// For local development, set VITE_API_URL=http://localhost:8000 in .env +const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' const API_ENDPOINT = import.meta.env.VITE_API_ENDPOINT || '/chat/stream' const USE_MOCK_DATA = false // Set to true to use mock data for testing