126 lines
4.8 KiB
JavaScript
126 lines
4.8 KiB
JavaScript
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
const loginAttempts = new Map();
|
|
const RATE_LIMIT_WINDOW = 15 * 60 * 1000;
|
|
const MAX_LOGIN_ATTEMPTS = 5;
|
|
const CLEANUP_INTERVAL = 15 * 60 * 1000;
|
|
|
|
function checkRateLimit(ip) {
|
|
const now = Date.now();
|
|
const record = loginAttempts.get(ip);
|
|
if (!record) {
|
|
loginAttempts.set(ip, { count: 1, firstAttempt: now });
|
|
return true;
|
|
}
|
|
if (now - record.firstAttempt > RATE_LIMIT_WINDOW) {
|
|
loginAttempts.set(ip, { count: 1, firstAttempt: now });
|
|
return true;
|
|
}
|
|
if (record.count >= MAX_LOGIN_ATTEMPTS) {
|
|
return false;
|
|
}
|
|
record.count++;
|
|
return true;
|
|
}
|
|
|
|
function clearRateLimit(ip) {
|
|
loginAttempts.delete(ip);
|
|
}
|
|
|
|
function cleanupLoginAttempts() {
|
|
const now = Date.now();
|
|
for (const [ip, record] of loginAttempts.entries()) {
|
|
if (now - record.firstAttempt > RATE_LIMIT_WINDOW) {
|
|
loginAttempts.delete(ip);
|
|
}
|
|
}
|
|
}
|
|
|
|
setInterval(cleanupLoginAttempts, CLEANUP_INTERVAL);
|
|
|
|
let db;
|
|
let JWT_SECRET;
|
|
|
|
function init(database, jwtSecret) {
|
|
db = database;
|
|
JWT_SECRET = jwtSecret;
|
|
}
|
|
|
|
function authenticateToken(req, res, next) {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
|
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
|
|
req.user = user;
|
|
next();
|
|
});
|
|
}
|
|
|
|
function requireAdmin(req, res, next) {
|
|
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' });
|
|
next();
|
|
}
|
|
|
|
function login(req, res) {
|
|
const clientIp = req.ip || req.connection.remoteAddress || 'unknown';
|
|
|
|
if (!checkRateLimit(clientIp)) {
|
|
return res.status(429).json({ error: 'Слишком много попыток входа. Попробуйте через 15 минут.' });
|
|
}
|
|
|
|
const { login, password } = req.body;
|
|
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
|
|
db.get(`SELECT id, login, password_hash, full_name, email, role FROM users WHERE login = ?`, [login], (err, user) => {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
const match = bcrypt.compareSync(password, user.password_hash);
|
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
|
|
clearRateLimit(clientIp);
|
|
|
|
const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
|
|
res.json({
|
|
token,
|
|
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateProfile(req, res) {
|
|
const { full_name, email } = req.body;
|
|
db.run(`UPDATE users SET full_name = COALESCE(?, full_name), email = COALESCE(?, email) WHERE id = ?`,
|
|
[full_name || null, email || null, req.user.id], function(err) {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
db.get(`SELECT id, login, full_name, email, role FROM users WHERE id = ?`, [req.user.id], (err, row) => {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
res.json({ message: 'Profile updated', user: row });
|
|
});
|
|
});
|
|
}
|
|
|
|
function changePassword(req, res) {
|
|
const { current_password, new_password } = req.body;
|
|
if (!current_password || !new_password) return res.status(400).json({ error: 'Current and new password required' });
|
|
if (new_password.length < 6) return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
db.get(`SELECT password_hash FROM users WHERE id = ?`, [req.user.id], (err, row) => {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
if (!row) return res.status(404).json({ error: 'User not found' });
|
|
const match = bcrypt.compareSync(current_password, row.password_hash);
|
|
if (!match) return res.status(401).json({ error: 'Current password is incorrect' });
|
|
const hash = bcrypt.hashSync(new_password, 10);
|
|
db.run(`UPDATE users SET password_hash = ? WHERE id = ?`, [hash, req.user.id], (err) => {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
res.json({ message: 'Password changed successfully' });
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupRoutes(app) {
|
|
app.post('/api/auth/login', login);
|
|
app.put('/api/auth/me', authenticateToken, updateProfile);
|
|
app.post('/api/auth/change-password', authenticateToken, changePassword);
|
|
}
|
|
|
|
module.exports = { init, setupRoutes, authenticateToken, requireAdmin, login }; |