privet/server.js

702 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
const http = require("http");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const PORT = process.env.PORT || 3000;
const ROOT = __dirname;
const DATA_FILE = path.join(ROOT, "data.json");
const UPLOADS_DIR = path.join(ROOT, "uploads");
const store = require("./lib/store");
const alem = require("./lib/alem");
const SECRET = process.env.KFU_SECRET || crypto.randomBytes(16).toString("hex");
const PUBLIC_BASE = process.env.KFU_PUBLIC_BASE || "";
const SESSION_TTL_MS = 1000 * 60 * 60 * 12;
const MAX_UPLOAD = 50 * 1024 * 1024;
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
const DB = store.load();
let GENERATED_PASSWORD = DB._consolidatorTempPassword || null;
if (GENERATED_PASSWORD) {
log("FIRST RUN: consolidator login=svod password=" + GENERATED_PASSWORD);
delete DB._consolidatorTempPassword;
store.save(DB);
}
if (DB._firstRunSubsidiaryPasswords) {
log("FIRST RUN: subsidiary logins/passwords:\n" + DB._firstRunSubsidiaryPasswords);
delete DB._firstRunSubsidiaryPasswords;
store.save(DB);
}
const sessions = {};
function nowMs() { return Date.now(); }
function iso() { return new Date().toISOString(); }
function log() {
const args = Array.prototype.slice.call(arguments);
args.unshift("[KFU " + new Date().toISOString() + "]");
console.log.apply(null, args);
}
function readBody(req) {
return new Promise(function (resolve) {
let s = "";
req.on("data", function (c) { s += c; });
req.on("end", function () {
try { resolve(JSON.parse(s || "{}")); } catch (e) { resolve({}); }
});
});
}
function sendJson(res, code, obj) {
const body = JSON.stringify(obj);
res.writeHead(code, {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
});
res.end(body);
}
function makeToken() {
return crypto.randomBytes(24).toString("hex");
}
function createUserSession(res, user) {
const token = makeToken();
sessions[token] = { userId: user.id, exp: nowMs() + SESSION_TTL_MS };
res.setHeader("Set-Cookie", "kfu_session=" + token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=" + Math.floor(SESSION_TTL_MS / 1000));
}
function currentUser(req) {
const cookies = parseCookies(req);
const token = cookies.kfu_session;
if (!token) return null;
const s = sessions[token];
if (!s || s.exp < nowMs()) return null;
return DB.users.find(function (u) { return u.id === s.userId && u.active; }) || null;
}
function parseCookies(req) {
const out = {};
const raw = req.headers.cookie;
if (!raw) return out;
raw.split(";").forEach(function (pair) {
const i = pair.indexOf("=");
if (i === -1) return;
out[pair.slice(0, i).trim()] = decodeURIComponent(pair.slice(i + 1).trim());
});
return out;
}
// ---------- auth ----------
function findUserByLogin(login) {
return DB.users.find(function (u) { return u.login === login; }) || null;
}
function handleLogin(req, res) {
readBody(req).then(function (b) {
const user = findUserByLogin(String(b.login || "").trim());
if (!user || !user.active) {
return sendJson(res, 404, { error: "Неверный логин или пароль" });
}
if (user.lockedUntil && user.lockedUntil > nowMs()) {
const waitSec = Math.ceil((user.lockedUntil - nowMs()) / 1000);
return sendJson(res, 423, { error: "Слишком много попыток. Повторите через " + waitSec + " с" });
}
let ok = false;
try { ok = store.verifyPassword(String(b.password || ""), user.salt, user.passwordHash); } catch (e) {}
if (!ok) {
user.failedCount = (user.failedCount || 0) + 1;
if (user.failedCount >= 5) {
user.lockedUntil = nowMs() + 5 * 60 * 1000;
user.failedCount = 0;
store.save(DB);
return sendJson(res, 423, { error: "Слишком много попыток. Повторите через 5 минут" });
}
store.save(DB);
return sendJson(res, 401, { error: "Неверный логин или пароль" });
}
user.failedCount = 0;
user.lockedUntil = 0;
store.save(DB);
createUserSession(res, user);
const out = { ok: true, role: user.role };
if (user.role === "subsidiary") out.companyName = user.companyName;
sendJson(res, 200, out);
});
}
function handleLogout(req, res) {
const cookies = parseCookies(req);
const token = cookies.kfu_session;
if (token) delete sessions[token];
res.setHeader("Set-Cookie", "kfu_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
sendJson(res, 200, { ok: true });
}
// ---------- uploads ----------
function statusFromReport(r) {
if (!r) return "not_submitted";
if (r.status === "processing") return "processing";
if (r.status === "error") return "error";
if (r.status === "done" && r.result) return "done";
return "not_submitted";
}
function latestReportFor(company) {
let best = null;
DB.reports.forEach(function (r) {
if (r.companyName !== company) return;
if (!best || (r.uploadedAt || "") > (best.uploadedAt || "")) best = r;
});
return best;
}
function saveUpload(req, destPath, cb) {
const chunks = [];
let total = 0;
let aborted = false;
req.on("data", function (ch) {
if (aborted) return;
total += ch.length;
if (total > MAX_UPLOAD) {
aborted = true;
req.destroy();
return cb(new Error("FILE_TOO_LARGE"));
}
chunks.push(ch);
});
req.on("end", function () {
if (aborted) return;
try {
fs.writeFileSync(destPath, Buffer.concat(chunks));
cb(null, total);
} catch (e) {
cb(e);
}
});
req.on("error", function (e) { if (!aborted) { aborted = true; cb(e); } });
}
function sendNotification(event, company, period, status) {
const url = process.env.NOTIFY_URL;
if (!url) return;
const secret = process.env.KFU_NOTIFY_SECRET;
const headers = { "Content-Type": "application/json" };
if (secret) headers["X-KFU-Secret"] = secret;
fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify({ event: event, company: company, period: period, status: status }),
}).catch(function () { });
}
function computeFromText(user, report, fileText) {
report.status = "processing";
store.save(DB);
alem.callKfuAgent(user.companyName, DB.period, fileText).then(function (result) {
report.status = "done";
report.result = result;
report.error = null;
report.computedAt = iso();
store.save(DB);
sendNotification("report_uploaded", user.companyName, DB.period, "done");
}).catch(function (err) {
report.status = "error";
report.error = String((err && err.message) || err).slice(0, 500);
report.computedAt = iso();
store.save(DB);
sendNotification("report_uploaded", user.companyName, DB.period, "error");
});
}
// ---------- API handlers ----------
function handleMe(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
const out = {
role: user.role,
login: user.login,
companyName: user.companyName || null,
person: user.person || null,
period: DB.period,
deadline: DB.deadline,
};
if (user.role === "subsidiary") {
const r = latestReportFor(user.companyName);
const st = statusFromReport(r);
out.lastStatus = st;
if (r) out.lastUploadedAt = r.uploadedAt;
}
sendJson(res, 200, out);
}
function handleUpload(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "subsidiary") return sendJson(res, 403, { error: "Доступно только дочерним компаниям" });
const comment = String(req.headers["x-comment"] || "").slice(0, 2000);
const reportId = store.genId("r");
const destPath = path.join(UPLOADS_DIR, reportId + ".bin");
let uploadName = String(req.headers["x-filename"] || "");
try { uploadName = decodeURIComponent(uploadName); } catch (e) {}
if (!uploadName) uploadName = "file";
saveUpload(req, destPath, function (err, size) {
if (err) {
if (err.message === "FILE_TOO_LARGE") return sendJson(res, 413, { error: "Файл больше 50 МБ" });
return sendJson(res, 400, { error: "Не удалось прочитать файл" });
}
const fileExt = path.extname(uploadName).toLowerCase();
const isPdf = fileExt === ".pdf";
const isXl = fileExt === ".xlsx" || fileExt === ".xlsm";
if (!isPdf && !isXl) {
fs.unlinkSync(destPath);
return sendJson(res, 415, { error: "Поддерживаются только xlsx, xlsm, pdf" });
}
const report = {
id: reportId,
companyName: user.companyName,
userId: user.id,
period: DB.period,
filename: uploadName,
ext: fileExt,
size: size,
comment: comment,
uploadedAt: iso(),
status: "processing",
result: null,
error: null,
computedAt: null,
file: path.join("uploads", reportId + ".bin"),
};
DB.reports.push(report);
store.save(DB);
log("upload", user.companyName, uploadName, size, "bytes");
sendJson(res, 200, { ok: true, uploadedAt: report.uploadedAt });
// background: convert to text then call agent
fileToText(destPath, isPdf).then(function (text) {
computeFromText(user, report, text);
}).catch(function (e) {
report.status = "error";
report.error = "Не удалось разобрать файл: " + String((e && e.message) || e).slice(0, 300);
report.computedAt = iso();
store.save(DB);
});
});
}
function fileToText(filePath, isPdf) {
if (isPdf) {
return new Promise(function (resolve, reject) {
const pdfParse = require("pdf-parse");
pdfParse(fs.readFileSync(filePath)).then(function (data) {
resolve(String(data.text || "").slice(0, 200000));
}).catch(reject);
});
}
return new Promise(function (resolve, reject) {
const XLSX = require("xlsx");
const wb = XLSX.readFile(filePath, { type: "file" });
const parts = [];
wb.SheetNames.forEach(function (name) {
const ws = wb.Sheets[name];
const csv = XLSX.utils.sheet_to_csv(ws, { blankrows: false,FS: "\t",RS: "\n" });
// filter empty rows
const lines = csv.split("\n").map(function (l) { return l.trim(); }).filter(function (l) { return l.length > 0; });
if (lines.length) parts.push("=== ЛИСТ: " + name + " ===\n" + lines.join("\n"));
});
resolve(parts.join("\n\n").slice(0, 200000));
});
}
function dashboardRows() {
return DB.users.filter(function (u) { return u.role === "subsidiary"; }).map(function (u) {
const r = latestReportFor(u.companyName);
const st = statusFromReport(r);
const row = {
company: u.companyName,
status: st,
uploadedAt: r ? r.uploadedAt : null,
person: u.person || "",
phone: u.phone || "",
submitted: st === "done",
};
if (r) {
row.comment = r.comment || "";
row.filename = r.filename;
row.fileId = r.id;
}
if (r && r.result) {
const res = r.result;
row.debt = res.debt;
row.ebitda_ltm = res.ebitda_ltm;
row.interest_ltm = res.interest_ltm;
row.equity = res.equity;
row.units = res.units;
row.k1 = res.k1;
row.k2 = res.k2;
row.k3 = res.k3;
row.zone_k1 = res.zone_k1;
row.zone_k2 = res.zone_k2;
row.zone_k3 = res.zone_k3;
row.zone_company = res.zone_company;
row.flags = res.flags || [];
row.sources = res.sources || {};
row.agent_comment = res.comment || "";
}
return row;
});
}
function handleDashboard(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Доступно только сводящему" });
const rows = dashboardRows();
const submitted = rows.filter(function (r) { return r.submitted; }).length;
sendJson(res, 200, {
period: DB.period,
deadline: DB.deadline,
consolidatorPhone: DB.consolidatorPhone,
total: rows.length,
submitted: submitted,
rows: rows,
});
}
function handleFile(req, res, id) {
const user = currentUser(req);
if (!user) { res.writeHead(401); return res.end(); }
const report = DB.reports.find(function (r) { return r.id === id; });
if (!report) { res.writeHead(404); return res.end("Нет такого файла"); }
if (user.role !== "consolidator") { res.writeHead(404); return res.end("Нет такого файла"); }
const fp = path.join(UPLOADS_DIR, id + ".bin");
if (!fs.existsSync(fp)) { res.writeHead(404); return res.end("Файл удалён"); }
const name = (report.filename || "file").replace(/[^\w.\-а-яА-ЯёЁ ]/g, "_");
res.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Disposition": 'attachment; filename="' + name + '"',
});
fs.createReadStream(fp).pipe(res);
}
function handleRecalc(req, res, id) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
const report = DB.reports.find(function (r) { return r.id === id; });
if (!report) return sendJson(res, 404, { error: "Нет такого отчёта" });
const companyUser = DB.users.find(function (u) { return u.companyName === report.companyName; });
const fp = path.join(UPLOADS_DIR, report.id + ".bin");
if (!fs.existsSync(fp)) return sendJson(res, 410, { error: "Файл не найден" });
report.status = "processing";
report.error = null;
store.save(DB);
const isPdf = report.ext === ".pdf";
fileToText(fp, isPdf).then(function (text) {
computeFromText(companyUser || { companyName: report.companyName }, report, text);
}).catch(function (e) {
report.status = "error";
report.error = "Не удалось разобрать файл: " + String((e && e.message) || e).slice(0, 300);
report.computedAt = iso();
store.save(DB);
});
sendJson(res, 200, { ok: true, status: "processing" });
}
function handleExport(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
const XLSX = require("xlsx");
const rows = dashboardRows();
const cols = [
["company", "Компания"],
["status", "Статус"],
["uploadedAt", "Когда загружен"],
["comment", "Комментарий дочки"],
["debt", "Долг"],
["ebitda_ltm", "EBITDA 12М"],
["interest_ltm", "Процентные расходы 12М"],
["equity", "Капитал (млрд тенге)"],
["k1", "К1 Долг/EBITDA"],
["k2", "К2 EBITDA/Проценты"],
["k3", "К3 Долг/Капитал"],
["zone_company", "Зона компании"],
["agent_comment", "Замечания агента"],
];
const aoa = [[]];
cols.forEach(function (c) { aoa[0].push(c[1]); });
rows.forEach(function (r) {
const line = [];
cols.forEach(function (c) {
let v = r[c[0]];
if (v === null || v === undefined) v = "";
if (c[0] === "status") v = { done: "сдан", processing: "обрабатывается", error: "ошибка расчёта", not_submitted: "не сдан" }[v] || v;
line.push(v);
});
aoa.push(line);
});
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), "Свод");
const comm = [["Компания", "Ответственный", "Телефон", "Период", "Комментарий"]];
rows.forEach(function (r) {
comm.push([r.company, r.person || "", r.phone || "", DB.period, r.comment || ""]);
});
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(comm), "Комментарии");
const buf = XLSX.write(wb, { type: "buffer", bookType: "xlsx" });
res.writeHead(200, {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": 'attachment; filename="kfu-svod-' + DB.period.replace(/[^\wа-яА-ЯёЁ]/g, "").replace(/\s+/g, "-") + '.xlsx"',
});
res.end(buf);
}
function handleSettingsGet(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
const companies = DB.users.filter(function (u) { return u.role === "subsidiary"; }).map(function (u) {
return {
id: u.id,
name: u.companyName,
person: u.person || "",
phone: u.phone || "",
login: u.login,
active: u.active,
};
});
sendJson(res, 200, {
period: DB.period,
deadline: DB.deadline,
consolidatorPhone: DB.consolidatorPhone,
consolidatorLogin: user.login,
companies: companies,
});
}
function handleSettingsSave(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
readBody(req).then(function (b) {
if (b.period) DB.period = String(b.period).slice(0, 100);
if (b.deadline) DB.deadline = String(b.deadline).slice(0, 20);
if (typeof b.consolidatorPhone === "string") DB.consolidatorPhone = b.consolidatorPhone.slice(0, 30);
store.save(DB);
const companies = Array.isArray(b.companies) ? b.companies : null;
if (companies) {
const wantLogins = {};
companies.forEach(function (c) {
const u = DB.users.find(function (x) { return x.id === c.id; });
if (u) {
u.companyName = String(c.name || u.companyName).slice(0, 200);
u.person = String(c.person || "").slice(0, 100);
u.phone = String(c.phone || "").slice(0, 30);
u.active = c.active !== false;
if (c.login && c.login !== u.login) {
const taken = DB.users.some(function (other) { return other.id !== u.id && other.login === c.login; });
if (!taken) u.login = String(c.login).slice(0, 50);
}
}
wantLogins[u ? u.login : (c.login || "")] = true;
});
// delete companies not in list
DB.users = DB.users.filter(function (u) {
if (u.role !== "subsidiary") return true;
return companies.some(function (c) { return c.id === u.id; });
});
store.save(DB);
}
sendJson(res, 200, { ok: true });
});
}
function handleAddCompany(req, res) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
readBody(req).then(function (b) {
const name = String(b.name || "").trim();
if (name.length < 3) return sendJson(res, 400, { error: "Укажите название компании" });
const exists = DB.users.some(function (u) { return u.role === "subsidiary" && u.companyName === name; });
if (exists) return sendJson(res, 409, { error: "Компания уже есть в списке" });
let login = String(b.login || "").trim() || generateLogin();
if (DB.users.some(function (u) { return u.login === login; })) {
return sendJson(res, 409, { error: "Логин уже занят" });
}
const u = store.makeUser({
id: store.genId("u"),
role: "subsidiary",
name: "",
companyName: name,
person: String(b.person || "").slice(0, 100),
phone: String(b.phone || "").slice(0, 30),
login: login,
});
const tempPw = store.randPassword(10);
const salt = crypto.randomBytes(32);
u.salt = salt.toString("hex");
u.passwordHash = crypto.scryptSync(tempPw, salt, 32).toString("hex");
DB.users.push(u);
store.save(DB);
sendJson(res, 200, { ok: true, login: u.login, tempPassword: tempPw });
});
}
function generateLogin() {
return "c" + crypto.randomBytes(3).toString("hex");
}
function handleUserAction(req, res, userId) {
const user = currentUser(req);
if (!user) return sendJson(res, 401, { error: "Требуется вход" });
if (user.role !== "consolidator") return sendJson(res, 403, { error: "Нет доступа" });
const target = DB.users.find(function (u) { return u.id === userId; });
if (!target || target.role !== "subsidiary") return sendJson(res, 404, { error: "Запись не найдена" });
readBody(req).then(function (b) {
if (b.action === "toggle") {
target.active = !target.active;
store.save(DB);
sendJson(res, 200, { ok: true, active: target.active });
} else if (b.action === "set_password") {
const pw = b.password && String(b.password).length >= 4 ? String(b.password) : store.randPassword(10);
const salt = crypto.randomBytes(32);
target.salt = salt.toString("hex");
target.passwordHash = crypto.scryptSync(pw, salt, 32).toString("hex");
target.lockedUntil = 0;
target.failedCount = 0;
store.save(DB);
sendJson(res, 200, { ok: true, tempPassword: pw });
} else {
sendJson(res, 400, { error: "Неизвестное действие" });
}
});
}
function handleConsolidatorPassword(req, res) {
const user = currentUser(req);
if (!user || user.role !== "consolidator") return sendJson(res, 401, { error: "Требуется вход" });
readBody(req).then(function (b) {
const cur = String(b.current || "");
if (!store.verifyPassword(cur, user.salt, user.passwordHash)) {
return sendJson(res, 401, { error: "Текущий пароль неверен" });
}
const nw = String(b.next || "");
if (nw.length < 4) return sendJson(res, 400, { error: "Новый пароль — минимум 4 символа" });
const salt = crypto.randomBytes(32);
user.salt = salt.toString("hex");
user.passwordHash = crypto.scryptSync(nw, salt, 32).toString("hex");
store.save(DB);
sendJson(res, 200, { ok: true });
});
}
// ---------- external service APIs (X-KFU-Secret) ----------
function checkXKfuSecret(req, res) {
const key = req.headers["x-kfu-secret"];
if (key !== SECRET) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "bad secret" }));
return false;
}
return true;
}
function handleRecipients(req, res) {
const rows = dashboardRows();
sendJson(res, 200, {
period: DB.period,
deadline: DB.deadline,
upload_link: PUBLIC_BASE ? PUBLIC_BASE.replace(/\/$/, "") + "/" : "",
consolidator: {
phone: DB.consolidatorPhone || "",
dashboard_link: PUBLIC_BASE ? PUBLIC_BASE.replace(/\/$/, "") + "/dashboard" : "",
},
companies: rows.map(function (r) {
return { company: r.company, person: r.person || "", phone: r.phone || "", submitted: r.submitted };
}),
});
}
function handleStatusApi(req, res) {
const rows = dashboardRows();
sendJson(res, 200, {
period: DB.period,
deadline: DB.deadline,
total: rows.length,
submitted: rows.filter(function (r) { return r.submitted; }).length,
rows: rows,
});
}
// ---------- static ----------
function serveStatic(req, res, urlPath) {
let file = urlPath === "/" ? "/index.html" : urlPath;
if (path.extname(file) === "") {
if (fs.existsSync(path.join(ROOT, file + ".html"))) file = file + ".html";
else file = "/index.html";
}
const full = path.join(ROOT, file.replace(/\//g, path.sep));
if (!full.startsWith(ROOT)) { res.writeHead(403); return res.end(); }
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) {
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
return res.end("<h1>404</h1>");
}
const ext = path.extname(full).toLowerCase();
const types = {
".html": "text/html", ".css": "text/css", ".js": "application/javascript",
".json": "application/json", ".png": "image/png", ".svg": "image/svg+xml",
".woff2": "font/woff2", ".woff": "font/woff", ".ico": "image/x-icon",
};
res.writeHead(200, { "Content-Type": (types[ext] || "application/octet-stream") + "; charset=utf-8" });
fs.createReadStream(full).pipe(res);
}
// ---------- router ----------
http.createServer(function (req, res) {
const urlPath = (req.url || "/").split("?")[0];
try {
if (req.method === "POST" && urlPath === "/api/login") return handleLogin(req, res);
if (req.method === "POST" && urlPath === "/api/logout") return handleLogout(req, res);
if (req.method === "GET" && urlPath === "/api/me") return handleMe(req, res);
if (req.method === "POST" && urlPath === "/api/upload") return handleUpload(req, res);
if (req.method === "GET" && urlPath === "/api/dashboard") return handleDashboard(req, res);
if (req.method === "GET" && urlPath === "/api/export") return handleExport(req, res);
if (req.method === "GET" && urlPath === "/api/settings") return handleSettingsGet(req, res);
if (req.method === "POST" && urlPath === "/api/settings") return handleSettingsSave(req, res);
if (req.method === "POST" && urlPath === "/api/add-company") return handleAddCompany(req, res);
if (req.method === "POST" && urlPath === "/api/recalc") {
const q = (req.url.split("?")[1] || "");
return handleRecalc(req, res, decodeURIComponent((q.replace(/^id=/, "") || "").toUpperCase()));
}
const userMatch = urlPath.match(/^\/api\/user\/([\w]+)/);
if (req.method === "POST" && userMatch) return handleUserAction(req, res, userMatch[1]);
if (req.method === "POST" && urlPath === "/api/consolidator-password") return handleConsolidatorPassword(req, res);
if (urlPath.indexOf("/api/file/") === 0) {
const id = urlPath.slice("/api/file/".length).toUpperCase();
return handleFile(req, res, decodeURIComponent(id));
}
if (urlPath === "/api/recipients") {
if (!checkXKfuSecret(req, res)) return;
return handleRecipients(req, res);
}
if (urlPath === "/api/status") {
if (!checkXKfuSecret(req, res)) return;
return handleStatusApi(req, res);
}
if (urlPath.indexOf("/api/") === 0) { res.writeHead(404, { "Content-Type": "application/json" }); return res.end("{}"); }
return serveStatic(req, res, urlPath);
} catch (e) {
log("handler error", req.method, urlPath, String((e && e.message) || e));
try { sendJson(res, 500, { error: "Внутренняя ошибка сервера" }); } catch (_) {}
}
}).listen(PORT, function () {
log("KFU server on port " + PORT);
});