'use strict'; const crypto = require('crypto'); const express = require('express'); const ldapClient = require('./ldapClient'); const config = require('./config'); const sessions = require('./sessions'); const adminSettings = require('./adminSettings'); const requests = require('./requests'); const mailer = require('./mailer'); const audit = require('./auditLog'); const rateLimit = require('./rateLimit'); const router = express.Router(); function errorResponse(res, err) { const status = err.status || 500; res.status(status).json({ success: false, error: err.message || 'Внутренняя ошибка сервера' }); } function getToken(req) { const header = req.headers.authorization || ''; if (header.startsWith('Bearer ')) return header.slice(7); return null; } function requireAuth(req, res, next) { const session = sessions.getSession(getToken(req)); if (!session) { return res.status(401).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 (!username || !password) { return res.status(400).json({ success: false, error: 'Логин и пароль обязательны' }); } if (!config.adminLogin || !config.adminPassword) { return res.status(500).json({ success: false, error: 'Учётные данные администратора не заданы в .env (ADMIN_LOGIN/ADMIN_PASSWORD)' }); } const adminAliases = new Set( ldapClient.buildBindNames(config.adminLogin).map((n) => n.toLowerCase()) ); adminAliases.add(config.adminLogin.split('@')[0].split('\\').pop().toLowerCase()); if (!adminAliases.has(String(username).toLowerCase())) { audit.logAdminEntry({ ip, username, success: false, reason: 'не администратор' }); return res.status(401).json({ success: false, error: 'Доступ разрешён только администратору' }); } try { const info = await ldapClient.authenticate(username, password, 'basic'); audit.logAdminEntry({ ip, username, success: true }); const token = sessions.createSession({ username: info.username, full_name: info.full_name, groups: info.groups }); res.json({ success: true, token, user: { username: info.username, full_name: info.full_name } }); } catch (err) { audit.logAdminEntry({ ip, username, success: false, reason: err.message || 'неверный пароль' }); return errorResponse(res, err); } }); router.post('/logout', (req, res) => { sessions.destroySession(getToken(req)); res.json({ success: true }); }); router.get('/me', requireAuth, (req, res) => { res.json({ success: true, user: req.session.user }); }); router.get('/ous', requireAuth, async (req, res) => { try { const ous = await ldapClient.listOUs(); res.json({ success: true, ous }); } catch (err) { errorResponse(res, err); } }); router.get('/groups', requireAuth, async (req, res) => { try { const groups = await ldapClient.listGroups(); res.json({ success: true, groups }); } catch (err) { errorResponse(res, err); } }); router.get('/settings', requireAuth, (req, res) => { res.json({ success: true, settings: adminSettings.getSettings() }); }); router.put('/settings', requireAuth, (req, res) => { const body = req.body || {}; const patch = {}; if (typeof body.ouDn === 'string') patch.ouDn = body.ouDn; if (Array.isArray(body.defaultGroups)) { patch.defaultGroups = body.defaultGroups.filter((g) => typeof g === 'string'); } if (typeof body.phoneRequired === 'boolean') patch.phoneRequired = body.phoneRequired; if (typeof body.authEnabled === 'boolean') patch.authEnabled = body.authEnabled; if (typeof body.logonScript === 'string') patch.logonScript = body.logonScript; if (typeof body.homeDrive === 'string') patch.homeDrive = body.homeDrive; if (typeof body.homeBasePath === 'string') patch.homeBasePath = body.homeBasePath; if (typeof body.crmUrl === 'string') patch.crmUrl = body.crmUrl; if (typeof body.quickLinksUrl === 'string') patch.quickLinksUrl = body.quickLinksUrl; if (Object.keys(patch).length === 0) { return res.status(400).json({ success: false, error: 'Нет данных для сохранения' }); } try { const settings = adminSettings.updateSettings(patch); res.json({ success: true, settings }); } catch (err) { errorResponse(res, err); } }); router.post('/api-key', requireAuth, (req, res) => { const key = crypto.randomBytes(24).toString('base64url'); try { const settings = adminSettings.updateSettings({ apiKey: key }); res.json({ success: true, apiKey: settings.apiKey }); } catch (err) { errorResponse(res, err); } }); router.get('/users', requireAuth, async (req, res) => { try { const users = await ldapClient.listUsers(); res.json({ success: true, users }); } catch (err) { errorResponse(res, err); } }); router.get('/users/:login', requireAuth, async (req, res) => { try { const user = await ldapClient.getUser(req.params.login); res.json({ success: true, user }); } catch (err) { errorResponse(res, err); } }); router.post('/users', requireAuth, async (req, res) => { const { ouDn, fio, login, password, email, phone, position, groupCNs } = req.body || {}; if (!login || !fio) { return res.status(400).json({ success: false, error: 'ФИО и логин обязательны' }); } try { const s = adminSettings.getSettings(); const result = await ldapClient.createUser({ ouDn, fio, login, password, email, phone, position, groupCNs, logonScript: s.logonScript, homeDrive: s.homeDrive, homeBasePath: s.homeBasePath }); res.json(result); } catch (err) { errorResponse(res, err); } }); router.put('/users/:login', requireAuth, async (req, res) => { const { fio, email, phone, position, password, groupCNs } = req.body || {}; try { const s = adminSettings.getSettings(); const result = await ldapClient.updateUser(req.params.login, { fio, email, phone, position, password, groupCNs, logonScript: s.logonScript, homeDrive: s.homeDrive, homeBasePath: s.homeBasePath }); res.json(result); } catch (err) { errorResponse(res, err); } }); router.post('/users/:login/disable', requireAuth, async (req, res) => { try { const result = await ldapClient.setUserEnabled(req.params.login, false); res.json(result); } catch (err) { errorResponse(res, err); } }); router.post('/users/:login/enable', requireAuth, async (req, res) => { try { const result = await ldapClient.setUserEnabled(req.params.login, true); res.json(result); } catch (err) { errorResponse(res, err); } }); // ================= Registration requests ================= async function getTakenLogins(excludeId) { const users = await ldapClient.listUsers(); const taken = users.map((u) => u.login).filter(Boolean); const pending = requests.listRequests().pending; for (const r of pending) { if (r.login && r.id !== excludeId) taken.push(r.login); } return taken; } router.get('/requests', requireAuth, (req, res) => { res.json({ success: true, ...requests.listRequests() }); }); router.get('/requests/:id', requireAuth, (req, res) => { const record = requests.getRequest(req.params.id); if (!record) return res.status(404).json({ success: false, error: 'Заявка не найдена' }); res.json({ success: true, request: record }); }); router.put('/requests/:id', requireAuth, async (req, res) => { const { surname, name, patronymic, login, email, password, phone, position, groupCNs } = req.body || {}; try { const taken = await getTakenLogins(req.params.id); const record = requests.updateRequest(req.params.id, { surname, name, patronymic, login, email, password, phone, position, groupCNs }, taken); res.json({ success: true, request: record }); } catch (err) { errorResponse(res, err); } }); router.post('/requests/:id/approve', requireAuth, async (req, res) => { const record = requests.getRequest(req.params.id); if (!record) return res.status(404).json({ success: false, error: 'Заявка не найдена' }); if (record.phase !== 'pending') { return res.status(409).json({ success: false, error: 'Заявка уже обработана' }); } try { const settings = adminSettings.getSettings(); let warning = null; if (record.type === 'password-change') { await ldapClient.updateUser(record.login, { fio: record.fio, email: record.email, phone: record.phone || '', position: record.position || '', password: record.password }); try { await mailer.sendPasswordChangeApproved(record.email, { fio: record.fio, login: record.login, crmUrl: settings.crmUrl, quickLinksUrl: settings.quickLinksUrl, domain: config.domain }); requests.markProcessed(record.id, { phase: 'approved', emailSent: true }); } catch (mailErr) { requests.markProcessed(record.id, { phase: 'approved', emailSent: false }); warning = `Пароль изменён, но письмо не отправлено: ${mailErr.message}`; } return res.json({ success: true, login: record.login, warning }); } if (!settings.ouDn) { return res.status(400).json({ success: false, error: 'Не задано подразделение — откройте Настройки' }); } const groupCNs = record.groupsEdited ? record.groups : (settings.defaultGroups || []); await ldapClient.createUser({ ouDn: settings.ouDn || undefined, fio: record.fio, login: record.login, password: record.password, email: record.email, phone: record.phone || undefined, position: record.position || undefined, logonScript: settings.logonScript, homeDrive: settings.homeDrive, homeBasePath: settings.homeBasePath, groupCNs }); try { await mailer.sendApproved(record.email, { login: record.login, password: record.password, fio: record.fio, position: record.position, crmUrl: settings.crmUrl, quickLinksUrl: settings.quickLinksUrl, domain: config.domain }); requests.markProcessed(record.id, { phase: 'approved', emailSent: true }); } catch (mailErr) { requests.markProcessed(record.id, { phase: 'approved', emailSent: false }); warning = `Пользователь создан, но письмо не отправлено: ${mailErr.message}`; } res.json({ success: true, login: record.login, warning }); } catch (err) { errorResponse(res, err); } }); router.post('/requests/:id/deny', requireAuth, async (req, res) => { const reason = String((req.body || {}).reason || '').trim(); if (!reason) return res.status(400).json({ success: false, error: 'Укажите причину отказа' }); const record = requests.getRequest(req.params.id); if (!record) return res.status(404).json({ success: false, error: 'Заявка не найдена' }); if (record.phase !== 'pending') { return res.status(409).json({ success: false, error: 'Заявка уже обработана' }); } let warning = null; try { if (record.type === 'password-change') { await mailer.sendPasswordChangeDenied(record.email, { fio: record.fio, reason }); } else { await mailer.sendDenied(record.email, { fio: record.fio, reason }); } requests.markProcessed(record.id, { phase: 'denied', reason, emailSent: true }); } catch (mailErr) { requests.markProcessed(record.id, { phase: 'denied', reason, emailSent: false }); warning = `Заявка отклонена, но письмо не отправлено: ${mailErr.message}`; } res.json({ success: true, warning }); }); router.post('/requests/:id/resend', requireAuth, async (req, res) => { const record = requests.getRequest(req.params.id); if (!record) return res.status(404).json({ success: false, error: 'Заявка не найдена' }); if (record.phase !== 'approved' && record.phase !== 'denied') { return res.status(409).json({ success: false, error: 'Письмо можно отправить только по обработанной заявке' }); } try { if (record.type === 'password-change') { if (record.phase === 'approved') { const s = adminSettings.getSettings(); await mailer.sendPasswordChangeApproved(record.email, { fio: record.fio, login: record.login, crmUrl: s.crmUrl, quickLinksUrl: s.quickLinksUrl, domain: config.domain }); } else { await mailer.sendPasswordChangeDenied(record.email, { fio: record.fio, reason: record.reason }); } } else if (record.phase === 'approved') { const s = adminSettings.getSettings(); await mailer.sendApproved(record.email, { login: record.login, password: record.password, fio: record.fio, position: record.position, crmUrl: s.crmUrl, quickLinksUrl: s.quickLinksUrl, domain: config.domain }); } else { await mailer.sendDenied(record.email, { fio: record.fio, reason: record.reason }); } requests.markProcessed(record.id, { emailSent: true }); res.json({ success: true }); } catch (err) { errorResponse(res, err); } }); // ================= Security: journal, blocks, whitelist ================= router.get('/journal/:type', requireAuth, (req, res) => { const type = req.params.type; if (!['admin', 'auth', 'blocks'].includes(type)) { return res.status(400).json({ success: false, error: 'Неизвестный тип журнала' }); } try { if (type === 'blocks') { return res.json({ success: true, rows: audit.getBlockHistory(req.query.limit) }); } const rows = audit.queryJournal(type, req.query); res.json({ success: true, rows }); } catch (err) { errorResponse(res, err); } }); router.get('/blocks', requireAuth, (req, res) => { res.json({ success: true, active: audit.getActiveBlocks(), history: audit.getBlockHistory(req.query.limit) }); }); router.get('/whitelist', requireAuth, (req, res) => { res.json({ success: true, whitelist: audit.listWhitelist() }); }); router.post('/whitelist', requireAuth, (req, res) => { const { pattern, comment } = req.body || {}; try { const item = audit.addWhitelist(pattern, comment); res.json({ success: true, item }); } catch (err) { errorResponse(res, err); } }); router.delete('/whitelist/:id', requireAuth, (req, res) => { audit.removeWhitelist(req.params.id); res.json({ success: true }); }); module.exports = router;