const express = require("express"); const QRCode = require("qrcode"); const app = express(); const PORT = process.env.PORT || 3000; const TRUST_PROXY = process.env.TRUST_PROXY !== "false"; const ALLOWED_QR_IPS = new Set( (process.env.ALLOWED_QR_IPS || "") .split(",") .map((ip) => ip.trim()) .filter(Boolean) .map((ip) => normalizeIp(ip)) ); app.set("trust proxy", TRUST_PROXY); app.use(express.json({ limit: "100kb" })); function normalizeIp(ip) { if (!ip || typeof ip !== "string") { return ""; } const trimmed = ip.trim(); const withoutMappedV4Prefix = trimmed.startsWith("::ffff:") ? trimmed.slice(7) : trimmed; const ipv4WithPortMatch = withoutMappedV4Prefix.match(/^(\d+\.\d+\.\d+\.\d+):(\d+)$/); if (ipv4WithPortMatch) { return ipv4WithPortMatch[1]; } return withoutMappedV4Prefix; } function getClientIp(req) { const forwardedFor = req.headers["x-forwarded-for"]; if (typeof forwardedFor === "string" && forwardedFor.trim()) { return normalizeIp(forwardedFor.split(",")[0]); } return normalizeIp(req.ip || req.socket?.remoteAddress || ""); } function enforceQrIpAllowlist(req, res, next) { if (ALLOWED_QR_IPS.size === 0) { return next(); } const clientIp = getClientIp(req); if (ALLOWED_QR_IPS.has(clientIp)) { return next(); } return res.status(403).json({ error: "Access denied for this IP address" }); } function parseIntWithBounds(value, fallback, min, max) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed)) { return fallback; } return Math.min(max, Math.max(min, parsed)); } function normalizePayload(source) { const payload = source || {}; const text = typeof payload.text === "string" ? payload.text.trim() : ""; if (!text) { return { error: "Missing required parameter: text", }; } return { text, width: parseIntWithBounds(payload.size, 512, 128, 2048), margin: parseIntWithBounds(payload.margin, 2, 0, 10), dark: typeof payload.dark === "string" && payload.dark ? payload.dark : "#000000", light: typeof payload.light === "string" && payload.light ? payload.light : "#FFFFFF", }; } async function sendQrPng(source, res, next) { const normalized = normalizePayload(source); if (normalized.error) { return res.status(400).json({ error: normalized.error }); } try { const image = await QRCode.toBuffer(normalized.text, { type: "png", width: normalized.width, margin: normalized.margin, color: { dark: normalized.dark, light: normalized.light, }, }); res.setHeader("Content-Type", "image/png"); res.setHeader("Cache-Control", "no-store"); return res.send(image); } catch (error) { return next(error); } } app.get("/", (_req, res) => { res.json({ service: "qr-code-api", usage: "GET /api/qr?text=hello or POST /api/qr with JSON body { text }", }); }); app.get("/health", (_req, res) => { res.json({ status: "ok" }); }); app.get("/api/qr", enforceQrIpAllowlist, (req, res, next) => sendQrPng(req.query, res, next)); app.post("/api/qr", enforceQrIpAllowlist, (req, res, next) => sendQrPng(req.body, res, next)); app.use((err, _req, res, _next) => { console.error(err); res.status(500).json({ error: "Failed to generate QR code" }); }); app.listen(PORT, () => { console.log(`QR API listening on port ${PORT}`); });