import http from "http"; import { promises as fs } from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = Number(process.env.PORT) || 4173; const distDir = path.resolve(__dirname, "..", "dist"); const emailGenRoot = path.resolve(process.env.EMAIL_GEN_ROOT || path.resolve(__dirname, "..", "..", "email-gen")); const emailGenProject = process.env.EMAIL_GEN_PROJECT || "vipavenue"; const htmlRelPath = process.env.EMAIL_GEN_HTML_PATH || `emails/${emailGenProject}/html.pug`; const lettersRelDir = process.env.EMAIL_GEN_LETTERS_DIR || `emails/${emailGenProject}/letters`; const publicIndexRel = process.env.EMAIL_GEN_PUBLIC_INDEX || "public/index.html"; const allowOrigin = process.env.API_ALLOW_ORIGIN || "*"; const contentTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".ico": "image/x-icon", ".json": "application/json; charset=utf-8", ".map": "application/json; charset=utf-8" }; const json = (res, status, payload) => { res.statusCode = status; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(payload)); }; const text = (res, status, payload) => { res.statusCode = status; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end(payload); }; const safeResolve = (root, rel) => { const target = path.resolve(root, rel); const safeRoot = root.endsWith(path.sep) ? root : root + path.sep; if (!target.startsWith(safeRoot)) { throw new Error("Path escapes root"); } return target; }; const normalizeContentPath = (value = "") => value .replace(/^(\.\/)?letters\//i, "") .replace(/^\/+/, "") .replace(/\.pug$/i, "") .trim(); const readBody = (req) => new Promise((resolve, reject) => { let data = ""; req.on("data", (chunk) => { data += chunk; if (data.length > 5 * 1024 * 1024) { reject(new Error("Payload too large")); req.destroy(); } }); req.on("end", () => resolve(data)); req.on("error", reject); }); const server = http.createServer(async (req, res) => { try { const url = new URL(req.url, `http://${req.headers.host}`); const pathname = decodeURIComponent(url.pathname); if (pathname === "/api/save") { res.setHeader("Access-Control-Allow-Origin", allowOrigin); res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.statusCode = 204; res.end(); return; } if (req.method !== "POST") { text(res, 405, "Method Not Allowed"); return; } const raw = await readBody(req); let payload; try { payload = JSON.parse(raw); } catch (e) { json(res, 400, { ok: false, error: "Invalid JSON" }); return; } const { pugCode, htmlPug, contentPath } = payload || {}; if (typeof pugCode !== "string" || !pugCode.trim()) { json(res, 400, { ok: false, error: "Missing pugCode" }); return; } if (typeof htmlPug !== "string" || !htmlPug.trim()) { json(res, 400, { ok: false, error: "Missing htmlPug" }); return; } if (typeof contentPath !== "string" || !contentPath.trim()) { json(res, 400, { ok: false, error: "Missing contentPath" }); return; } const normalizedPath = normalizeContentPath(contentPath); if (!normalizedPath || normalizedPath.includes("..")) { json(res, 400, { ok: false, error: "Invalid contentPath" }); return; } const letterRelPath = path.posix.join(lettersRelDir, `${normalizedPath}.pug`); const htmlTarget = safeResolve(emailGenRoot, htmlRelPath); const letterTarget = safeResolve(emailGenRoot, letterRelPath); await fs.mkdir(path.dirname(letterTarget), { recursive: true }); await fs.writeFile(letterTarget, pugCode, "utf8"); await fs.writeFile(htmlTarget, htmlPug, "utf8"); json(res, 200, { ok: true, saved: { letter: letterRelPath, html: htmlRelPath } }); return; } if (pathname === "/api/save-html") { res.setHeader("Access-Control-Allow-Origin", allowOrigin); res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.statusCode = 204; res.end(); return; } if (req.method !== "POST") { text(res, 405, "Method Not Allowed"); return; } const raw = await readBody(req); let payload; try { payload = JSON.parse(raw); } catch (e) { json(res, 400, { ok: false, error: "Invalid JSON" }); return; } const { htmlPug } = payload || {}; if (typeof htmlPug !== "string" || !htmlPug.trim()) { json(res, 400, { ok: false, error: "Missing htmlPug" }); return; } const htmlTarget = safeResolve(emailGenRoot, htmlRelPath); await fs.writeFile(htmlTarget, htmlPug, "utf8"); json(res, 200, { ok: true, saved: { html: htmlRelPath } }); return; } if (pathname === "/api/html") { res.setHeader("Access-Control-Allow-Origin", allowOrigin); res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.statusCode = 204; res.end(); return; } if (req.method !== "GET" && req.method !== "HEAD") { text(res, 405, "Method Not Allowed"); return; } const previewPath = safeResolve(emailGenRoot, publicIndexRel); try { const data = await fs.readFile(previewPath); res.statusCode = 200; res.setHeader("Content-Type", "text/html; charset=utf-8"); if (req.method === "HEAD") { res.end(); } else { res.end(data); } } catch (e) { text(res, 404, "Preview not found. Run the email generator."); } return; } if (pathname === "/preview") { if (req.method !== "GET" && req.method !== "HEAD") { text(res, 405, "Method Not Allowed"); return; } const previewPath = safeResolve(emailGenRoot, publicIndexRel); try { const data = await fs.readFile(previewPath); res.statusCode = 200; res.setHeader("Content-Type", "text/html; charset=utf-8"); if (req.method === "HEAD") { res.end(); } else { res.end(data); } } catch (e) { text(res, 404, "Preview not found. Run the email generator."); } return; } if (pathname.startsWith("/api/")) { text(res, 404, "Not Found"); return; } if (req.method !== "GET" && req.method !== "HEAD") { text(res, 405, "Method Not Allowed"); return; } const safePath = path.resolve(distDir, "." + pathname); const safeRoot = distDir.endsWith(path.sep) ? distDir : distDir + path.sep; if (!safePath.startsWith(safeRoot)) { text(res, 403, "Forbidden"); return; } let filePath = safePath; try { const stat = await fs.stat(filePath); if (!stat.isFile()) { throw new Error("Not a file"); } } catch (e) { filePath = path.join(distDir, "index.html"); } const ext = path.extname(filePath).toLowerCase(); const contentType = contentTypes[ext] || "application/octet-stream"; let data; try { data = await fs.readFile(filePath); } catch (e) { text(res, 404, "Not Found"); return; } res.statusCode = 200; res.setHeader("Content-Type", contentType); if (req.method === "HEAD") { res.end(); } else { res.end(data); } } catch (e) { console.error(e); text(res, 500, "Internal Server Error"); } }); server.listen(PORT, () => { console.log(`editor-svelte server running on http://localhost:${PORT}`); console.log(`email-gen root: ${emailGenRoot}`); });