Опубликовано через кнопку
This commit is contained in:
parent
09e179107c
commit
6286f1826b
@ -224,7 +224,7 @@
|
||||
}
|
||||
var idx = ROW_IDX++;
|
||||
ROW_DATA[idx] = r;
|
||||
var hasDetail = !!(r.comment || (r.flags && r.flags.length) || r.agent_comment);
|
||||
var hasDetail = !!(r.comment || (r.flags && r.flags.length) || r.agent_comment || r.error);
|
||||
var actsStr = acts.length ? acts.join(" ") : "";
|
||||
return "<tr>" +
|
||||
"<td class='company company-cell' title='" + esc(r.company) + "'>" + esc(r.company) +
|
||||
@ -247,6 +247,7 @@
|
||||
(r.comment ? "<div class='kfu-cell'><span class='lbl'>Комментарий дочки:</span> " + esc(r.comment) + "</div>" : "") +
|
||||
(r.agent_comment ? "<div class='kfu-cell'><span class='lbl'>Замечания агента:</span> " + esc(r.agent_comment) + "</div>" : "") +
|
||||
((r.flags && r.flags.length) ? "<div class='kfu-cell'><span class='lbl'>Признаки:</span><br>" + r.flags.map(esc).join("<br>") + "</div>" : "") +
|
||||
(r.error ? "<div class='kfu-cell' style='color:var(--kt-ai-danger)'><span class='lbl'>Ошибка расчёта:</span> " + esc(r.error) + "</div>" : "") +
|
||||
"</td></tr>" : "");
|
||||
}).join("");
|
||||
tbody.innerHTML = html;
|
||||
|
||||
35
lib/alem.js
35
lib/alem.js
@ -52,7 +52,7 @@ function fetchWithTimeout(url, options, timeoutMs) {
|
||||
|
||||
|
||||
function createAlemSession() {
|
||||
const url = process.env.ALEM_BASE_URL + "/api/v1/open-api/conversation";
|
||||
const url = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, "") + "/api/v1/open-api/conversation";
|
||||
return fetchWithTimeout(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -93,7 +93,7 @@ function extractSseText(raw) {
|
||||
}
|
||||
|
||||
function askAlem(sessionId, message) {
|
||||
var url = process.env.ALEM_BASE_URL + sendPath(sessionId);
|
||||
var url = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, "") + sendPath(sessionId);
|
||||
return fetchWithTimeout(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -102,10 +102,32 @@ function askAlem(sessionId, message) {
|
||||
},
|
||||
body: JSON.stringify({ content: message, user_id: 'kfu_app', model_api_key: process.env.ALEM_API_KEY })
|
||||
}, ALEM_TIMEOUT_MS).then(function (resp) {
|
||||
if (!resp.ok) { const e = new Error("Alem send HTTP " + resp.status + (resp.statusText ? " " + resp.statusText : "")); e.code = "ALEM_HTTP_" + resp.status; throw e; }
|
||||
return resp.text().then(function (raw) {
|
||||
return extractSseText(raw);
|
||||
if (!resp.ok) {
|
||||
const e = new Error("Alem send HTTP " + resp.status + (resp.statusText ? " " + resp.statusText : ""));
|
||||
e.code = "ALEM_HTTP_" + resp.status;
|
||||
throw e;
|
||||
}
|
||||
const reader = resp.body.getReader();
|
||||
const dec = new TextDecoder();
|
||||
let raw = "";
|
||||
let finished = false;
|
||||
function settle(fn) {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
try { reader.cancel(); } catch (e) { }
|
||||
fn(extractSseText(raw));
|
||||
}
|
||||
}
|
||||
function pump() {
|
||||
reader.read().then(function (r) {
|
||||
if (r.done) { settle(resolve); return; }
|
||||
raw += dec.decode(r.value, { stream: true });
|
||||
pump();
|
||||
}).catch(function (err) {
|
||||
if (!finished) { finished = true; reject(err.code === "ALEM_TIMEOUT" ? err : new Error("Alem stream read error: " + String((err && err.message) || err))); }
|
||||
});
|
||||
}
|
||||
return new Promise(function (resolve, reject) { pump(); });
|
||||
});
|
||||
}
|
||||
function sendPath(id) {
|
||||
@ -146,7 +168,8 @@ function callKfuAgent(company, period, fileText) {
|
||||
'Числа — числом (без слов и валютного знака), zone_* — одно из: зелёная, жёлтая, красная, без оценки.',
|
||||
'flags — массив строк (пустой, если замечаний нет), sources — объект-источник цифр, comment — краткое замечание (или пустая строка).'
|
||||
].join("\n");
|
||||
if (!process.env.ALEM_BASE_URL || !process.env.ALEM_API_KEY || !process.env.ALEM_TEMPLATE_ID) {
|
||||
const base = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, "");
|
||||
if (!base || !process.env.ALEM_API_KEY || !process.env.ALEM_TEMPLATE_ID) {
|
||||
const e = new Error("Агент Alem не настроен: отсутствуют ALEM_BASE_URL / ALEM_API_KEY / ALEM_TEMPLATE_ID.");
|
||||
e.code = "ALEM_NOT_CONFIGURED";
|
||||
return Promise.reject(e);
|
||||
|
||||
13
lib/store.js
13
lib/store.js
@ -4,8 +4,13 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const DATA_FILE = path.join(__dirname, "..", "data.json");
|
||||
const UPLOADS_DIR = path.join(__dirname, "..", "uploads");
|
||||
let DATA_FILE = path.join(__dirname, "..", "data.json");
|
||||
let UPLOADS_DIR = path.join(__dirname, "..", "uploads");
|
||||
|
||||
function configure(dataFile, uploadsDir) {
|
||||
DATA_FILE = dataFile;
|
||||
UPLOADS_DIR = uploadsDir;
|
||||
}
|
||||
|
||||
const DEFAULT_DEADLINE = "2026-10-15";
|
||||
|
||||
@ -77,6 +82,7 @@ function seed() {
|
||||
consolidatorPhone: "",
|
||||
users: [],
|
||||
reports: [],
|
||||
sessions: {},
|
||||
};
|
||||
const consPw = process.env.KFU_SVOD_PASSWORD || "12345678";
|
||||
const cons = makeUser({
|
||||
@ -86,7 +92,6 @@ function seed() {
|
||||
companyName: "",
|
||||
login: "svod",
|
||||
_seedPassword: consPw,
|
||||
_keepTemp: true,
|
||||
});
|
||||
db._consolidatorTempPassword = consPw;
|
||||
db.users.push(cons);
|
||||
@ -130,6 +135,7 @@ function load() {
|
||||
if (typeof u.lockedUntil !== "number") u.lockedUntil = 0;
|
||||
if (typeof u.active !== "boolean") u.active = true;
|
||||
});
|
||||
if (!db.sessions || typeof db.sessions !== "object") db.sessions = {};
|
||||
return db;
|
||||
}
|
||||
|
||||
@ -155,6 +161,7 @@ function verifyPassword(password, saltHex, expectedHex) {
|
||||
module.exports = {
|
||||
DATA_FILE: DATA_FILE,
|
||||
UPLOADS_DIR: UPLOADS_DIR,
|
||||
configure: configure,
|
||||
ensureUploadsDir: ensureUploadsDir,
|
||||
randPassword: randPassword,
|
||||
makeUser: makeUser,
|
||||
|
||||
185
server.js
185
server.js
@ -7,11 +7,38 @@ const crypto = require("crypto");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const ROOT = __dirname;
|
||||
let DATA_DIR = path.join(ROOT, "..", ".kfu-data");
|
||||
let DATA_FILE = path.join(DATA_DIR, "data.json");
|
||||
let UPLOADS_DIR = path.join(DATA_DIR, "uploads");
|
||||
|
||||
function resolveDataDir() {
|
||||
const candidates = [
|
||||
process.env.KFU_DATA_DIR,
|
||||
path.join(ROOT, "..", ".kfu-data"),
|
||||
path.join(ROOT, ".kfu-data"),
|
||||
].filter(Boolean);
|
||||
for (const dir of candidates) {
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const probe = path.join(dir, ".writable-test");
|
||||
fs.writeFileSync(probe, "1");
|
||||
fs.unlinkSync(probe);
|
||||
return dir;
|
||||
} catch (e) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const EXTERNAL_DATA_DIR = resolveDataDir();
|
||||
let DATA_DIR = EXTERNAL_DATA_DIR || path.join(ROOT, "data");
|
||||
let UPLOADS_DIR = path.join(DATA_DIR, "uploads");
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
if (!EXTERNAL_DATA_DIR) {
|
||||
console.warn("[KFU] NOTE: data dir outside project not writable, using in-project " + DATA_DIR + " (protected by 404 allowlist)");
|
||||
}
|
||||
|
||||
// store.js must point at the same files
|
||||
const store = require("./lib/store");
|
||||
store.configure(path.join(DATA_DIR, "data.json"), UPLOADS_DIR);
|
||||
|
||||
const alem = require("./lib/alem");
|
||||
|
||||
const SECRET = process.env.KFU_SECRET || crypto.randomBytes(16).toString("hex");
|
||||
@ -19,8 +46,6 @@ const PUBLIC_BASE = process.env.KFU_PUBLIC_BASE || "";
|
||||
const SESSION_TTL_MS = 1000 * 60 * 60 * 12;
|
||||
const MAX_UPLOAD = 50 * 1024 * 1024;
|
||||
|
||||
try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(UPLOADS_DIR, { recursive: true }); }
|
||||
catch (e) { DATA_DIR = ROOT; UPLOADS_DIR = path.join(ROOT, "uploads"); fs.mkdirSync(UPLOADS_DIR, { recursive: true }); }
|
||||
|
||||
const DB = store.load();
|
||||
let GENERATED_PASSWORD = DB._consolidatorTempPassword || null;
|
||||
@ -36,7 +61,8 @@ if (DB._firstRunSubsidiaryPasswords) {
|
||||
store.save(DB);
|
||||
}
|
||||
|
||||
const sessions = {};
|
||||
// sessions are persisted in DB.sessions = { token: { userId, exp } }
|
||||
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
function iso() { return new Date().toISOString(); }
|
||||
@ -65,16 +91,29 @@ function sendJson(res, code, obj) {
|
||||
function makeToken() {
|
||||
return crypto.randomBytes(24).toString("hex");
|
||||
}
|
||||
function pruneSessions() {
|
||||
if (!DB.sessions) DB.sessions = {};
|
||||
const now = nowMs();
|
||||
let changed = false;
|
||||
Object.keys(DB.sessions).forEach(function (tok) {
|
||||
if (DB.sessions[tok].exp < now) { delete DB.sessions[tok]; changed = true; }
|
||||
});
|
||||
if (changed) store.save(DB);
|
||||
}
|
||||
|
||||
function createUserSession(res, user) {
|
||||
if (!DB.sessions) DB.sessions = {};
|
||||
pruneSessions();
|
||||
const token = makeToken();
|
||||
sessions[token] = { userId: user.id, exp: nowMs() + SESSION_TTL_MS };
|
||||
DB.sessions[token] = { userId: user.id, exp: nowMs() + SESSION_TTL_MS };
|
||||
store.save(DB);
|
||||
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];
|
||||
const s = DB.sessions ? DB.sessions[token] : null;
|
||||
if (!s || s.exp < nowMs()) return null;
|
||||
return DB.users.find(function (u) { return u.id === s.userId && u.active; }) || null;
|
||||
}
|
||||
@ -132,7 +171,7 @@ function handleLogin(req, res) {
|
||||
function handleLogout(req, res) {
|
||||
const cookies = parseCookies(req);
|
||||
const token = cookies.kfu_session;
|
||||
if (token) delete sessions[token];
|
||||
if (token && DB.sessions) { delete DB.sessions[token]; store.save(DB); }
|
||||
res.setHeader("Set-Cookie", "kfu_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
||||
sendJson(res, 200, { ok: true });
|
||||
}
|
||||
@ -198,6 +237,7 @@ function sendNotification(event, company, period, status) {
|
||||
function computeFromText(user, report, fileText) {
|
||||
report.status = "processing";
|
||||
store.save(DB);
|
||||
log("alem call start", user.companyName, "ALEM_BASE_URL=" + (process.env.ALEM_BASE_URL || "<not set>"), "ALEM_API_KEY=" + (process.env.ALEM_API_KEY ? "<set>" : "<not set>"), "ALEM_TEMPLATE_ID=" + (process.env.ALEM_TEMPLATE_ID || "<not set>"));
|
||||
alem.callKfuAgent(user.companyName, DB.period, fileText).then(function (result) {
|
||||
report.status = "done";
|
||||
report.result = result;
|
||||
@ -221,7 +261,7 @@ function describeAlemError(err) {
|
||||
if (code === "ALEM_TIMEOUT") return "Агент не ответил за 10 минут. " + msg;
|
||||
if (code === "ALEM_NOT_CONFIGURED") return "Агент Alem не настроен.";
|
||||
if (code === "ALEM_NOT_JSON") return msg;
|
||||
var m = msg.match(/^.*?HTTP (\d\d\d)/);
|
||||
var m = msg.match(/HTTP (\d\d\d)/);
|
||||
if (m) return "Ошибка HTTP " + m[1] + " от сервера Alem.";
|
||||
return "Ошибка агента: " + msg;
|
||||
}
|
||||
@ -364,6 +404,7 @@ function dashboardRows() {
|
||||
row.sources = res.sources || {};
|
||||
row.agent_comment = res.comment || "";
|
||||
}
|
||||
if (r && r.error) row.error = r.error;
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@ -392,10 +433,14 @@ function handleFile(req, res, id) {
|
||||
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, "_");
|
||||
const safe = (report.filename || "file").replace(/[^\w.\-а-яА-ЯёЁ ]/g, "_").slice(0, 150);
|
||||
const ascii = safe.replace(/[^\x20-\x7E]/g, "_");
|
||||
const dispName = ascii || "file";
|
||||
let disp = 'attachment; filename="' + dispName + '"';
|
||||
try { disp += "; filename*=UTF-8''" + encodeURIComponent(safe); } catch (e) {}
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": 'attachment; filename="' + name + '"',
|
||||
"Content-Disposition": disp,
|
||||
});
|
||||
fs.createReadStream(fp).pipe(res);
|
||||
}
|
||||
@ -452,13 +497,16 @@ function handleExport(req, res) {
|
||||
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;
|
||||
const ST = { done: "сдан", processing: "обрабатывается", error: "ошибка расчёта", not_submitted: "не сдан" };
|
||||
if (c[0] === "status") v = ST[v] || v;
|
||||
line.push(v);
|
||||
});
|
||||
aoa.push(line);
|
||||
});
|
||||
const sheet = XLSX.utils.aoa_to_sheet(aoa);
|
||||
sheet["!cols"] = cols.map(function (c) { return { wch: Math.min(40, Math.max(10, c[1].length + 4)) }; });
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), "Свод");
|
||||
XLSX.utils.book_append_sheet(wb, sheet, "Свод");
|
||||
const comm = [["Компания", "Ответственный", "Телефон", "Период", "Комментарий"]];
|
||||
rows.forEach(function (r) {
|
||||
comm.push([r.company, r.person || "", r.phone || "", DB.period, r.comment || ""]);
|
||||
@ -467,7 +515,7 @@ function handleExport(req, res) {
|
||||
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"',
|
||||
"Content-Disposition": 'attachment; filename="kfu-svod.xlsx"',
|
||||
});
|
||||
res.end(buf);
|
||||
}
|
||||
@ -654,12 +702,31 @@ function handleStatusApi(req, res) {
|
||||
}
|
||||
|
||||
// ---------- 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";
|
||||
// only UI html pages and design-system/ are public; everything else is 404
|
||||
const INTERNAL_NAMES = {
|
||||
"/server.js": true, "/data.json": true, "/package.json": true, "/package-lock.json": true,
|
||||
"/lib": true, "/node_modules": true, "/.git": true, "/data": true, "/.kfu-data": true,
|
||||
"/uploads": true, "/AGENTS.md": true, "/README.md": true, "/design.md": true,
|
||||
"/run": true, "/index": false, "/upload": false, "/dashboard": false,
|
||||
};
|
||||
function isServeable(urlPath) {
|
||||
if (!urlPath || typeof urlPath !== "string") return false;
|
||||
if (urlPath === "/" || urlPath === "/index.html" || urlPath === "/upload.html" || urlPath === "/dashboard.html") return true;
|
||||
if (urlPath.indexOf("/design-system/") === 0) return true;
|
||||
if (INTERNAL_NAMES[urlPath] || INTERNAL_NAMES[path.dirname(urlPath)]) return false;
|
||||
// unknown file: 404 unless it is an existing html file in ROOT (future pages)
|
||||
const norm = path.normalize(urlPath);
|
||||
const full = path.join(ROOT, norm);
|
||||
if (norm.indexOf("..") !== -1 || full !== ROOT && !full.startsWith(ROOT + path.sep)) return false;
|
||||
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) return false;
|
||||
return path.extname(full).toLowerCase() === ".html";
|
||||
}
|
||||
function serveStatic(req, res, urlPath) {
|
||||
if (!isServeable(urlPath)) {
|
||||
res.writeHead(404, { "Content-Type": "application/json" });
|
||||
return res.end("{}");
|
||||
}
|
||||
let file = urlPath === "/" ? "/index.html" : urlPath;
|
||||
const norm = path.normalize(file);
|
||||
const full = path.join(ROOT, norm);
|
||||
if (norm.indexOf("..") !== -1 || (full !== ROOT && !full.startsWith(ROOT + path.sep))) {
|
||||
@ -667,8 +734,8 @@ function serveStatic(req, res, urlPath) {
|
||||
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>");
|
||||
res.writeHead(404, { "Content-Type": "application/json" });
|
||||
return res.end("{}");
|
||||
}
|
||||
const ext = path.extname(full).toLowerCase();
|
||||
const types = {
|
||||
@ -681,42 +748,72 @@ function serveStatic(req, res, urlPath) {
|
||||
}
|
||||
|
||||
// ---------- router ----------
|
||||
function guard(fn) {
|
||||
return function (req, res) {
|
||||
try {
|
||||
const out = fn(req, res);
|
||||
if (out && typeof out.then === "function") {
|
||||
out.catch(function (e) {
|
||||
log("async handler error", req.method, req.url, String((e && e.stack) || e));
|
||||
try {
|
||||
if (!res.headersSent) sendJson(res, 500, { error: "Внутренняя ошибка сервера" });
|
||||
else res.end();
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
log("handler error", req.method, req.url, String((e && e.stack) || e));
|
||||
try {
|
||||
if (!res.headersSent) sendJson(res, 500, { error: "Внутренняя ошибка сервера" });
|
||||
else res.end();
|
||||
} catch (_) {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process.on("uncaughtException", function (e) {
|
||||
log("UNCAUGHT", String((e && e.stack) || e));
|
||||
});
|
||||
process.on("unhandledRejection", function (e) {
|
||||
log("UNHANDLED REJECTION", String((e && e.stack) || e));
|
||||
});
|
||||
|
||||
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/login") return guard(handleLogin)(req, res);
|
||||
if (req.method === "POST" && urlPath === "/api/logout") return guard(handleLogout)(req, res);
|
||||
if (req.method === "GET" && urlPath === "/api/me") return guard(handleMe)(req, res);
|
||||
if (req.method === "POST" && urlPath === "/api/upload") return guard(handleUpload)(req, res);
|
||||
if (req.method === "GET" && urlPath === "/api/dashboard") return guard(handleDashboard)(req, res);
|
||||
if (req.method === "GET" && urlPath === "/api/export") return guard(handleExport)(req, res);
|
||||
if (req.method === "GET" && urlPath === "/api/settings") return guard(handleSettingsGet)(req, res);
|
||||
if (req.method === "POST" && urlPath === "/api/settings") return guard(handleSettingsSave)(req, res);
|
||||
if (req.method === "POST" && urlPath === "/api/add-company") return guard(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()));
|
||||
return guard(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 (req.method === "POST" && userMatch) return guard(handleUserAction)(req, res, userMatch[1]);
|
||||
if (req.method === "POST" && urlPath === "/api/consolidator-password") return guard(handleConsolidatorPassword)(req, res);
|
||||
if (urlPath.indexOf("/api/file/") === 0) {
|
||||
const id = urlPath.slice("/api/file/".length).toUpperCase();
|
||||
return handleFile(req, res, decodeURIComponent(id));
|
||||
return guard(handleFile)(req, res, decodeURIComponent(id));
|
||||
}
|
||||
if (urlPath === "/api/recipients") {
|
||||
if (urlPath === "/api/recipients" || urlPath === "/api/status") {
|
||||
if (!checkXKfuSecret(req, res)) return;
|
||||
return handleRecipients(req, res);
|
||||
}
|
||||
if (urlPath === "/api/status") {
|
||||
if (!checkXKfuSecret(req, res)) return;
|
||||
return handleStatusApi(req, res);
|
||||
return urlPath === "/api/recipients" ? guard(handleRecipients)(req, res) : guard(handleStatusApi)(req, res);
|
||||
}
|
||||
if (urlPath.indexOf("/api/") === 0) { res.writeHead(404, { "Content-Type": "application/json" }); return res.end("{}"); }
|
||||
return serveStatic(req, res, urlPath);
|
||||
return guard(serveStatic)(req, res, urlPath);
|
||||
} catch (e) {
|
||||
log("handler error", req.method, urlPath, String((e && e.message) || e));
|
||||
try { sendJson(res, 500, { error: "Внутренняя ошибка сервера" }); } catch (_) {}
|
||||
log("router error", req.method, req.url, String((e && e.stack) || e));
|
||||
try {
|
||||
if (!res.headersSent) sendJson(res, 500, { error: "Внутренняя ошибка сервера" });
|
||||
else res.end();
|
||||
} catch (_) {}
|
||||
}
|
||||
}).listen(PORT, function () {
|
||||
log("KFU server on port " + PORT);
|
||||
|
||||
30
upload.html
30
upload.html
@ -184,22 +184,38 @@
|
||||
xhr.upload.onprogress = function (ev) {
|
||||
if (ev.lengthComputable) sendBtn.textContent = "Отправка… " + Math.round(ev.loaded * 100 / ev.total) + "%";
|
||||
};
|
||||
xhr.onload = function () {
|
||||
function failMessage(status) {
|
||||
try {
|
||||
var b = JSON.parse(xhr.responseText || "{}");
|
||||
if (xhr.status >= 200 && xhr.status < 300 && b.ok) {
|
||||
showDone(b.uploadedAt);
|
||||
return;
|
||||
var msg = (b && b.error) || "";
|
||||
if (xhr.status === 401) return "Сессия истекла — войдите заново.";
|
||||
if (!msg) {
|
||||
if (xhr.status === 413) msg = "Файл не принят: файл больше 50 МБ";
|
||||
else if (xhr.status === 415) msg = "Файл не принят: поддерживаются только xlsx, xlsm, pdf";
|
||||
else if (xhr.status === 423) msg = "Слишком много попыток — попробуйте позже";
|
||||
else if (xhr.status >= 500) msg = "Ошибка сервера (" + xhr.status + "). Попробуйте ещё раз.";
|
||||
else msg = "Файл не принят: причина — " + (xhr.statusText || xhr.status);
|
||||
}
|
||||
alert((b && b.error) || "Не удалось отправить файл");
|
||||
return msg;
|
||||
} catch (err) {
|
||||
alert("Ошибка сервера: " + xhr.status);
|
||||
if (xhr.status === 401) return "Сессия истекла — войдите заново.";
|
||||
return "Ошибка сервера: " + xhr.status + (xhr.statusText ? " " + xhr.statusText : "");
|
||||
}
|
||||
}
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
var b = JSON.parse(xhr.responseText || "{}");
|
||||
if (b.ok) { showDone(b.uploadedAt); return; }
|
||||
} catch (err) {}
|
||||
}
|
||||
alert(failMessage(xhr.status));
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = "Отправить";
|
||||
if (xhr.status === 401) setTimeout(function () { window.location.href = "index.html"; }, 1500);
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
alert("Сервер недоступен. Проверьте соединение.");
|
||||
alert("Сервер недоступен. Проверьте соединение и попробуйте ещё раз.");
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = "Отправить";
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user