feat: add Node.js Express server for IAP and backend auth
Replace Python http.server with Express to handle: - Large IAP JWT headers that caused 500 errors - API request proxying with GCP identity tokens - Static React build serving with SPA fallback Update api.ts to use relative URLs in production for proxy routing.
This commit is contained in:
parent
5eb6f9ea31
commit
5195901f01
23
Dockerfile
23
Dockerfile
|
|
@ -1,4 +1,4 @@
|
||||||
# ---------- Build stage (Keep this the same) ----------
|
# Build stage
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
@ -6,18 +6,11 @@ RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ---------- Diagnostic Production stage ----------
|
# Production stage
|
||||||
FROM python:3.11-slim
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
# Set the working directory to where the React files live
|
COPY --from=build /app/dist ./dist
|
||||||
WORKDIR /usr/share/nginx/html
|
COPY server.js .
|
||||||
|
RUN npm install express
|
||||||
# Copy build output from the build stage
|
|
||||||
COPY --from=build /app/dist .
|
|
||||||
|
|
||||||
# Expose port 80 (Cloud Run expects this)
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
CMD ["node", "server.js"]
|
||||||
# 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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { ChatMode } from '@/types/chat'
|
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 API_ENDPOINT = import.meta.env.VITE_API_ENDPOINT || '/chat/stream'
|
||||||
const USE_MOCK_DATA = false // Set to true to use mock data for testing
|
const USE_MOCK_DATA = false // Set to true to use mock data for testing
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue