Files
ldap-auth-service/lib/zupApi.js
2026-08-25 00:44:10 +05:00

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;