-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
122 lines (109 loc) · 4.3 KB
/
Copy pathserver.js
File metadata and controls
122 lines (109 loc) · 4.3 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
// FStea AI Cockpit — zero-dependency HTTP server.
// Serves the static SPA from public/ and dispatches /api/* to the router.
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { URL } from 'url';
import { route } from './lib/api.js';
import { load } from './lib/db.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PUBLIC = path.join(__dirname, 'public');
const MEDIA = path.join(__dirname, 'data', 'images'); // runtime-generated drink designs
const PORT = process.env.PORT || 4178;
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.csv': 'text/csv; charset=utf-8',
'.map': 'application/json'
};
function sendJson(res, status, obj) {
const body = JSON.stringify(obj);
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body) });
res.end(body);
}
function readBody(req) {
return new Promise((resolve) => {
let data = '';
req.on('data', (c) => { data += c; if (data.length > 8e6) req.destroy(); });
req.on('end', () => {
if (!data) return resolve({});
try { resolve(JSON.parse(data)); } catch { resolve({ _raw: data }); }
});
req.on('error', () => resolve({}));
});
}
// Prevent path traversal; resolve a request path to a file inside PUBLIC.
function safeFile(pathname) {
const clean = decodeURIComponent(pathname.split('?')[0]);
const rel = clean === '/' ? 'index.html' : clean.replace(/^\/+/, '');
const abs = path.normalize(path.join(PUBLIC, rel));
if (!abs.startsWith(PUBLIC)) return null;
return abs;
}
function serveStatic(res, pathname) {
let file = safeFile(pathname);
if (!file) { res.writeHead(403); return res.end('Forbidden'); }
fs.stat(file, (err, stat) => {
if (err || !stat.isFile()) {
// SPA fallback: serve index.html for unknown non-asset routes
if (!path.extname(pathname)) {
file = path.join(PUBLIC, 'index.html');
} else {
res.writeHead(404); return res.end('Not found');
}
}
const ext = path.extname(file).toLowerCase();
const type = MIME[ext] || 'application/octet-stream';
fs.readFile(file, (e, buf) => {
if (e) { res.writeHead(500); return res.end('Server error'); }
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-cache' });
res.end(buf);
});
});
}
const server = http.createServer(async (req, res) => {
const u = new URL(req.url, 'http://localhost');
const pathname = u.pathname;
// serve runtime-generated drink-design images from data/images
if (pathname.startsWith('/media/')) {
const name = path.basename(decodeURIComponent(pathname.slice('/media/'.length)));
const file = path.join(MEDIA, name);
if (!file.startsWith(MEDIA) || !fs.existsSync(file)) { res.writeHead(404); return res.end('Not found'); }
const ext = path.extname(file).toLowerCase();
fs.readFile(file, (e, buf) => {
if (e) { res.writeHead(500); return res.end('error'); }
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'public, max-age=86400' });
res.end(buf);
});
return;
}
if (pathname.startsWith('/api/')) {
const query = Object.fromEntries(u.searchParams.entries());
const body = ['POST', 'PATCH', 'PUT', 'DELETE'].includes(req.method) ? await readBody(req) : {};
let result;
try {
result = await route(req.method, pathname, query, body);
} catch (err) {
result = { status: 500, json: { error: String(err && err.message || err) } };
}
if (!result) result = { status: 404, json: { error: 'Not found' } };
return sendJson(res, result.status, result.json);
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405); return res.end('Method not allowed');
}
serveStatic(res, pathname);
});
// Initialize store (seeds on first run) before listening.
load();
server.listen(PORT, () => {
console.log(`\n 🍵 FStea AI Cockpit running at http://localhost:${PORT}\n Data dir: ./data | API: /api/* | Stop: Ctrl-C\n`);
});