NODEDC_SITE/tools/admin-server.mjs

152 lines
4.3 KiB
JavaScript

import { createServer } from "node:http";
import { readFile, stat, writeFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import { dirname } from "node:path";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const port = Number(process.env.PORT || 8090);
const host = process.env.HOST || "127.0.0.1";
const pagePath = join(repoRoot, "content", "pages", "home.json");
const MIME = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".avif": "image/avif",
".webp": "image/webp",
".mp4": "video/mp4",
".glb": "model/gltf-binary",
};
function send(res, status, body, contentType = "text/plain; charset=utf-8") {
res.writeHead(status, {
"content-type": contentType,
"cache-control": "no-store",
});
res.end(body);
}
function sendJson(res, status, payload) {
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function runNodeScript(scriptName) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [join(repoRoot, "tools", scriptName)], {
cwd: repoRoot,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(new Error(stderr || stdout || `${scriptName} exited with ${code}`));
}
});
});
}
function safePublicPath(urlPath) {
const withoutQuery = decodeURIComponent(urlPath.split("?")[0]);
const requestPath =
withoutQuery === "/"
? "/index.html"
: withoutQuery === "/admin" || withoutQuery === "/admin/"
? "/admin/index.html"
: withoutQuery;
const normalized = normalize(requestPath).replace(/^(\.\.[/\\])+/, "");
const absolute = join(repoRoot, normalized);
if (!absolute.startsWith(repoRoot)) {
return null;
}
return absolute;
}
async function serveStatic(req, res) {
const filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname);
if (!filePath) {
send(res, 403, "Forbidden");
return;
}
try {
const fileStat = await stat(filePath);
if (!fileStat.isFile()) {
send(res, 404, "Not found");
return;
}
const ext = extname(filePath);
send(res, 200, await readFile(filePath), MIME[ext] || "application/octet-stream");
} catch {
send(res, 404, "Not found");
}
}
const server = createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${host}:${port}`);
if (req.method === "GET" && url.pathname === "/api/page/home") {
send(res, 200, await readFile(pagePath, "utf8"), "application/json; charset=utf-8");
return;
}
if (req.method === "PUT" && url.pathname === "/api/page/home") {
const body = await readBody(req);
const parsed = JSON.parse(body);
await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`);
sendJson(res, 200, { ok: true });
return;
}
if (req.method === "POST" && url.pathname === "/api/render/home") {
const result = await runNodeScript("render-home.mjs");
sendJson(res, 200, { ok: true, ...result });
return;
}
if (req.method === "POST" && url.pathname === "/api/extract/home") {
const result = await runNodeScript("extract-home-blocks.mjs");
sendJson(res, 200, { ok: true, ...result });
return;
}
await serveStatic(req, res);
} catch (error) {
sendJson(res, 500, { ok: false, error: error.message });
}
});
server.listen(port, host, () => {
console.log(`NODE.DC admin: http://${host}:${port}/admin/`);
console.log(`NODE.DC site: http://${host}:${port}/`);
});