169 lines
7.5 KiB
JavaScript
169 lines
7.5 KiB
JavaScript
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 };
|