341 lines
14 KiB
JavaScript
341 lines
14 KiB
JavaScript
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));
|