829 lines
30 KiB
JavaScript
829 lines
30 KiB
JavaScript
const http = require("http");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const DATA = path.join(__dirname, "data.json");
|
||
|
||
const EMPTY = {
|
||
commonTodos: [],
|
||
personalTodos: [],
|
||
systems: [],
|
||
servers: [],
|
||
admins: [],
|
||
users: [],
|
||
distribution: [],
|
||
appUsers: [],
|
||
auditLog: []
|
||
};
|
||
|
||
function logAction(user, action, details) {
|
||
const data = load();
|
||
data.auditLog.push({
|
||
id: Date.now(),
|
||
when: new Date().toISOString(),
|
||
user: user,
|
||
action: action,
|
||
details: details
|
||
});
|
||
if (data.auditLog.length > 1000) data.auditLog = data.auditLog.slice(-1000);
|
||
save(data);
|
||
}
|
||
|
||
function load() {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(DATA, "utf8"));
|
||
} catch (_) {
|
||
return EMPTY;
|
||
}
|
||
}
|
||
|
||
function save(data) {
|
||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
function body(req) {
|
||
return new Promise((resolve) => {
|
||
let s = "";
|
||
req.on("data", (c) => (s += c));
|
||
req.on("end", () => {
|
||
try {
|
||
resolve(JSON.parse(s || "{}"));
|
||
} catch (_) {
|
||
resolve({});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function json(res, data) {
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
res.end(JSON.stringify(data));
|
||
}
|
||
|
||
function notFound(res) {
|
||
res.writeHead(404);
|
||
res.end("Not found");
|
||
}
|
||
|
||
function forbidden(res) {
|
||
res.writeHead(403);
|
||
res.end(JSON.stringify({ error: "Forbidden" }));
|
||
}
|
||
|
||
function checkPermission(req, section, action) {
|
||
const user = req.headers["x-user"];
|
||
if (!user) return false;
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser) return false;
|
||
|
||
const role = appUser.role;
|
||
|
||
// Admin — полный доступ
|
||
if (role === "admin") return true;
|
||
|
||
// Оператор — только просмотр на отдельные вкладки + создание личных задач
|
||
if (role === "operator") {
|
||
if (action === "view") {
|
||
// Оператор видит: дашборд, системы, серверы, админы, операторы, распределение
|
||
const viewSections = ["dashboard", "systems", "servers", "admins", "operators", "distribution"];
|
||
return viewSections.includes(section);
|
||
}
|
||
// Оператор может создавать только личные задачи
|
||
if (action === "create" && section === "personal-todos") return true;
|
||
if (action === "complete" && section === "todos") return true; // закрывать свои задачи
|
||
return false;
|
||
}
|
||
|
||
// Модератор — все вкладки кроме "Пользователи приложения" + гибкие права
|
||
if (role === "moderator") {
|
||
const perms = appUser.permissions || {};
|
||
// Проверка доступа к вкладке
|
||
if (action === "view") {
|
||
// Модератор НЕ видит "Пользователи приложения"
|
||
if (section === "appUsers" || section === "users") return false;
|
||
const sectionPerms = perms[section] || {};
|
||
return sectionPerms.view === true;
|
||
}
|
||
// Проверка действий (create, edit, delete)
|
||
if (section === "appUsers" || section === "users") return false;
|
||
const sectionPerms = perms[section] || {};
|
||
return sectionPerms[action] === true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
http.createServer(async (req, res) => {
|
||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-User");
|
||
|
||
if (req.method === "OPTIONS") {
|
||
res.writeHead(200, {
|
||
"Access-Control-Allow-Origin": "*",
|
||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||
"Access-Control-Allow-Headers": "Content-Type, X-User"
|
||
});
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
const url = req.url.split("?")[0];
|
||
console.log("Request:", req.method, url);
|
||
|
||
// API: Todos
|
||
// API: Common Todos (общие задачи)
|
||
if (req.method === "GET" && url === "/api/common-todos") {
|
||
const data = load();
|
||
return json(res, data.commonTodos || []);
|
||
}
|
||
if (req.method === "POST" && url === "/api/common-todos") {
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
item.createdBy = user;
|
||
item.createdAt = new Date().toISOString();
|
||
// userStates: { login: { completed: boolean, completedAt: string|null } }
|
||
item.userStates = {};
|
||
data.commonTodos.push(item);
|
||
save(data);
|
||
logAction(user, "create_common_todo", item.title);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/common-todos/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const idx = data.commonTodos.findIndex((t) => t.id === id);
|
||
if (idx !== -1) {
|
||
const bodyData = await body(req);
|
||
// Если есть userState, обновляем только для текущего пользователя
|
||
if (bodyData.userState !== undefined) {
|
||
if (!data.commonTodos[idx].userStates) data.commonTodos[idx].userStates = {};
|
||
data.commonTodos[idx].userStates[user] = bodyData.userState;
|
||
} else {
|
||
data.commonTodos[idx] = { ...data.commonTodos[idx], ...bodyData };
|
||
}
|
||
save(data);
|
||
logAction(user, "update_common_todo", "id=" + id);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/common-todos/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
data.commonTodos = data.commonTodos.filter((t) => t.id !== id);
|
||
save(data);
|
||
logAction(user, "delete_common_todo", "id=" + id);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Personal Todos (личные задачи)
|
||
if (req.method === "GET" && url === "/api/personal-todos") {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const todos = data.personalTodos.filter(t => t.createdBy === user);
|
||
return json(res, todos);
|
||
}
|
||
if (req.method === "POST" && url === "/api/personal-todos") {
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
item.completed = false;
|
||
item.completedAt = null;
|
||
item.createdBy = user;
|
||
data.personalTodos.push(item);
|
||
save(data);
|
||
logAction(user, "create_personal_todo", item.title);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/personal-todos/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const idx = data.personalTodos.findIndex((t) => t.id === id);
|
||
if (idx !== -1) {
|
||
data.personalTodos[idx] = { ...data.personalTodos[idx], ...(await body(req)) };
|
||
save(data);
|
||
logAction(user, "update_personal_todo", "id=" + id);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/personal-todos/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
data.personalTodos = data.personalTodos.filter((t) => t.id !== id);
|
||
save(data);
|
||
logAction(user, "delete_personal_todo", "id=" + id);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Systems
|
||
if (req.method === "GET" && url === "/api/systems") {
|
||
return json(res, load().systems);
|
||
}
|
||
if (req.method === "POST" && url === "/api/systems") {
|
||
if (!checkPermission(req, "systems", "edit")) return forbidden(res);
|
||
const data = load();
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
data.systems.push(item);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/systems/")) {
|
||
if (!checkPermission(req, "systems", "edit")) return forbidden(res);
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const idx = data.systems.findIndex((s) => s.id === id);
|
||
if (idx !== -1) {
|
||
data.systems[idx] = { ...data.systems[idx], ...(await body(req)) };
|
||
save(data);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/systems/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
data.systems = data.systems.filter((s) => s.id !== id);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Servers
|
||
if (req.method === "GET" && url === "/api/servers") {
|
||
return json(res, load().servers);
|
||
}
|
||
if (req.method === "POST" && url === "/api/servers") {
|
||
if (!checkPermission(req, "servers", "edit")) return forbidden(res);
|
||
const data = load();
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
data.servers.push(item);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/servers/")) {
|
||
if (!checkPermission(req, "servers", "edit")) return forbidden(res);
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const idx = data.servers.findIndex((s) => s.id === id);
|
||
if (idx !== -1) {
|
||
data.servers[idx] = { ...data.servers[idx], ...(await body(req)) };
|
||
save(data);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/servers/")) {
|
||
if (!checkPermission(req, "servers", "edit")) return forbidden(res);
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
data.servers = data.servers.filter((s) => s.id !== id);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Admins
|
||
if (req.method === "GET" && url === "/api/admins") {
|
||
return json(res, load().admins);
|
||
}
|
||
if (req.method === "POST" && url === "/api/admins") {
|
||
const data = load();
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
data.admins.push(item);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/admins/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const idx = data.admins.findIndex((a) => a.id === id);
|
||
if (idx !== -1) {
|
||
data.admins[idx] = { ...data.admins[idx], ...(await body(req)) };
|
||
save(data);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/admins/")) {
|
||
const idParam = url.split("/").pop();
|
||
const id = idParam.includes('.') ? parseFloat(idParam) : parseInt(idParam);
|
||
const data = load();
|
||
data.admins = data.admins.filter((a) => a.id !== id);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Admin Servers (multiple servers per admin)
|
||
if (req.method === "GET" && url === "/api/admin-servers") {
|
||
return json(res, load().adminServers || []);
|
||
}
|
||
if (req.method === "POST" && url === "/api/admin-servers") {
|
||
const data = load();
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
if (!data.adminServers) data.adminServers = [];
|
||
data.adminServers.push(item);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/admin-servers/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const idx = (data.adminServers || []).findIndex((s) => s.id === id);
|
||
if (idx !== -1) {
|
||
data.adminServers[idx] = { ...data.adminServers[idx], ...(await body(req)) };
|
||
save(data);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/admin-servers/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
data.adminServers = (data.adminServers || []).filter((s) => s.id !== id);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Import servers from file
|
||
if (req.method === "POST" && url === "/api/import-servers") {
|
||
const data = load();
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const filePath = path.join(__dirname, "список серверов.txt");
|
||
if (!fs.existsSync(filePath)) {
|
||
return json(res, { ok: false, error: "Файл не найден" });
|
||
}
|
||
const content = fs.readFileSync(filePath, "utf8");
|
||
const lines = content.trim().split("\n").slice(1);
|
||
const rows = lines.map(line => {
|
||
const cols = line.split("\t");
|
||
return { admin: cols[0], city: cols[1], region: cols[2], ip: cols[3], cityCode: cols[4] };
|
||
});
|
||
|
||
const existingAdmins = new Set(data.admins.map(a => a.name));
|
||
rows.forEach(row => {
|
||
if (!existingAdmins.has(row.admin)) {
|
||
data.admins.push({ id: Date.now() + Math.random(), name: row.admin, position: "", phone: "", email: "" });
|
||
existingAdmins.add(row.admin);
|
||
}
|
||
});
|
||
|
||
const existingIps = new Set(data.servers.map(s => s.ip));
|
||
const addedIps = new Set();
|
||
rows.forEach(row => {
|
||
const ip = row.ip.trim();
|
||
if (!existingIps.has(ip) && !addedIps.has(ip)) {
|
||
data.servers.push({ id: Date.now() + Math.random(), admin: row.admin, city: row.city, region: row.region, cityCode: row.cityCode.trim(), ip: ip });
|
||
addedIps.add(ip);
|
||
}
|
||
});
|
||
|
||
save(data);
|
||
return json(res, { ok: true, count: rows.length });
|
||
}
|
||
|
||
// API: Import admins from file (старый формат)
|
||
if (req.method === "POST" && url === "/api/import-admins") {
|
||
const data = load();
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const filePath = path.join(__dirname, "список админов.txt");
|
||
if (!fs.existsSync(filePath)) {
|
||
return json(res, { ok: false, error: "Файл не найден" });
|
||
}
|
||
const content = fs.readFileSync(filePath, "utf8");
|
||
const lines = content.trim().split("\n").slice(1);
|
||
const rows = lines.map(line => {
|
||
const cols = line.split("\t");
|
||
return {
|
||
name: cols[0],
|
||
workPhone: cols[1],
|
||
homePhone: cols[2],
|
||
mobilePhone: cols[3],
|
||
city: cols[4]
|
||
};
|
||
});
|
||
|
||
const existingNames = new Set(data.admins.map(a => a.name));
|
||
let added = 0;
|
||
rows.forEach(row => {
|
||
if (!existingNames.has(row.name)) {
|
||
data.admins.push({
|
||
id: Date.now() + Math.random(),
|
||
name: row.name,
|
||
position: "",
|
||
workPhone: row.workPhone || "",
|
||
homePhone: row.homePhone || "",
|
||
mobilePhone: row.mobilePhone || "",
|
||
email: ""
|
||
});
|
||
existingNames.add(row.name);
|
||
added++;
|
||
}
|
||
});
|
||
|
||
save(data);
|
||
return json(res, { ok: true, count: added });
|
||
}
|
||
|
||
// API: Import admins from file -1 (обновление существующих)
|
||
if (req.method === "POST" && url === "/api/import-admins-1") {
|
||
const data = load();
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const filePath = path.join(__dirname, "список админов-1.txt");
|
||
if (!fs.existsSync(filePath)) {
|
||
return json(res, { ok: false, error: "Файл не найден" });
|
||
}
|
||
const content = fs.readFileSync(filePath, "utf8");
|
||
const lines = content.trim().split("\n").slice(1);
|
||
const rows = lines.map(line => {
|
||
const cols = line.split("\t");
|
||
return {
|
||
name: cols[0],
|
||
workPhone: cols[1],
|
||
homePhone: cols[2],
|
||
mobilePhone: cols[3],
|
||
city: cols[4]
|
||
};
|
||
});
|
||
|
||
let updated = 0;
|
||
let added = 0;
|
||
rows.forEach(row => {
|
||
const idx = data.admins.findIndex(a => a.name === row.name);
|
||
if (idx !== -1) {
|
||
// Обновляем существующего
|
||
data.admins[idx].workPhone = row.workPhone;
|
||
data.admins[idx].homePhone = row.homePhone;
|
||
data.admins[idx].mobilePhone = row.mobilePhone;
|
||
updated++;
|
||
} else {
|
||
// Добавляем нового
|
||
data.admins.push({
|
||
id: Date.now() + Math.random(),
|
||
name: row.name,
|
||
position: "",
|
||
workPhone: row.workPhone || "",
|
||
homePhone: row.homePhone || "",
|
||
mobilePhone: row.mobilePhone || "",
|
||
email: ""
|
||
});
|
||
added++;
|
||
}
|
||
});
|
||
|
||
save(data);
|
||
return json(res, { ok: true, updated, added });
|
||
}
|
||
|
||
// API: Users
|
||
if (req.method === "GET" && url === "/api/users") {
|
||
return json(res, load().users);
|
||
}
|
||
if (req.method === "POST" && url === "/api/users") {
|
||
const data = load();
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
data.users.push(item);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/users/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const idx = data.users.findIndex((u) => u.id === id);
|
||
if (idx !== -1) {
|
||
data.users[idx] = { ...data.users[idx], ...(await body(req)) };
|
||
save(data);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/users/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
data.users = data.users.filter((u) => u.id !== id);
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Distribution
|
||
if (req.method === "GET" && url === "/api/distribution") {
|
||
return json(res, load().distribution);
|
||
}
|
||
if (req.method === "POST" && url === "/api/distribution") {
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const item = await body(req);
|
||
item.id = Date.now();
|
||
data.distribution.push(item);
|
||
save(data);
|
||
logAction(user, "create_distribution", JSON.stringify(item));
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/distribution/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const idx = data.distribution.findIndex((d) => d.id === id);
|
||
if (idx !== -1) {
|
||
data.distribution[idx] = { ...data.distribution[idx], ...(await body(req)) };
|
||
save(data);
|
||
logAction(user, "update_distribution", "id=" + id);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/distribution/")) {
|
||
const id = parseInt(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
data.distribution = data.distribution.filter((d) => d.id !== id);
|
||
save(data);
|
||
logAction(user, "delete_distribution", "id=" + id);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Auth
|
||
if (req.method === "POST" && url === "/api/login") {
|
||
const data = load();
|
||
const { login, password } = await body(req);
|
||
const user = data.appUsers.find((u) => u.login === login && u.password === password);
|
||
if (user) {
|
||
logAction(login, "login", "ok");
|
||
const permissions = user.role === 'admin' ? 'all' : (user.permissions || {});
|
||
return json(res, { ok: true, user: { login: user.login, role: user.role, name: user.name, permissions } });
|
||
}
|
||
logAction(login || "unknown", "login", "failed");
|
||
res.writeHead(401, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify({ ok: false, error: "Неверный логин или пароль" }));
|
||
}
|
||
|
||
// API: Profile текущего пользователя
|
||
if (req.method === "GET" && url === "/api/profile") {
|
||
const user = req.headers["x-user"];
|
||
if (!user) return forbidden(res);
|
||
const data = load();
|
||
const appUser = data.appUsers.find((u) => u.login === user);
|
||
if (!appUser) return forbidden(res);
|
||
// Поддержка старых полей для совместимости
|
||
return json(res, {
|
||
login: appUser.login,
|
||
name: appUser.name,
|
||
position: appUser.position,
|
||
workPhone: appUser.workPhone || appUser.phone || "",
|
||
homePhone: appUser.homePhone || "",
|
||
mobilePhone: appUser.mobilePhone || "",
|
||
email: appUser.email || "",
|
||
city: appUser.city || "",
|
||
ipAddress: appUser.ipAddress || appUser.dbAddress || "",
|
||
password: appUser.password || ""
|
||
});
|
||
}
|
||
if (req.method === "PUT" && url === "/api/profile") {
|
||
const user = req.headers["x-user"];
|
||
if (!user) return forbidden(res);
|
||
const data = load();
|
||
const idx = data.appUsers.findIndex((u) => u.login === user);
|
||
if (idx === -1) return forbidden(res);
|
||
const upd = await body(req);
|
||
// Сохраняем новые поля, оставляем старые для совместимости
|
||
data.appUsers[idx] = {
|
||
...data.appUsers[idx],
|
||
...upd,
|
||
workPhone: upd.workPhone !== undefined ? upd.workPhone : data.appUsers[idx].workPhone,
|
||
homePhone: upd.homePhone !== undefined ? upd.homePhone : data.appUsers[idx].homePhone,
|
||
mobilePhone: upd.mobilePhone !== undefined ? upd.mobilePhone : data.appUsers[idx].mobilePhone,
|
||
ipAddress: upd.ipAddress !== undefined ? upd.ipAddress : data.appUsers[idx].ipAddress
|
||
};
|
||
save(data);
|
||
logAction(user, "update_profile", user);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Users (app users management)
|
||
if (req.method === "GET" && url === "/api/app-users") {
|
||
const users = load().appUsers.map((u) => ({ login: u.login, role: u.role, name: u.name, position: u.position, phone: u.phone, workPhone: u.workPhone, homePhone: u.homePhone, mobilePhone: u.mobilePhone, email: u.email, city: u.city, cityCode: u.cityCode, dbAddress: u.dbAddress, ipAddress: u.ipAddress, permissions: u.permissions }));
|
||
return json(res, users);
|
||
}
|
||
if (req.method === "POST" && url === "/api/app-users") {
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const item = await body(req);
|
||
if (data.appUsers.find((u) => u.login === item.login)) {
|
||
res.writeHead(400, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify({ ok: false, error: "Пользователь существует" }));
|
||
}
|
||
data.appUsers.push({
|
||
login: item.login,
|
||
password: item.password,
|
||
name: item.name || "",
|
||
position: item.position || "",
|
||
email: item.email || "",
|
||
city: item.city || "",
|
||
workPhone: item.workPhone || item.phone || "",
|
||
homePhone: item.homePhone || "",
|
||
mobilePhone: item.mobilePhone || "",
|
||
ipAddress: item.ipAddress || item.dbAddress || "",
|
||
role: item.role,
|
||
permissions: item.role === "moderator" ? item.permissions || {} : {}
|
||
});
|
||
save(data);
|
||
logAction(user, "create_user", item.login);
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "PUT" && url.startsWith("/api/app-users/")) {
|
||
const login = decodeURIComponent(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
const idx = data.appUsers.findIndex((u) => u.login === login);
|
||
if (idx !== -1) {
|
||
const upd = await body(req);
|
||
// Сохраняем новые поля и поддерживаем старые для совместимости
|
||
data.appUsers[idx] = {
|
||
...data.appUsers[idx],
|
||
...upd,
|
||
workPhone: upd.workPhone !== undefined ? upd.workPhone : data.appUsers[idx].workPhone,
|
||
homePhone: upd.homePhone !== undefined ? upd.homePhone : data.appUsers[idx].homePhone,
|
||
mobilePhone: upd.mobilePhone !== undefined ? upd.mobilePhone : data.appUsers[idx].mobilePhone,
|
||
ipAddress: upd.ipAddress !== undefined ? upd.ipAddress : data.appUsers[idx].ipAddress
|
||
};
|
||
save(data);
|
||
logAction(user, "update_user", login);
|
||
}
|
||
return json(res, { ok: true });
|
||
}
|
||
if (req.method === "DELETE" && url.startsWith("/api/app-users/")) {
|
||
const login = decodeURIComponent(url.split("/").pop());
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
data.appUsers = data.appUsers.filter((u) => u.login !== login);
|
||
save(data);
|
||
logAction(user, "delete_user", login);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Sync admins to app-users
|
||
if (req.method === "POST" && url === "/api/sync-admins-to-users") {
|
||
const data = load();
|
||
const user = req.headers["x-user"];
|
||
let created = 0;
|
||
let updated = 0;
|
||
|
||
(data.admins || []).forEach(admin => {
|
||
// Используем login из админа, если нет - генерируем из имени
|
||
let login = admin.login || '';
|
||
if (!login) {
|
||
login = (admin.name || '').toLowerCase().replace(/[^a-zа-яё0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
||
}
|
||
if (!login) return;
|
||
|
||
const existing = data.appUsers.find(u => u.login === login);
|
||
if (!existing) {
|
||
// Создаём нового пользователя с правами оператора по умолчанию
|
||
data.appUsers.push({
|
||
login: login,
|
||
password: "password123",
|
||
name: admin.name || "",
|
||
position: admin.position || "",
|
||
phone: admin.workPhone || admin.phone || "",
|
||
email: admin.email || "",
|
||
city: admin.city || "",
|
||
cityCode: admin.cityCode || "",
|
||
dbAddress: "",
|
||
role: "operator",
|
||
permissions: {}
|
||
});
|
||
created++;
|
||
logAction(user, "create_user_from_admin", login);
|
||
} else {
|
||
// Обновляем данные существующего
|
||
existing.name = admin.name || existing.name;
|
||
existing.position = admin.position || existing.position;
|
||
existing.phone = admin.workPhone || admin.phone || existing.phone;
|
||
existing.email = admin.email || existing.email;
|
||
existing.city = admin.city || existing.city;
|
||
existing.cityCode = admin.cityCode || existing.cityCode;
|
||
updated++;
|
||
}
|
||
});
|
||
|
||
save(data);
|
||
logAction(user, "sync_admins", "created=" + created + ", updated=" + updated);
|
||
return json(res, { ok: true, created, updated });
|
||
}
|
||
|
||
// API: Audit log
|
||
if (req.method === "GET" && url === "/api/audit") {
|
||
return json(res, load().auditLog.reverse().slice(0, 200));
|
||
}
|
||
|
||
// API: Roles (admin only)
|
||
if (req.method === "GET" && url === "/api/roles") {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser || appUser.role !== "admin") return forbidden(res);
|
||
return json(res, data.roles || []);
|
||
}
|
||
|
||
if (req.method === "POST" && url === "/api/roles") {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser || appUser.role !== "admin") return forbidden(res);
|
||
const b = await body(req);
|
||
if (!data.roles) data.roles = [];
|
||
if (data.roles.find(r => r.name === b.name)) return json(res, { error: "Роль существует" }, 400);
|
||
data.roles.push({ name: b.name, description: b.description || "", permissions: b.permissions || {} });
|
||
save(data);
|
||
logAction(user, "create", "role: " + b.name);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
if (req.method === "PUT" && url.startsWith("/api/roles/")) {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser || appUser.role !== "admin") return forbidden(res);
|
||
const oldName = decodeURIComponent(url.split("/")[3]);
|
||
const role = (data.roles || []).find(r => r.name === oldName);
|
||
if (!role) return notFound(res);
|
||
const b = await body(req);
|
||
if (b.name && b.name !== oldName && data.roles.find(r => r.name === b.name)) {
|
||
return json(res, { error: "Роль с таким именем существует" }, 400);
|
||
}
|
||
role.name = b.name || oldName;
|
||
role.description = b.description !== undefined ? b.description : role.description;
|
||
role.permissions = b.permissions !== undefined ? b.permissions : role.permissions;
|
||
save(data);
|
||
logAction(user, "edit", "role: " + oldName);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
if (req.method === "DELETE" && url.startsWith("/api/roles/")) {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser || appUser.role !== "admin") return forbidden(res);
|
||
const name = decodeURIComponent(url.split("/")[3]);
|
||
if (name === "admin") return json(res, { error: "Нельзя удалить роль admin" }, 400);
|
||
const idx = (data.roles || []).findIndex(r => r.name === name);
|
||
if (idx === -1) return notFound(res);
|
||
data.roles.splice(idx, 1);
|
||
// Сбросить роль у пользователей
|
||
(data.appUsers || []).forEach(u => { if (u.role === name) u.role = "operator"; });
|
||
save(data);
|
||
logAction(user, "delete", "role: " + name);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// API: Update moderator permissions (admin only)
|
||
if (req.method === "POST" && url === "/api/update-permissions") {
|
||
const user = req.headers["x-user"];
|
||
const data = load();
|
||
const appUser = (data.appUsers || []).find(u => u.login === user);
|
||
if (!appUser || appUser.role !== "admin") return forbidden(res);
|
||
|
||
(data.appUsers || []).forEach(u => {
|
||
if (u.role === "moderator" && u.permissions) {
|
||
const old = u.permissions;
|
||
u.permissions = {};
|
||
["todos", "systems", "servers", "admins", "operators", "distribution", "audit"].forEach(section => {
|
||
const oldPerm = old[section] || {};
|
||
u.permissions[section] = {
|
||
view: oldPerm.view === true,
|
||
create: oldPerm.edit === true,
|
||
edit: oldPerm.edit === true,
|
||
delete: oldPerm.edit === true
|
||
};
|
||
});
|
||
}
|
||
if (u.role === "operator" && !u.permissions) {
|
||
u.permissions = {};
|
||
}
|
||
});
|
||
|
||
save(data);
|
||
return json(res, { ok: true });
|
||
}
|
||
|
||
// Static files
|
||
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" : "text/html";
|
||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||
return res.end(fs.readFileSync(full));
|
||
}
|
||
|
||
notFound(res);
|
||
}).listen(PORT, () => console.log("Server running on port " + PORT));
|