-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
257 lines (218 loc) · 8.89 KB
/
server.js
File metadata and controls
257 lines (218 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import express from 'express';
import compression from 'compression';
import cors from 'cors';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
app.use(compression());
app.use('/api', cors()); // Enable CORS for API routes so Flutter apps can sync data
// Trust the Google Cloud Run proxy so req.hostname resolves correctly
app.set('trust proxy', true);
// Redirect legacy URLs and alternative domains to the primary pocketgull.app domain
app.use((req, res, next) => {
const host = req.hostname || '';
if (host.includes('understory') || host.includes('pocketgall') || host.includes('pocketgull.com') || (host.includes('pocketgull') && !host.includes('pocketgull.app'))) {
return res.redirect(301, `https://pocketgull.app${req.originalUrl}`);
}
next();
});
const port = process.env.PORT || 3000;
// Use process.cwd() to ensure we are looking in the right place
const rootDir = process.cwd();
const distFolder = join(rootDir, 'dist');
console.log(`[SERVER] Starting...`);
console.log(`[SERVER] Current working directory: ${rootDir}`);
console.log(`[SERVER] Expected dist folder: ${distFolder}`);
let geminiApiKeyCached = '';
async function fetchGeminiApiKey() {
// Layer 1: Process Environment
if (process.env.GEMINI_API_KEY) {
console.log('[Secrets] Using GEMINI_API_KEY from environment.');
return process.env.GEMINI_API_KEY;
}
// Layer 2: Local Filesystem (.env or .env.local)
for (const envFile of ['.env.local', '.env']) {
try {
const localEnv = fs.readFileSync(join(rootDir, envFile), 'utf8');
const match = localEnv.match(/GEMINI_API_KEY=["']?([^"'\n]+)["']?/);
if (match) {
console.log(`[Secrets] Manual load success: ${envFile}`);
return match[1].trim();
}
} catch (e) { }
}
// Layer 3: Cloud Infrastructure (Secret Manager)
try {
const client = new SecretManagerServiceClient();
let projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
if (!projectId) {
console.log('[Secrets] GOOGLE_CLOUD_PROJECT not set, attempting to resolve automatically...');
projectId = await client.getProjectId();
}
if (!projectId) {
console.warn('[WARN] Could not determine project ID. Returning empty string.');
return '';
}
console.log(`[Secrets] Fetching GEMINI_API_KEY from GCP Secret Manager for project ${projectId}...`);
const [version] = await client.accessSecretVersion({
name: `projects/${projectId}/secrets/GEMINI_API_KEY/versions/latest`,
});
const payload = version.payload.data.toString('utf8');
console.log('[Secrets] Successfully fetched GEMINI_API_KEY from GCP.');
return payload;
} catch (err) {
console.warn(`[WARN] Failed to fetch secret GEMINI_API_KEY from GCP. Returning empty string. Error: ${err.message}`);
return '';
}
}
// Fetch secret on startup
fetchGeminiApiKey().then(key => {
geminiApiKeyCached = key;
});
if (fs.existsSync(distFolder)) {
const contents = fs.readdirSync(distFolder);
console.log(`[SERVER] Contents of ${distFolder}:`, contents);
} else {
console.error(`[SERVER] ERROR: ${distFolder} does not exist!`);
console.log(`[SERVER] Contents of ${rootDir}:`, fs.readdirSync(rootDir));
}
// Add security headers
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// PubMed Proxy Endpoints
app.get('/api/pubmed/search', async (req, res) => {
try {
const { term } = req.query;
if (!term) return res.status(400).json({ error: 'Term is required' });
const eSearchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${encodeURIComponent(term)}&retmode=json&retmax=15`;
const response = await fetch(eSearchUrl);
const data = await response.json();
res.json(data);
} catch (err) {
console.error('PubMed Search Proxy Error:', err);
res.status(500).json({ error: err.message });
}
});
app.get('/api/pubmed/summary', async (req, res) => {
try {
const { id } = req.query;
if (!id) return res.status(400).json({ error: 'ID is required' });
const eSummaryUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${id}&retmode=json`;
const response = await fetch(eSummaryUrl);
const data = await response.json();
res.json(data);
} catch (err) {
console.error('PubMed Summary Proxy Error:', err);
res.status(500).json({ error: err.message });
}
});
// Enable parsing JSON bodies for POST requests
app.use(express.json({ limit: '50mb' }));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// JSON File Database Configuration
const dataDir = join(rootDir, 'data');
const patientsDbPath = join(dataDir, 'patients.json');
// Ensure data directory and empty DB exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
if (!fs.existsSync(patientsDbPath)) {
fs.writeFileSync(patientsDbPath, JSON.stringify([], null, 2));
}
// Patients API Endpoints
app.get('/api/patients', (req, res) => {
try {
const data = fs.readFileSync(patientsDbPath, 'utf8');
res.setHeader('Content-Type', 'application/json');
res.send(data);
} catch (err) {
console.error('[API] Error reading patients database:', err);
res.status(500).json({ error: 'Internal server error while reading database' });
}
});
app.post('/api/patients', (req, res) => {
try {
if (!req.body || !Array.isArray(req.body)) {
return res.status(400).json({ error: 'Body must be a JSON array of patients' });
}
// Save exactly what the frontend sends
fs.writeFileSync(patientsDbPath, JSON.stringify(req.body, null, 2));
console.log(`[API] Saved ${req.body.length} patients to database.`);
res.status(200).json({ success: true, count: req.body.length });
} catch (err) {
console.error('[API] Error saving patients database:', err);
res.status(500).json({ error: 'Internal server error while saving database' });
}
});
app.put('/api/patients/:id', (req, res) => {
try {
const { id } = req.params;
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({ error: 'Body must be a JSON object representing the patient' });
}
const data = fs.readFileSync(patientsDbPath, 'utf8');
const patients = JSON.parse(data);
const index = patients.findIndex(p => p.id === id);
if (index !== -1) {
patients[index] = { ...patients[index], ...req.body, id }; // Ensure ID stays same
} else {
// If it doesn't exist, we can create it
patients.push({ ...req.body, id });
}
fs.writeFileSync(patientsDbPath, JSON.stringify(patients, null, 2));
console.log(`[API] Synced patient ${id} from mobile/app to database.`);
res.status(200).json({ success: true, patient: patients.find(p => p.id === id) });
} catch (err) {
console.error('[API] Error syncing patient to database:', err);
res.status(500).json({ error: 'Internal server error while syncing patient' });
}
});
// Serve static files via Express directly to avoid generic filesystem deadlocks
// index: false prevents static middleware from serving index.html on root `/` requests
app.use(express.static(distFolder, { maxAge: '1y', index: false }));
// Fallback to index.html for Angular routing and root requests
app.get(/(.*)/, (req, res) => {
const indexPath = join(distFolder, 'index.html');
// A request is a "document" request if it:
// 1. Is the root '/'
// 2. Is index.html
// 3. Doesn't have a file extension (likely an Angular route)
const isDoc = req.url === '/' || req.url === '/index.html' || !req.url.includes('.');
if (!isDoc) {
// If it's not a doc and wasn't caught by express.static, it's a 404
console.log(`[SERVER] 404 Not Found: ${req.url}`);
return res.status(404).send('Not Found');
}
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
if (fs.existsSync(indexPath)) {
try {
let html = fs.readFileSync(indexPath, 'utf8');
if (geminiApiKeyCached) {
// Inject script immediately before closing </head>
const scriptTag = `<script px-api-key="true">window.GEMINI_API_KEY = "${geminiApiKeyCached}";</script>\n</head>`;
html = html.replace('</head>', scriptTag);
}
res.setHeader('Content-Type', 'text/html');
return res.status(200).send(html);
} catch (err) {
console.error('[SERVER] Error injecting secret into index.html:', err);
}
}
res.sendFile(indexPath);
});
app.listen(port, '0.0.0.0', () => {
console.log(`[SERVER] Listening on port ${port}`);
});