const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const secrets = require('../secrets'); let db; function init(database) { db = database; } function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Требуется авторизация' }); } jwt.verify(token, secrets.getJwtSecret(), (err, user) => { if (err) { return res.status(403).json({ error: 'Недействительный токен' }); } req.user = user; next(); }); } function requireAdmin(req, res, next) { if (req.user && req.user.role === 'admin') { return next(); } return res.status(403).json({ error: 'Требуются права администратора' }); } /** * Синхронизация администратора при каждом запуске: * - если в .env заданы ADMIN_USERNAME и ADMIN_PASSWORD — учётная запись создаётся/обновляется по ним; * - если не заданы и в БД уже есть администратор — ничего не меняем; * - если не заданы и администратора нет — логин "admin" и случайный пароль * генерируются и один раз выводятся в консоль. * Никаких паролей по умолчанию в коде нет. */ function syncAdmin() { const dbGet = (sql, params) => new Promise((resolve, reject) => db.get(sql, params, (e, r) => (e ? reject(e) : resolve(r)))); const dbRun = (sql, params) => new Promise((resolve, reject) => db.run(sql, params, function (e) { e ? reject(e) : resolve(this); })); return (async () => { const envLogin = (process.env.ADMIN_USERNAME || '').trim(); const envPassword = process.env.ADMIN_PASSWORD || ''; if (envLogin && envPassword) { const hash = bcrypt.hashSync(envPassword, 10); const row = await dbGet(`SELECT id FROM users WHERE login = ?`, [envLogin]); if (row) { await dbRun(`UPDATE users SET password_hash = ?, role = 'admin' WHERE login = ?`, [hash, envLogin]); } else { await dbRun(`INSERT INTO users (login, password_hash, full_name, role) VALUES (?, ?, ?, 'admin')`, [envLogin, hash, 'Администратор']); } return; } const existing = await dbGet(`SELECT id FROM users WHERE role = 'admin' LIMIT 1`, []); if (existing) return; const login = 'admin'; const password = secrets.generatePassword(16); await dbRun(`INSERT INTO users (login, password_hash, full_name, role) VALUES (?, ?, ?, 'admin')`, [login, bcrypt.hashSync(password, 10), 'Администратор']); console.log('='.repeat(60)); console.log('Администратор не найден в базе и не задан в .env.'); console.log(`Создана учётная запись: логин "${login}", пароль "${password}"`); console.log('Сохраните пароль сейчас (показывается один раз) или задайте ADMIN_USERNAME/ADMIN_PASSWORD в .env.'); console.log('='.repeat(60)); })(); } // Ограничение перебора пароля: не более 8 неудачных попыток с одного IP за 15 минут const failedLogins = new Map(); const LOGIN_WINDOW_MS = 15 * 60 * 1000; const LOGIN_MAX_FAILS = 8; function isLoginBlocked(ip) { const rec = failedLogins.get(ip); if (!rec) return false; if (Date.now() - rec.first > LOGIN_WINDOW_MS) { failedLogins.delete(ip); return false; } return rec.count >= LOGIN_MAX_FAILS; } function registerLoginFail(ip) { const rec = failedLogins.get(ip); if (!rec || Date.now() - rec.first > LOGIN_WINDOW_MS) failedLogins.set(ip, { count: 1, first: Date.now() }); else rec.count += 1; } function setupRoutes(app) { app.post('/api/auth/login', (req, res) => { const { login, password } = req.body || {}; if (!login || !password) { return res.status(400).json({ error: 'Логин и пароль обязательны' }); } if (isLoginBlocked(req.ip)) { return res.status(429).json({ error: 'Слишком много попыток входа. Повторите через 15 минут.' }); } db.get(`SELECT * FROM users WHERE login = ?`, [login], (err, user) => { if (err || !user) { registerLoginFail(req.ip); return res.status(401).json({ error: 'Неверный логин или пароль' }); } const valid = bcrypt.compareSync(password, user.password_hash); if (!valid) { registerLoginFail(req.ip); return res.status(401).json({ error: 'Неверный логин или пароль' }); } const token = jwt.sign( { id: user.id, login: user.login, role: user.role }, secrets.getJwtSecret(), { expiresIn: '24h' } ); res.json({ token, user: { id: user.id, login: user.login, full_name: user.full_name, role: user.role } }); }); }); app.get('/api/auth/me', authenticateToken, (req, res) => { db.get(`SELECT id, login, full_name, role, created_at FROM users WHERE id = ?`, [req.user.id], (err, user) => { if (err || !user) { return res.status(404).json({ error: 'Пользователь не найден' }); } res.json(user); }); }); app.put('/api/auth/me', authenticateToken, (req, res) => { const { full_name } = req.body; db.run(`UPDATE users SET full_name = ? WHERE id = ?`, [full_name || '', req.user.id], function(err) { if (err) return res.status(500).json({ error: 'Ошибка обновления' }); res.json({ message: 'Профиль обновлён' }); }); }); app.post('/api/auth/change-password', authenticateToken, (req, res) => { const { old_password, new_password } = req.body; if (!old_password || !new_password) { return res.status(400).json({ error: 'Старый и новый пароль обязательны' }); } db.get(`SELECT password_hash FROM users WHERE id = ?`, [req.user.id], (err, user) => { if (err || !user) return res.status(404).json({ error: 'Пользователь не найден' }); if (!bcrypt.compareSync(old_password, user.password_hash)) { return res.status(400).json({ error: 'Неверный старый пароль' }); } const hash = bcrypt.hashSync(new_password, 10); db.run(`UPDATE users SET password_hash = ? WHERE id = ?`, [hash, req.user.id], (err2) => { if (err2) return res.status(500).json({ error: 'Ошибка обновления пароля' }); res.json({ message: 'Пароль изменён' }); }); }); }); } module.exports = { init, authenticateToken, requireAdmin, syncAdmin, setupRoutes };