qonaev-compliance/server.js

235 lines
8.9 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.

const http = require("http");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "data.json");
function load() {
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); }
catch (e) { return { users: [], announcements: [], org: [], contacts: [], insider: [], giftRules: {}, videos: [], laws: [], tests: [], complaints: [], testResults: [], directMessages: [] }; }
}
function save(d) {
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
}
const sessions = new Map();
function body(req) {
return new Promise((resolve) => {
let s = "";
req.on("data", (c) => (s += c));
req.on("end", () => {
try { resolve(JSON.parse(s || "{}")); } catch (e) { resolve({}); }
});
});
}
function send(res, code, obj) {
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(obj));
}
function getUser(req) {
const h = req.headers["authorization"] || "";
const token = h.replace(/^Bearer\s+/i, "");
return sessions.get(token) || null;
}
function publicUser(u) {
return { login: u.login, name: u.name, role: u.role };
}
http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
const db = load();
// ---------- ЛОГИН ----------
if (req.method === "POST" && url === "/api/login") {
const b = await body(req);
const u = db.users.find((x) => x.login === String(b.login) && x.password === String(b.password));
if (!u) return send(res, 401, { ok: false, error: "Логин немесе құпия сөз қате" });
const token = crypto.randomBytes(24).toString("hex");
sessions.set(token, u);
return send(res, 200, { ok: true, token, user: publicUser(u) });
}
// ---------- АУТЕНТИФИКАЦИЯ ----------
const user = getUser(req);
const isApi = url.startsWith("/api/");
if (isApi && !user) return send(res, 401, { ok: false, error: "Кіру қажет" });
// ---------- КОНТЕНТ ----------
if (req.method === "GET" && url === "/api/content") {
return send(res, 200, { ok: true, data: {
announcements: db.announcements,
org: db.org,
contacts: db.contacts,
insider: db.insider,
giftRules: db.giftRules,
videos: db.videos,
laws: db.laws,
tests: db.tests.map((t) => ({
id: t.id, title: t.title, pass: t.pass,
questions: t.questions.map((q) => ({ q: q.q, options: q.options })),
})),
}});
}
// ---------- ХАБАРЛАНДЫРУ (админ) ----------
if (req.method === "POST" && url === "/api/announcements") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
if (!b.title || !b.text) return send(res, 400, { ok: false, error: "Тақырып пен мәтінді толтырыңыз" });
db.announcements.unshift({ id: Date.now(), title: b.title, text: b.text, date: b.date || new Date().toISOString().slice(0, 10) });
save(db);
return send(res, 200, { ok: true });
}
// ---------- ШАҒЫМДАР ----------
if (req.method === "GET" && url === "/api/complaints") {
let list = db.complaints;
if (user.role !== "admin") {
list = list.filter((c) => c.authorLogin === user.login || c.anonymous);
}
return send(res, 200, { ok: true, data: list });
}
if (req.method === "POST" && url === "/api/complaints") {
const b = await body(req);
if (!b.text) return send(res, 400, { ok: false, error: "Мәтінді толтырыңыз" });
const item = {
id: Date.now(),
category: b.category || "Басқа",
text: String(b.text).slice(0, 3000),
anonymous: !!b.anonymous,
status: "new",
date: new Date().toISOString().slice(0, 10),
authorLogin: b.anonymous ? null : user.login,
authorName: b.anonymous ? null : user.name,
};
db.complaints.unshift(item);
save(db);
return send(res, 200, { ok: true });
}
const stMatch = url.match(/^\/api\/complaints\/(\d+)\/status$/);
if (req.method === "POST" && stMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const c = db.complaints.find((x) => x.id === Number(stMatch[1]));
if (!c) return send(res, 404, { ok: false, error: "Табылмады" });
if (["new", "work", "done"].includes(b.status)) c.status = b.status;
save(db);
return send(res, 200, { ok: true });
}
// ---------- ТЕСТ НӘТИЖЕЛЕРІ ----------
if (req.method === "GET" && url === "/api/test-results") {
let list = db.testResults;
if (user.role !== "admin") list = list.filter((r) => r.login === user.login);
return send(res, 200, { ok: true, data: list });
}
if (req.method === "POST" && url === "/api/test-results") {
const b = await body(req);
const test = db.tests.find((t) => t.id === Number(b.testId));
if (!test) return send(res, 400, { ok: false, error: "Тест табылмады" });
const answers = Array.isArray(b.answers) ? b.answers : [];
let correct = 0;
test.questions.forEach((q, i) => {
if (answers[i] === q.correct) correct++;
});
const total = test.questions.length;
const score = total ? Math.round((correct / total) * 100) : 0;
db.testResults.push({
id: Date.now(),
testId: test.id,
login: user.login,
name: user.name,
score,
date: new Date().toISOString().slice(0, 10),
});
save(db);
return send(res, 200, { ok: true, score, correct, total });
}
// ---------- КОМПЛАЕНС ОФИЦЕРГЕ ЖЕКЕ ХАТ ----------
if (req.method === "GET" && url === "/api/direct-messages") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
return send(res, 200, { ok: true, data: db.directMessages || [] });
}
if (req.method === "POST" && url === "/api/direct-messages") {
const b = await body(req);
if (!b.text) return send(res, 400, { ok: false, error: "Мәтінді толтырыңыз" });
const item = {
id: Date.now(),
text: String(b.text).slice(0, 3000),
category: b.category || "Жалпы",
status: "new",
date: new Date().toISOString().slice(0, 10),
authorLogin: b.anonymous ? null : user.login,
authorName: b.anonymous ? null : user.name,
anonymous: !!b.anonymous,
};
if (!db.directMessages) db.directMessages = [];
db.directMessages.unshift(item);
save(db);
return send(res, 200, { ok: true });
}
const dmMatch = url.match(/^\/api\/direct-messages\/(\d+)\/status$/);
if (req.method === "POST" && dmMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const m = (db.directMessages || []).find((x) => x.id === Number(dmMatch[1]));
if (!m) return send(res, 404, { ok: false, error: "Табылмады" });
if (["new", "work", "done"].includes(b.status)) m.status = b.status;
save(db);
return send(res, 200, { ok: true });
}
// ---------- СТАТИСТИКА (админ) ----------
if (req.method === "GET" && url === "/api/stats") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const byStatus = { new: 0, work: 0, done: 0 };
const byCategory = {};
db.complaints.forEach((c) => {
byStatus[c.status] = (byStatus[c.status] || 0) + 1;
byCategory[c.category] = (byCategory[c.category] || 0) + 1;
});
const byTest = db.tests.map((t) => {
const rs = db.testResults.filter((r) => r.testId === t.id);
const avg = rs.length ? Math.round(rs.reduce((s, r) => s + r.score, 0) / rs.length) : 0;
return { title: t.title, count: rs.length, avg };
});
const dms = db.directMessages || [];
const dmOpen = dms.filter((m) => m.status !== "done").length;
return send(res, 200, { ok: true, data: {
users: db.users.length,
complaintsTotal: db.complaints.length,
complaintsOpen: byStatus.new + byStatus.work,
testsTotal: db.testResults.length,
dmTotal: dms.length,
dmOpen,
dmDone: dms.length - dmOpen,
byStatus,
byCategory: Object.keys(byCategory).map((k) => ({ cat: k, count: byCategory[k] })),
byTest,
}});
}
// ---------- СТАТИКА ----------
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));
}
send(res, 404, { ok: false, error: "Not found" });
}).listen(PORT, () => console.log("Qonaev TV compliance порталы: http://localhost:" + PORT));