397 lines
15 KiB
JavaScript
397 lines
15 KiB
JavaScript
'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; |