calorie-ai-android/web/server.js
2026-05-19 17:13:41 +02:00

80 lines
2.7 KiB
JavaScript

const http = require('http');
const fs = require('fs');
const path = require('path');
const port = Number(process.env.PORT || 8080);
const publicDir = path.join(__dirname, 'public');
const publicRoot = publicDir + path.sep;
const types = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
};
function readBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk;
if (body.length > 12 * 1024 * 1024) {
req.destroy();
reject(new Error('Request body is too large'));
}
});
req.on('end', () => resolve(body));
req.on('error', reject);
});
}
function send(res, status, body, contentType = 'application/json; charset=utf-8', headOnly = false) {
res.writeHead(status, {
'content-type': contentType,
'cache-control': 'no-store',
});
res.end(headOnly ? '' : body);
}
function serveStatic(req, res) {
const cleanUrl = decodeURIComponent(req.url.split('?')[0]);
const file = cleanUrl === '/' ? '/index.html' : cleanUrl;
const target = path.normalize(path.join(publicDir, file));
if (!target.startsWith(publicRoot)) return send(res, 403, 'Forbidden', 'text/plain; charset=utf-8');
fs.readFile(target, (err, data) => {
if (err) return send(res, 404, 'Not found', 'text/plain; charset=utf-8');
send(res, 200, data, types[path.extname(target)] || 'application/octet-stream', req.method === 'HEAD');
});
}
async function proxyChat(req, res) {
try {
const payload = JSON.parse(await readBody(req));
const baseUrl = String(payload.baseUrl || '').replace(/\/+$/, '');
if (!/^https?:\/\//.test(baseUrl)) throw new Error('A valid API base URL is required');
if (!payload.body || typeof payload.body !== 'object') throw new Error('Missing chat completion body');
const headers = { 'content-type': 'application/json' };
if (payload.apiKey) headers.authorization = `Bearer ${payload.apiKey}`;
const upstream = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(payload.body),
});
const text = await upstream.text();
send(res, upstream.status, text);
} catch (error) {
send(res, 400, JSON.stringify({ error: error.message }));
}
}
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/api/chat') return proxyChat(req, res);
if (req.method === 'GET' || req.method === 'HEAD') return serveStatic(req, res);
send(res, 405, 'Method not allowed', 'text/plain; charset=utf-8');
}).listen(port, () => {
console.log(`Calorie AI web listening on ${port}`);
});