const express = require("express"); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const db = require("./db"); require("dotenv").config(); function log(level, message, meta = {}) { const timestamp = new Date().toISOString(); console.log(JSON.stringify({ timestamp, level, service: "auth_api", message, ...meta })); } const app = express(); app.use(express.json()); // Simple CORS middleware for browser-based admin UI testing. app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (req.method === "OPTIONS") { return res.sendStatus(204); } next(); }); const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || "https://error.xtransit.testinstance.nl/error_log"; function friendlyErrorMessage(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", "x-internal-api-key": process.env.INTERNAL_API_KEY || "", }, body: JSON.stringify({ service: "auth_api", partofservice, error, }), }); } catch (_) {} } function reportUnhandled(context, err) { const message = err?.message || String(err); const stack = err?.stack || message; log("error", context, { error: message, stack }); void reportError({ partofservice: context, error: stack }); } process.on("uncaughtException", (err) => reportUnhandled("uncaughtException", err)); process.on("unhandledRejection", (reason) => reportUnhandled("unhandledRejection", reason)); 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] .filter((v) => v !== undefined && v !== null && v !== "") .join(" - ") || `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: friendlyErrorMessage(res.statusCode) }); } return originalJson(body); }; next(); }); const JWT_SECRET = process.env.JWT_SECRET; const ADMIN_USER_CODES = new Set( String(process.env.ADMIN_USER_CODES || "") .split(",") .map((c) => Number(c.trim())) .filter(Number.isFinite), ); app.post("/login", async (req, res) => { try { const { userCode, password } = req.body; if (!userCode || !password) return res.status(400).json({ error: "userCode and password required" }); const result = await db.query( "SELECT id, usercode, password FROM workers.users WHERE usercode = $1", [userCode], ); const user = result.rows[0]; if (!user) { log("warn", "Login attempt for non-existent user", { userCode }); return res.status(401).json({ error: "invalid_credentials" }); } const ok = await bcrypt.compare(password, user.password); if (!ok) { log("warn", "Invalid password for user", { userCode }); return res.status(401).json({ error: "invalid_credentials" }); } const isAdmin = ADMIN_USER_CODES.has(Number(user.usercode)); const payload = { userId: user.id, userCode: user.usercode, isAdmin: Boolean(isAdmin), }; const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" }); log("info", "User logged in", { userCode: user.usercode, isAdmin }); res.json({ token, user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin: Boolean(isAdmin), }, }); } catch (err) { log("error", "Login error", { error: err.message, stack: err.stack }); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); res.status(500).json({ error: friendlyErrorMessage(500) }); } }); app.get("/me", async (req, res) => { 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: "missing_token" }); } const token = auth.slice(7); try { const payload = jwt.verify(token, JWT_SECRET); res.json({ user: payload }); } catch (err) { log("error", "JWT verification failed", { error: err.message, stack: err.stack }); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); res.status(401).json({ error: friendlyErrorMessage(401) }); } }); // Development helper removed for production safety. // The /dev/token endpoint was intentionally removed to prevent accidental // issuance of tokens in production builds. Use the real authentication flow // (POST /login) or re-enable this code only in local development branches. const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Auth API listening on ${PORT}`)); app.get("/health", async (_req, res) => { try { await db.query("SELECT 1"); res.json({ status: "ok", service: "auth_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: "auth_api" }); } }); module.exports = app;