Files
VitaChat/lib/db.js
T
2026-07-23 09:58:58 +02:00

121 lines
3.8 KiB
JavaScript

// Gemeinsame Helfer für alle vitaAPI-Endpunkte (Vercel Serverless + Neon Postgres)
import { neon } from '@neondatabase/serverless';
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
export const ONLINE_SECS = 60;
export const MAX_TEXT_LEN = 500;
export const MAX_POLL = 50;
export const PREVIEW_LEN = 38;
/* ---------------- Datenbank-Verbindung (verzögert aufgebaut) ----------------
* Wichtig: NICHT beim Import verbinden. Fehlt die Umgebungsvariable, würde
* neon() sofort werfen und die Function stürzt ab, bevor irgendein try/catch
* greift (Vercel zeigt dann nur FUNCTION_INVOCATION_FAILED). So bekommen wir
* stattdessen eine lesbare ERR|-Zeile.
*/
let _sql = null;
function getSql() {
if (_sql) return _sql;
// Je nach Integration heißt die Variable unterschiedlich.
const url =
process.env.DATABASE_URL ||
process.env.POSTGRES_URL ||
process.env.POSTGRES_PRISMA_URL ||
process.env.DATABASE_URL_UNPOOLED ||
process.env.POSTGRES_URL_NON_POOLING;
if (!url) {
const found = Object.keys(process.env)
.filter(k => /POSTGRES|DATABASE|NEON/i.test(k))
.join(', ') || 'keine';
throw new Error(
`Keine Datenbank-URL gefunden. Gefundene passende Variablen: ${found}. ` +
`Im Vercel-Dashboard unter Storage eine Neon-Datenbank verbinden und neu deployen.`
);
}
_sql = neon(url);
return _sql;
}
/** Als Tagged Template nutzbar: sql`SELECT ...` */
export function sql(strings, ...values) {
return getSql()(strings, ...values);
}
/* ---------------- Antwort-Protokoll (zeilenbasiert, Pipe-getrennt) ---------------- */
export function textHead(res) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
}
export function ok(res, body = '') {
textHead(res);
res.status(200).send(body);
}
export function fail(res, msg) {
textHead(res);
res.status(200).send(`ERR|${msg}\n`); // 200, damit der Vita-Client die Zeile sicher liest
}
/** Pipes und Zeilenumbrüche aus Nutzdaten entfernen, damit das Protokoll heil bleibt. */
export function clean(s) {
return String(s).replace(/\|/g, '¦').replace(/[\r\n]+/g, ' ');
}
/* ---------------- Parameter (GET-Query oder POST-Formulardaten) ---------------- */
export function param(req, key) {
let v = req.query?.[key];
if (v === undefined && req.body) {
if (typeof req.body === 'string') {
v = new URLSearchParams(req.body).get(key);
} else {
v = req.body[key];
}
}
return (v ?? '').toString().trim();
}
/* ---------------- Passwörter (scrypt aus Node, keine Extra-Abhängigkeit) ---------------- */
export function hashPassword(pass) {
const salt = randomBytes(16);
const hash = scryptSync(pass, salt, 64);
return `${salt.toString('hex')}:${hash.toString('hex')}`;
}
export function verifyPassword(pass, stored) {
try {
const [saltHex, hashHex] = String(stored).split(':');
if (!saltHex || !hashHex) return false;
const expected = Buffer.from(hashHex, 'hex');
const actual = scryptSync(pass, Buffer.from(saltHex, 'hex'), expected.length);
return timingSafeEqual(expected, actual);
} catch {
return false;
}
}
export function newToken() {
return randomBytes(16).toString('hex');
}
/* ---------------- Authentifizierung ---------------- */
/** Gibt den Nutzer zurück oder null (dann hat der Aufrufer bereits geantwortet). */
export async function auth(req, res) {
const token = param(req, 'token');
if (!token) { fail(res, 'Kein Token'); return null; }
const rows = await sql`SELECT id, name FROM users WHERE token = ${token}`;
if (!rows.length) { fail(res, 'Ungueltiges Token'); return null; }
const now = Math.floor(Date.now() / 1000);
await sql`UPDATE users SET last_seen = ${now} WHERE id = ${rows[0].id}`;
return rows[0];
}