rasskazhi-o-sebe-kto-ty-chto/server.js
2026-09-10 08:44:33 +00:00

829 lines
30 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 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));