const express = require("express"); const cors = require("cors"); const jwt = require("jsonwebtoken"); require("dotenv").config(); const db = require("./db"); function log(level, message, meta = {}) { const timestamp = new Date().toISOString(); console.log(JSON.stringify({ timestamp, level, service: "error_log_api", message, ...meta })); } async function ensureErrorLogTable() { try { await db.query(`CREATE SCHEMA IF NOT EXISTS data`); await db.query(` CREATE TABLE IF NOT EXISTS data.errors ( id BIGSERIAL PRIMARY KEY, "date" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "service" TEXT NOT NULL, partofservice TEXT, error TEXT NOT NULL ) `); console.log("Error log table ready"); } catch (err) { console.error("Failed to create error log table:", err); } } const app = express(); app.use(express.json({ limit: "64kb" })); app.use(cors()); const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || "https://error.xtransit.testinstance.nl/error_log"; function toFriendlyError(statusCode) { if (statusCode === 400) return "Controleer je invoer en probeer opnieuw."; if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in."; if (statusCode === 403) return "Je hebt hier geen toegang toe."; if (statusCode === 404) return "Niet gevonden."; return "Er ging iets mis. Probeer het later opnieuw."; } async function reportError({ partofservice, error }) { try { await fetch(ERROR_LOG_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ service: "error_log_api", partofservice, error, }), }); } catch (_) {} } app.use((req, res, next) => { const originalJson = res.json.bind(res); res.json = (body) => { if (res.statusCode >= 400 && body && typeof body === "object") { const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`; log("warn", `HTTP ${res.statusCode}`, { path: req.originalUrl, method: req.method, error: rawError }); void reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: rawError, }); return originalJson({ error: toFriendlyError(res.statusCode) }); } return originalJson(body); }; next(); }); const PORT = Number(process.env.PORT || 5003); function normalizeErrorText(value) { if (!value) return "Unknown error"; if (typeof value === "string") return value; try { return JSON.stringify(value); } catch (_) { return String(value); } } async function insertErrorLog({ service, partofservice, partOfService, error, }) { const serviceName = String(service || "unknown_service").slice(0, 120); const part = partofservice || partOfService ? String(partofservice || partOfService).slice(0, 240) : null; const errorText = String(normalizeErrorText(error)).slice(0, 8000); await db.query( "INSERT INTO data.errors (service, partofservice, error) VALUES ($1, $2, $3)", [serviceName, part, errorText], ); } function verifyAdmin(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith("Bearer ")) { log("warn", "Missing or invalid Authorization header", { path: req.originalUrl }); return res.status(401).json({ error: toFriendlyError(401) }); } try { const payload = jwt.verify(auth.slice(7), JWT_SECRET); if (!payload?.isAdmin) { log("warn", "Non-admin user attempted admin access", { userCode: payload?.userCode, path: req.originalUrl }); return res.status(403).json({ error: toFriendlyError(403) }); } req.user = payload; return next(); } catch (err) { log("error", "JWT verification failed", { error: err.message, path: req.originalUrl }); reportError({ partofservice: `verifyAdmin ${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return res.status(401).json({ error: toFriendlyError(401) }); } } app.get("/health", async (_req, res) => { try { await db.query("SELECT 1"); res.json({ status: "ok", service: "error_log_api", database: "ok" }); } catch (err) { log("error", "Health check DB query failed", { error: err.message, stack: err.stack }); reportError({ partofservice: "GET /health", error: err.stack || err.message }); res.status(503).json({ status: "unavailable", service: "error_log_api" }); } }); app.post("/error_log", async (req, res) => { try { const service = req.body?.service; const partofservice = req.body?.partofservice || req.body?.partOfService || null; const error = req.body?.error; if (!service || !error) { log("warn", "Missing required fields in error_log", { body: req.body }); return res.status(400).json({ error: toFriendlyError(400) }); } await insertErrorLog({ service, partofservice, error }); return res.status(201).json({ ok: true }); } catch (err) { log("error", "POST /error_log failed", { error: err.message, stack: err.stack }); reportError({ partofservice: "POST /error_log", error: err.stack || err.message }); return res.status(500).json({ error: toFriendlyError(500) }); } }); app.get("/error_log", verifyAdmin, async (req, res) => { try { const limit = Math.min(Number(req.query.limit || 100), 500); const service = req.query.service ? String(req.query.service) : null; const partofservice = req.query.partofservice || req.query.partOfService ? String(req.query.partofservice || req.query.partOfService) : null; const where = []; const params = []; if (service) { params.push(service); where.push(`"service" = $${params.length}`); } if (partofservice) { params.push(partofservice); where.push(`partofservice ILIKE $${params.length}`); params[params.length - 1] = `%${partofservice}%`; } params.push(limit); const rows = await db.query( `SELECT id, date, service, partofservice, error FROM data.errors ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY "date" DESC, id DESC LIMIT $${params.length}`, params, ); return res.json({ errors: rows.rows }); } catch (err) { log("error", "GET /error_log failed", { error: err.message, stack: err.stack }); reportError({ partofservice: "GET /error_log", error: err.stack || err.message }); return res.status(500).json({ error: toFriendlyError(500) }); } }); app.use(async (err, req, res, _next) => { log("error", "Unhandled error", { error: err?.message, stack: err?.stack, path: req.originalUrl }); try { await insertErrorLog({ service: "error_log_api", partofservice: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err), }); } catch (logErr) { log("error", "Failed to log internal error", { error: logErr.message }); } if (res.headersSent) return; return res.status(500).json({ error: toFriendlyError(500) }); }); app.listen(PORT, async () => { await ensureErrorLogTable(); console.log(`Error Log API listening on ${PORT}`); }); module.exports = app;