Files
sts-avto-v2/modules/auth/index.js
kalugin66 91db3137bd
All checks were successful
Deploy sts / deploy-sber (push) Successful in 45s
диз
2026-10-12 00:23:22 +05:00

169 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };