256 lines
10 KiB
JavaScript
256 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
const express = require('express');
|
|
const ldapClient = require('./ldapClient');
|
|
const sessions = require('./sessions');
|
|
const zupAccess = require('./zupAccess');
|
|
const zupLog = require('./zupLog');
|
|
const zupChanges = require('./zupChanges');
|
|
const audit = require('./auditLog');
|
|
const rateLimit = require('./rateLimit');
|
|
|
|
const router = express.Router();
|
|
|
|
function getToken(req) {
|
|
const header = req.headers.authorization || '';
|
|
if (header.startsWith('Bearer ')) return header.slice(7);
|
|
return null;
|
|
}
|
|
|
|
function errorResponse(res, err) {
|
|
const status = err.status || 500;
|
|
res.status(status).json({ success: false, error: err.message || 'Внутренняя ошибка сервера' });
|
|
}
|
|
|
|
function requireZupAuth(req, res, next) {
|
|
const session = sessions.getSession(getToken(req));
|
|
if (!session) {
|
|
return res.status(401).json({ success: false, error: 'Требуется авторизация' });
|
|
}
|
|
const username = (session.user && session.user.username) || '';
|
|
if (!zupAccess.isAdminLogin(username) && !zupAccess.has(username)) {
|
|
return res.status(403).json({ success: false, error: 'Нет доступа к ЗУП' });
|
|
}
|
|
req.session = session;
|
|
next();
|
|
}
|
|
|
|
function requireAdmin(req, res, next) {
|
|
const session = sessions.getSession(getToken(req));
|
|
if (!session) {
|
|
return res.status(401).json({ success: false, error: 'Требуется авторизация' });
|
|
}
|
|
if (!zupAccess.isAdminLogin((session.user && session.user.username) || '')) {
|
|
return res.status(403).json({ success: false, error: 'Требуются права администратора' });
|
|
}
|
|
req.session = session;
|
|
next();
|
|
}
|
|
|
|
router.post('/login', async (req, res) => {
|
|
const { username, password } = req.body || {};
|
|
const ip = rateLimit.getClientIp(req);
|
|
if (rateLimit.isBlocked(ip)) {
|
|
return res.status(429).json({ success: false, error: 'Слишком много попыток, попробуйте через час' });
|
|
}
|
|
if (!username || !password) {
|
|
return res.status(400).json({ success: false, error: 'Логин и пароль обязательны' });
|
|
}
|
|
try {
|
|
const info = await ldapClient.authenticate(username, password, 'basic');
|
|
rateLimit.reset(ip);
|
|
const isAdmin = zupAccess.isAdminLogin(info.username);
|
|
if (!isAdmin && !zupAccess.has(info.username)) {
|
|
audit.logAdminEntry({ ip, username: info.username, success: false, reason: 'нет доступа к ЗУП' });
|
|
return res.status(403).json({ success: false, error: 'Нет доступа к ЗУП — обратитесь к администратору' });
|
|
}
|
|
audit.logAdminEntry({ ip, username: info.username, success: true, reason: 'вход в ЗУП' });
|
|
const token = sessions.createSession({ username: info.username, full_name: info.full_name });
|
|
res.json({ success: true, token, isAdmin, user: { username: info.username, full_name: info.full_name } });
|
|
} catch (err) {
|
|
if (err.status === 401) {
|
|
rateLimit.recordFailure(ip);
|
|
audit.logAdminEntry({ ip, username, success: false, reason: err.message || 'неверный пароль' });
|
|
}
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.post('/logout', (req, res) => {
|
|
sessions.destroySession(getToken(req));
|
|
res.json({ success: true });
|
|
});
|
|
|
|
router.get('/me', requireZupAuth, (req, res) => {
|
|
res.json({
|
|
success: true,
|
|
isAdmin: zupAccess.isAdminLogin((req.session.user && req.session.user.username) || ''),
|
|
user: req.session.user
|
|
});
|
|
});
|
|
|
|
router.get('/users', requireZupAuth, async (req, res) => {
|
|
try {
|
|
const users = await ldapClient.listUsers();
|
|
zupChanges.detectChanges(users);
|
|
res.json({
|
|
success: true,
|
|
users: users.map((u) => ({
|
|
login: u.login,
|
|
fio: u.full_name || '',
|
|
first_name: u.given_name || '',
|
|
last_name: u.sn || '',
|
|
middle_name: u.middle_name || '',
|
|
phone: u.phone || '',
|
|
email: u.email || '',
|
|
position: u.position || '',
|
|
disabled: !!u.disabled,
|
|
isAdmin: zupAccess.isAdminLogin(u.login)
|
|
}))
|
|
});
|
|
} catch (err) {
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.put('/users/:login', requireZupAuth, async (req, res) => {
|
|
const login = String(req.params.login || '').trim();
|
|
const actor = (req.session.user && req.session.user.username) || '';
|
|
const { fio, lastName, firstName, middleName, position, phone, email } = req.body || {};
|
|
try {
|
|
if (!login) return res.status(400).json({ success: false, error: 'Логин не указан' });
|
|
if (String(login).toLowerCase() === String(actor).toLowerCase()) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя редактировать самого себя' });
|
|
}
|
|
if (zupAccess.isAdminLogin(login)) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя редактировать учётную запись администратора' });
|
|
}
|
|
const hasNameFields = lastName !== undefined || firstName !== undefined || middleName !== undefined;
|
|
let newLastName = String(lastName !== undefined ? lastName : '').trim().slice(0, 64);
|
|
let newFirstName = String(firstName !== undefined ? firstName : '').trim().slice(0, 64);
|
|
let newMiddleName = String(middleName !== undefined ? middleName : '').trim().slice(0, 64);
|
|
if (fio !== undefined && !hasNameFields) {
|
|
// обратная совместимость: строка «Фамилия Имя Отчество»
|
|
const parts = String(fio).trim().split(/\s+/);
|
|
newLastName = parts[0] || '';
|
|
newFirstName = parts[1] || '';
|
|
newMiddleName = parts.slice(2).join(' ') || '';
|
|
}
|
|
if (hasNameFields) {
|
|
if (!newLastName || !newFirstName) {
|
|
return res.status(400).json({ success: false, error: 'Укажите фамилию и имя' });
|
|
}
|
|
} else if (!newLastName) {
|
|
return res.status(400).json({ success: false, error: 'ФИО обязательно' });
|
|
}
|
|
const newEmail = String(email || '').trim();
|
|
if (newEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
|
|
return res.status(400).json({ success: false, error: 'Некорректный email' });
|
|
}
|
|
const newPosition = String(position || '').trim().slice(0, 120);
|
|
const newPhone = String(phone || '').trim().slice(0, 60);
|
|
|
|
await ldapClient.getUser(login);
|
|
const result = await ldapClient.updateUser(login, {
|
|
lastName: newLastName,
|
|
firstName: newFirstName,
|
|
middleName: newMiddleName,
|
|
position: newPosition,
|
|
email: newEmail,
|
|
phone: newPhone
|
|
});
|
|
audit.logAdminEntry({ ip: req.ip, username: actor, success: true, reason: `zup edit: ${login}` });
|
|
res.json(result);
|
|
} catch (err) {
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.post('/users/:login/block', requireZupAuth, async (req, res) => {
|
|
const login = String(req.params.login || '').trim();
|
|
const actor = (req.session.user && req.session.user.username) || '';
|
|
try {
|
|
if (!login) return res.status(400).json({ success: false, error: 'Логин не указан' });
|
|
if (String(login).toLowerCase() === String(actor).toLowerCase()) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя заблокировать самого себя' });
|
|
}
|
|
if (zupAccess.isAdminLogin(login)) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя блокировать учётную запись администратора' });
|
|
}
|
|
const user = await ldapClient.getUser(login);
|
|
const result = await ldapClient.setUserEnabled(login, false);
|
|
zupLog.add({ actor, target: login, targetFio: user.full_name || '', action: 'block' });
|
|
audit.logAdminEntry({ ip: req.ip, username: actor, success: true, reason: `zup block: ${login}` });
|
|
res.json(result);
|
|
} catch (err) {
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.post('/users/:login/unblock', requireZupAuth, async (req, res) => {
|
|
const login = String(req.params.login || '').trim();
|
|
const actor = (req.session.user && req.session.user.username) || '';
|
|
try {
|
|
if (!login) return res.status(400).json({ success: false, error: 'Логин не указан' });
|
|
if (String(login).toLowerCase() === String(actor).toLowerCase()) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя разблокировать самого себя' });
|
|
}
|
|
if (zupAccess.isAdminLogin(login)) {
|
|
return res.status(400).json({ success: false, error: 'Нельзя разблокировать учётную запись администратора' });
|
|
}
|
|
const user = await ldapClient.getUser(login);
|
|
const result = await ldapClient.setUserEnabled(login, true);
|
|
zupLog.add({ actor, target: login, targetFio: user.full_name || '', action: 'unblock' });
|
|
audit.logAdminEntry({ ip: req.ip, username: actor, success: true, reason: `zup unblock: ${login}` });
|
|
res.json(result);
|
|
} catch (err) {
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.get('/users/:login/history', requireZupAuth, (req, res) => {
|
|
const login = String(req.params.login || '').trim();
|
|
const actions = zupLog.forTarget(login).map((e) => ({
|
|
kind: 'action',
|
|
ts: e.ts,
|
|
id: e.id,
|
|
actor: e.actor,
|
|
targetFio: e.targetFio,
|
|
action: e.action
|
|
}));
|
|
const changes = zupChanges.changesFor(login).map((c) => ({
|
|
kind: 'change',
|
|
ts: c.ts,
|
|
id: 'c' + c.ts,
|
|
label: c.label,
|
|
field: c.field,
|
|
old_value: c.old_value,
|
|
new_value: c.new_value
|
|
}));
|
|
const merged = [...actions, ...changes].sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
res.json({ success: true, entries: merged });
|
|
});
|
|
|
|
// ================= Управление доступом (только администратор) =================
|
|
|
|
router.get('/access', requireAdmin, (req, res) => {
|
|
res.json({ success: true, items: zupAccess.list() });
|
|
});
|
|
|
|
router.post('/access', requireAdmin, async (req, res) => {
|
|
const { login } = req.body || {};
|
|
const actor = (req.session.user && req.session.user.username) || '';
|
|
try {
|
|
const item = await zupAccess.add(login, actor);
|
|
res.json({ success: true, item });
|
|
} catch (err) {
|
|
errorResponse(res, err);
|
|
}
|
|
});
|
|
|
|
router.delete('/access/:login', requireAdmin, (req, res) => {
|
|
zupAccess.remove(req.params.login);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
module.exports = router; |