/** * app.js — Single-file leak checker webtool (Node built-in only) * - Serves a simple HTML page at / * - API: GET /api/hibp/breaches?email=you@example.com * * Deploy on Render: * - Build Command: (leave empty) or "echo no build" * - Start Command: node app.js * - Env var: HIBP_API_KEY = * * Notes: * - Uses HIBP official API (requires key) * - Includes a button that opens the Odido checker page (no scraping) */ const http = require("http"); const { URL } = require("url"); const PORT = process.env.PORT || 3000; function isLikelyEmail(email) { return typeof email === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); } function sendJson(res, status, obj) { const body = JSON.stringify(obj); res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", }); res.end(body); } function sendHtml(res, status, html) { res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", }); res.end(html); } const HTML = ` Odido / Datalek Check Tool

Check: (mogelijk) gelekte gegevens

Deze tool checkt je e-mail op bekende datalekken via Have I Been Pwned (officiële API). Voor de Odido-specifieke check opent hij de publieke Odido-checker pagina (geen scraping).

1) HIBP e-mail check


Let op: “niet gevonden” betekent alleen “niet gevonden in bekende/verwerkte lekken”. Zet 2FA aan en gebruik unieke wachtwoorden.

`; async function handleHibp(req, res, urlObj) { const email = (urlObj.searchParams.get("email") || "").trim(); if (!isLikelyEmail(email)) { return sendJson(res, 400, { ok: false, error: "Voer een geldig e-mailadres in." }); } const apiKey = process.env.HIBP_API_KEY; if (!apiKey) { return sendJson(res, 500, { ok: false, error: "Server mist HIBP_API_KEY. Zet deze als environment variable op Render." }); } const hibpUrl = "https://haveibeenpwned.com/api/v3/breachedaccount/" + encodeURIComponent(email) + "?truncateResponse=true"; // Node 18+ has global fetch const hibpResp = await fetch(hibpUrl, { headers: { "hibp-api-key": apiKey, "user-agent": "odido-checker-tool/1.0 (render-deploy)", "accept": "application/json", }, }); if (hibpResp.status === 404) { return sendJson(res, 200, { ok: true, breached: false, breaches: [] }); } if (hibpResp.status === 429) { const retryAfter = hibpResp.headers.get("retry-after"); return sendJson(res, 429, { ok: false, error: "Rate limit geraakt bij HIBP. Probeer later opnieuw.", retryAfterSeconds: retryAfter ? Number(retryAfter) : null, }); } if (!hibpResp.ok) { const text = await hibpResp.text().catch(() => ""); return sendJson(res, hibpResp.status, { ok: false, error: "HIBP API error", status: hibpResp.status, details: text.slice(0, 500), }); } const breaches = await hibpResp.json(); return sendJson(res, 200, { ok: true, breached: true, breaches }); } const server = http.createServer(async (req, res) => { try { const urlObj = new URL(req.url, `http://${req.headers.host || "localhost"}`); const path = urlObj.pathname; if (req.method === "GET" && path === "/") { return sendHtml(res, 200, HTML); } if (req.method === "GET" && path === "/api/hibp/breaches") { return await handleHibp(req, res, urlObj); } // health check if (req.method === "GET" && path === "/healthz") { return sendJson(res, 200, { ok: true }); } return sendJson(res, 404, { ok: false, error: "Not found" }); } catch (err) { return sendJson(res, 500, { ok: false, error: "Server error", details: String(err) }); } }); server.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`); });