'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;