-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
165 lines (150 loc) · 6.1 KB
/
Copy pathserver.js
File metadata and controls
165 lines (150 loc) · 6.1 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
require('dotenv').config();
const express = require('express');
const path = require('path');
const personalRoutes = require('./routes/personal');
const jobsRoutes = require('./routes/jobs');
const educationRoutes = require('./routes/education');
const projectsRoutes = require('./routes/projects');
const skillsRoutes = require('./routes/skills');
const certificationsRoutes = require('./routes/certifications');
const awardsRoutes = require('./routes/awards');
const resumesRoutes = require('./routes/resumes');
const settingsRoutes = require('./routes/settings');
const aiRoutes = require('./routes/ai');
const app = express();
const PORT = Number(process.env.PORT) || 3000;
app.disable('x-powered-by');
app.use(express.json({ limit: '512kb' }));
app.use(express.static(path.join(__dirname, 'public'), {
extensions: ['html'],
maxAge: '1h',
}));
// Request logger. Quiet under tests so node:test output stays readable.
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'test') return next();
const start = Date.now();
res.on('finish', () => {
console.log(`[req] ${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
});
app.use('/api/personal', personalRoutes);
app.use('/api/jobs', jobsRoutes);
app.use('/api/education', educationRoutes);
app.use('/api/projects', projectsRoutes);
app.use('/api/skills', skillsRoutes);
app.use('/api/certifications', certificationsRoutes);
app.use('/api/awards', awardsRoutes);
app.use('/api/resumes', resumesRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/ai', aiRoutes);
app.get('/api/health', (_req, res) => res.json({ ok: true, time: new Date().toISOString() }));
app.use('/api', (req, res) => {
res.status(404).json({ error: `Unknown API route: ${req.method} ${req.originalUrl}` });
});
const errorHandler = (err, req, res, _next) => {
// Tests already see the response body, so don't double-log to stderr.
if (process.env.NODE_ENV !== 'test') {
console.error(JSON.stringify({
level: 'error',
ts: new Date().toISOString(),
method: req.method,
path: req.originalUrl,
status: err.status || 500,
message: err.message,
stack: err.stack,
}));
}
const status = err.status || 500;
// 5xx messages can leak temp paths, SDK URLs, or SQL text. Replace with
// a generic string. <500 errors come from our own validators and are safe.
const message = status >= 500
? 'Internal server error'
: (err.message || 'Bad request');
res.status(status).json({ error: message });
};
app.use(errorHandler);
// Only auto-listen when run directly. tests/http.test.js requires this
// file and owns the listen() call itself.
if (require.main === module) {
// Print what's wired up so the user knows what to expect before clicking.
const reportStartupStatus = () => {
try {
const { detectEngine } = require('./services/latex');
const engine = detectEngine();
if (engine) console.log(`[latex] engine on PATH: ${engine.cmd}. PDF export ready.`);
else console.log('[latex] no engine on PATH. PDF export returns 501 until you install tectonic, pdflatex, xelatex, or lualatex (see README).');
} catch (_) { /* non-fatal */ }
try {
const db = require('./db/database');
const row = db.prepare("SELECT value FROM settings WHERE key = 'gemini_api_key'").get();
const hasKey = (row && row.value) || !!process.env.GEMINI_API_KEY;
if (hasKey) console.log('[ai] Gemini API key configured. AI review ready.');
else console.log('[ai] no Gemini API key. AI review is disabled until you add one in Settings or set GEMINI_API_KEY.');
} catch (_) { /* settings table may not exist yet on first run */ }
};
// Open the user's default browser. Skipped when OPEN=0 (headless / SSH).
// Fails silently when no opener is on PATH.
const openBrowser = (url) => {
if (process.env.OPEN === '0') return;
const cmd = process.platform === 'darwin' ? 'open'
: process.platform === 'win32' ? 'start'
: 'xdg-open';
const { spawn } = require('child_process');
try {
// 'start' is a shell builtin on Windows.
const child = spawn(cmd, [url], {
detached: true, stdio: 'ignore',
shell: process.platform === 'win32',
});
child.unref();
child.on('error', () => { /* no opener on this system */ });
} catch (_) { /* fail silently */ }
};
// 127.0.0.1 only. Node's default 0.0.0.0 would expose the no-auth API to
// the LAN (CWE-1327, OWASP A07).
const server = app.listen(PORT, '127.0.0.1', () => {
const url = `http://localhost:${PORT}`;
console.log(`Walrus listening on ${url}`);
reportStartupStatus();
openBrowser(url);
});
// Without these handlers, an unhandled rejection terminates the process
// with no log line (Node 15+ default).
process.on('unhandledRejection', (reason) => {
console.error('[unhandledRejection]', reason);
});
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err);
// Run shutdown so in-flight requests can drain.
shutdown('uncaughtException');
});
// SIGTERM/SIGINT: stop accepting new requests, drain, close the DB.
// 10s timer is a hard fallback if something hangs.
let shuttingDown = false;
const shutdown = (signal) => {
if (shuttingDown) return;
shuttingDown = true;
console.log(`[shutdown] received ${signal}; closing server`);
const force = setTimeout(() => {
console.error('[shutdown] forced exit after 10s timeout');
process.exit(1);
}, 10_000).unref();
server.close((err) => {
clearTimeout(force);
if (err) console.error('[shutdown] server.close error', err);
try {
const db = require('./db/database');
if (typeof db.close === 'function') db.close();
} catch (closeErr) {
console.error('[shutdown] db.close error', closeErr);
}
process.exit(err ? 1 : 0);
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
module.exports = app;
// http.test.js mounts this on a tiny test app to verify the real handler.
module.exports.errorHandler = errorHandler;