136 lines
5.6 KiB
JavaScript
136 lines
5.6 KiB
JavaScript
const http = require("http");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const DATA = path.join(__dirname, "data.json");
|
||
const OWNER_PIN = process.env.OWNER_PIN || "drakon2027";
|
||
|
||
function load() {
|
||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); }
|
||
catch (_) { return { regs: [], wishes: [], log: [] }; }
|
||
}
|
||
function save(d) {
|
||
fs.writeFileSync(DATA, JSON.stringify(d, null, 2));
|
||
}
|
||
function body(req) {
|
||
return new Promise((resolve) => {
|
||
let s = "";
|
||
req.on("data", (c) => (s += c));
|
||
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
|
||
});
|
||
}
|
||
function json(res, code, obj) {
|
||
res.writeHead(code, { "Content-Type": "application/json" });
|
||
res.end(JSON.stringify(obj));
|
||
}
|
||
function totalGuests(d) {
|
||
return d.regs.reduce((s, r) => s + (parseInt(r.guests) || 1), 0);
|
||
}
|
||
function pinOk(b) { return b && b.pin === OWNER_PIN; }
|
||
|
||
http.createServer(async (req, res) => {
|
||
const url = req.url.split("?")[0];
|
||
|
||
// Публичный: зарегистрироваться на праздник
|
||
if (req.method === "POST" && url === "/api/regs") {
|
||
const b = await body(req);
|
||
const d = load();
|
||
if (!b.name || !b.phone) return json(res, 400, { ok: false, error: "Укажи имя и телефон" });
|
||
const rec = {
|
||
id: Date.now(),
|
||
name: String(b.name).slice(0, 100),
|
||
phone: String(b.phone).slice(0, 30),
|
||
guests: Math.max(1, parseInt(b.guests) || 1),
|
||
note: String(b.note || "").slice(0, 300),
|
||
date: new Date().toISOString().slice(0, 10)
|
||
};
|
||
d.regs.push(rec);
|
||
d.log.push({ when: new Date().toISOString(), what: "Регистрация: " + rec.name });
|
||
save(d);
|
||
return json(res, 200, { ok: true, total: totalGuests(d) });
|
||
}
|
||
|
||
// Публичный: список подарков
|
||
if (req.method === "GET" && url === "/api/wish") {
|
||
return json(res, 200, { ok: true, data: load().wishes });
|
||
}
|
||
|
||
// Публичный: забронировать подарок
|
||
if (req.method === "POST" && url === "/api/wish/reserve") {
|
||
const b = await body(req);
|
||
const d = load();
|
||
const w = d.wishes.find((x) => x.id === b.id);
|
||
if (!w) return json(res, 404, { ok: false, error: "Подарок не найден" });
|
||
if (w.reservedBy) return json(res, 400, { ok: false, error: "Подарок уже забронирован" });
|
||
if (!b.guest) return json(res, 400, { ok: false, error: "Напиши, кто дарит" });
|
||
w.reservedBy = String(b.guest).slice(0, 100);
|
||
d.log.push({ when: new Date().toISOString(), what: "Подарок «" + w.title + "» забронирован: " + w.reservedBy });
|
||
save(d);
|
||
return json(res, 200, { ok: true, data: d.wishes });
|
||
}
|
||
|
||
// Публичный: снять бронь (на случай ошибки гостя)
|
||
if (req.method === "POST" && url === "/api/wish/unreserve") {
|
||
const b = await body(req);
|
||
const d = load();
|
||
const w = d.wishes.find((x) => x.id === b.id);
|
||
if (!w) return json(res, 404, { ok: false, error: "Подарок не найден" });
|
||
if (!w.reservedBy) return json(res, 400, { ok: false, error: "Брони нет" });
|
||
const who = w.reservedBy;
|
||
w.reservedBy = "";
|
||
d.log.push({ when: new Date().toISOString(), what: "Бронь снята с «" + w.title + "» (" + who + ")" });
|
||
save(d);
|
||
return json(res, 200, { ok: true, data: d.wishes });
|
||
}
|
||
|
||
// Админ: список гостей
|
||
if (req.method === "POST" && url === "/api/admin/regs") {
|
||
const b = await body(req);
|
||
if (!pinOk(b)) return json(res, 403, { ok: false, error: "Неверный код" });
|
||
const d = load();
|
||
return json(res, 200, { ok: true, data: d.regs, total: totalGuests(d) });
|
||
}
|
||
|
||
// Админ: добавить подарок
|
||
if (req.method === "POST" && url === "/api/admin/wish") {
|
||
const b = await body(req);
|
||
if (!pinOk(b)) return json(res, 403, { ok: false, error: "Неверный код" });
|
||
const d = load();
|
||
if (!b.title) return json(res, 400, { ok: false, error: "Название обязательно" });
|
||
d.wishes.push({
|
||
id: Date.now(),
|
||
title: String(b.title).slice(0, 100),
|
||
emoji: String(b.emoji || "🎁").slice(0, 4),
|
||
reservedBy: ""
|
||
});
|
||
d.log.push({ when: new Date().toISOString(), what: "Добавлен подарок: " + b.title });
|
||
save(d);
|
||
return json(res, 200, { ok: true, data: d.wishes });
|
||
}
|
||
|
||
// Админ: удалить подарок
|
||
if (req.method === "POST" && url === "/api/admin/wish/del") {
|
||
const b = await body(req);
|
||
if (!pinOk(b)) return json(res, 403, { ok: false, error: "Неверный код" });
|
||
const d = load();
|
||
const w = d.wishes.find((x) => x.id === b.id);
|
||
d.wishes = d.wishes.filter((x) => x.id !== b.id);
|
||
if (w) d.log.push({ when: new Date().toISOString(), what: "Удалён подарок: " + w.title });
|
||
save(d);
|
||
return json(res, 200, { ok: true, data: d.wishes });
|
||
}
|
||
|
||
// Статика: index.html / style.css / script.js / data.json
|
||
let file = url === "/" ? "/index.html" : url;
|
||
const full = path.join(__dirname, file);
|
||
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||
const ext = path.extname(full);
|
||
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : ext === ".json" ? "application/json" : "text/html";
|
||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||
return res.end(fs.readFileSync(full));
|
||
}
|
||
|
||
json(res, 404, { ok: false, error: "Not found" });
|
||
}).listen(PORT, () => console.log("Сервер на порту " + PORT));
|