feat: implement server-side proxy with google-auth-library
Rewrite server.js to use google-auth-library for generating Google ID tokens when authenticating requests to the backend Cloud Run service. Key changes: - Replace metadata server token fetch with GoogleAuth client - Implement proper SSE streaming with raw byte passthrough - Add health check endpoint for Cloud Run monitoring - Update server to listen on 0.0.0.0:8080 for Cloud Run compatibility - Add environment variable validation at startup - Improve error handling and logging throughout request lifecycle Update Dockerfile to install google-auth-library dependency and expose port 8080 instead of port 80.
This commit is contained in:
parent
2e7db6a952
commit
2d35f2d498
|
|
@ -11,6 +11,6 @@ FROM node:20-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY server.js .
|
COPY server.js .
|
||||||
RUN npm install express@4
|
RUN npm install express@4 google-auth-library
|
||||||
EXPOSE 80
|
EXPOSE 8080
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|
|
||||||
78
server.js
78
server.js
|
|
@ -1,63 +1,105 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { GoogleAuth } = require('google-auth-library');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 80;
|
const port = process.env.PORT || 8080;
|
||||||
const BACKEND_URL = process.env.BACKEND_URL;
|
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());
|
app.use(express.json());
|
||||||
|
|
||||||
// Serve static React build
|
// Serve static React build
|
||||||
app.use(express.static(path.join(__dirname, 'dist')));
|
app.use(express.static(path.join(__dirname, 'dist')));
|
||||||
|
|
||||||
// Proxy endpoint - fetches identity token and forwards to backend
|
// Proxy endpoint - generates ID token and forwards to backend
|
||||||
app.post('/api/chat/stream', async (req, res) => {
|
app.post('/api/chat/stream', async (req, res) => {
|
||||||
|
console.log('Proxy request received for /api/chat/stream');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch identity token from GCP metadata server
|
// Get ID token client with backend URL as audience
|
||||||
const tokenResponse = await fetch(
|
const client = await auth.getIdTokenClient(BACKEND_URL);
|
||||||
`http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${BACKEND_URL}`,
|
const headers = await client.getRequestHeaders();
|
||||||
{ headers: { 'Metadata-Flavor': 'Google' } }
|
|
||||||
);
|
console.log('Generated ID token, forwarding to backend...');
|
||||||
const identityToken = await tokenResponse.text();
|
|
||||||
|
|
||||||
// Forward request to backend with auth
|
// Forward request to backend with auth
|
||||||
const backendResponse = await fetch(`${BACKEND_URL}/chat/stream`, {
|
const backendResponse = await fetch(`${BACKEND_URL}/chat/stream`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${identityToken}`,
|
...headers,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(req.body),
|
body: JSON.stringify(req.body),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream response back to client
|
// 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('Content-Type', 'text/event-stream');
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
res.setHeader('Connection', 'keep-alive');
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.flushHeaders?.();
|
||||||
|
|
||||||
|
// Pipe raw SSE stream from backend to browser (no parsing)
|
||||||
const reader = backendResponse.body.getReader();
|
const reader = backendResponse.body.getReader();
|
||||||
const pump = async () => {
|
|
||||||
|
// Handle client disconnect
|
||||||
|
req.on('close', () => {
|
||||||
|
console.log('Client disconnected');
|
||||||
|
reader.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stream loop
|
||||||
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) {
|
if (done) {
|
||||||
|
console.log('Stream complete');
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.write(value);
|
res.write(value);
|
||||||
pump();
|
}
|
||||||
};
|
|
||||||
pump();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Proxy error:', error);
|
console.error('Proxy error:', error);
|
||||||
res.status(500).json({ error: 'Proxy 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
|
// SPA fallback - all other routes serve index.html
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
// Listen on 0.0.0.0 for Cloud Run
|
||||||
console.log(`Server running on port ${PORT}`);
|
app.listen(port, '0.0.0.0', () => {
|
||||||
|
console.log(`Server listening on ${port}`);
|
||||||
|
console.log(`Backend URL: ${BACKEND_URL}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue