qonaev-compliance/server.js

341 lines
14 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");
const USERS_FILE = path.join(__dirname, "users.json");
function load() {
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); }
catch (e) { return { announcements: [], org: [], contacts: [], insider: [], giftRules: {}, videos: [], laws: [], tests: [], complaints: [], testResults: [], directMessages: [] }; }
}
function save(d) {
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
}
function loadUsers() {
try { return JSON.parse(fs.readFileSync(USERS_FILE, "utf8")); }
catch (e) { return []; }
}
function saveUsers(users) {
fs.writeFileSync(USERS_FILE, JSON.stringify(users, 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 = loadUsers().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) });
}
// ---------- ПАРОЛЬДЫ ҚАЛПЫНА КЕЛТІРУ ----------
if (req.method === "POST" && url === "/api/recover-password") {
const b = await body(req);
const users = loadUsers();
const u = users.find((x) => x.login === String(b.login));
if (!u) return send(res, 404, { ok: false, error: "Мұндай логин табылмады" });
const newPass = "Qonaev" + Math.floor(1000 + Math.random() * 9000);
u.password = newPass;
saveUsers(users);
return send(res, 200, { ok: true, password: newPass, name: u.name });
}
// ---------- ТІРКЕЛУ ----------
if (req.method === "POST" && url === "/api/register") {
const b = await body(req);
if (!b.login || !b.name || !b.password) return send(res, 400, { ok: false, error: "Аты-жөні, логин және құпия сөз міндетті" });
const users = loadUsers();
if (users.find((x) => x.login === String(b.login))) return send(res, 400, { ok: false, error: "Мұндай логин бар, басқа нөмір жазыңыз" });
users.push({ login: String(b.login), password: String(b.password), name: String(b.name), phone: String(b.phone || ""), role: "user" });
saveUsers(users);
const token = crypto.randomBytes(24).toString("hex");
const nu = users[users.length - 1];
sessions.set(token, nu);
return send(res, 200, { ok: true, token, user: publicUser(nu) });
}
// ---------- АККАУНТТАРДЫ БАСҚАРУ (админ) ----------
if (req.method === "GET" && url === "/api/users") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
return send(res, 200, { ok: true, data: loadUsers().map((x) => ({
login: x.login, name: x.name, role: x.role, password: x.password, phone: x.phone || "",
})) });
}
if (req.method === "POST" && url === "/api/users") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
if (!b.login || !b.name || !b.password) return send(res, 400, { ok: false, error: "Логин, аты-жөні және құпия сөз міндетті" });
const users = loadUsers();
if (users.find((x) => x.login === String(b.login))) return send(res, 400, { ok: false, error: "Мұндай логин бар" });
users.push({ login: String(b.login), password: String(b.password), name: String(b.name), phone: String(b.phone || ""), role: b.role === "admin" ? "admin" : "user" });
saveUsers(users);
return send(res, 200, { ok: true });
}
const uDelMatch = url.match(/^\/api\/users\/([^/]+)$/);
if (req.method === "DELETE" && uDelMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const users = loadUsers();
const i = users.findIndex((x) => x.login === uDelMatch[1]);
if (i < 0) return send(res, 404, { ok: false, error: "Табылмады" });
users.splice(i, 1);
saveUsers(users);
return send(res, 200, { ok: true });
}
const uPassMatch = url.match(/^\/api\/users\/([^/]+)\/password$/);
if (req.method === "POST" && uPassMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const users = loadUsers();
const u = users.find((x) => x.login === uPassMatch[1]);
if (!u) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.password) return send(res, 400, { ok: false, error: "Жаңа құпия сөзді жазыңыз" });
u.password = String(b.password);
saveUsers(users);
return send(res, 200, { ok: true });
}
// ---------- АУТЕНТИФИКАЦИЯ ----------
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 compReplyMatch = url.match(/^\/api\/complaints\/(\d+)\/reply$/);
if (req.method === "POST" && compReplyMatch) {
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(compReplyMatch[1]));
if (!c) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.reply) return send(res, 400, { ok: false, error: "Жауап мәтінін жазыңыз" });
c.reply = String(b.reply).slice(0, 3000);
c.replyDate = new Date().toISOString().slice(0, 10);
if (c.status === "new") c.status = "work";
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") {
let list = db.directMessages || [];
if (user.role !== "admin") list = list.filter((m) => m.authorLogin === user.login);
return send(res, 200, { ok: true, data: list });
}
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 dmReplyMatch = url.match(/^\/api\/direct-messages\/(\d+)\/reply$/);
if (req.method === "POST" && dmReplyMatch) {
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(dmReplyMatch[1]));
if (!m) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.reply) return send(res, 400, { ok: false, error: "Жауап мәтінін жазыңыз" });
m.reply = String(b.reply).slice(0, 3000);
m.replyDate = new Date().toISOString().slice(0, 10);
if (m.status === "new") m.status = "work";
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: loadUsers().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));