const express = require('express'); const bcrypt = require('bcrypt'); const path = require('path'); const rateLimit = require('express-rate-limit'); const db = require('../lib/db'); const log = require('../lib/logger'); const router = express.Router(); const SALT_ROUNDS = 10; // Dummy hash compared when the user doesn't exist, so login timing doesn't leak account existence. const DUMMY_HASH = bcrypt.hashSync('wa-dev-tools-dummy-password', SALT_ROUNDS); // Rate limiting for auth endpoints const authLimiter = rateLimit({ keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? "user_" + req.session.user.id : req.ip, windowMs: 15 * 60 * 1000, // 15 min max: 10, // 10 attempts per window message: { error: 'Слишком много попыток. Попробуйте через 15 минут.' }, standardHeaders: true, legacyHeaders: false, }); // Pages router.get('/login', (req, res) => { if (req.session.user) return res.redirect('/dashboard'); res.sendFile(path.join(__dirname, '..', 'public', 'login.html')); }); router.get('/register', (req, res) => { if (req.session.user) return res.redirect('/dashboard'); res.sendFile(path.join(__dirname, '..', 'public', 'register.html')); }); // Login API router.post('/login', authLimiter, express.json(), express.urlencoded({ extended: true }), async (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: 'Email и пароль обязательны' }); } try { const user = await db.findUserByEmail(email.toLowerCase().trim()); // Always run bcrypt (dummy hash when user missing) so response time doesn't reveal account existence. const valid = await bcrypt.compare(password, (user && user.password_hash) ? user.password_hash : DUMMY_HASH); if (!user || !valid) { return res.status(401).json({ error: 'Неверный email или пароль' }); } if (user.is_blocked) { return res.status(403).json({ error: 'Аккаунт заблокирован' }); } await db.updateLastLogin(user.id); // Session regeneration (prevent session fixation) const returnTo = req.session.returnTo; req.session.regenerate((err) => { if (err) { log.error('Session regenerate error', { error: err.message }); return res.status(500).json({ error: 'Ошибка сервера' }); } req.session.user = { id: user.id, email: user.email, name: user.display_name || user.email.split('@')[0], role: user.role || 'user', }; log.info(`Login: ${user.email}`); res.json({ ok: true, redirect: returnTo || '/dashboard' }); }); } catch (err) { log.error('Login error', { error: err.message }); res.status(500).json({ error: 'Ошибка сервера' }); } }); // Sanitize display name (strip HTML tags) function sanitizeName(name) { if (!name) return ''; return name.replace(/<[^>]*>/g, '').trim().slice(0, 100); } // Register API router.post('/register', authLimiter, express.json(), express.urlencoded({ extended: true }), async (req, res) => { const { email, password, password2 } = req.body; const name = sanitizeName(req.body.name); if (!email || !password) { return res.status(400).json({ error: 'Email и пароль обязательны' }); } const emailClean = email.toLowerCase().trim(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailClean)) { return res.status(400).json({ error: 'Некорректный email' }); } if (password.length < 6) { return res.status(400).json({ error: 'Пароль минимум 6 символов' }); } if (password2 && password !== password2) { return res.status(400).json({ error: 'Пароли не совпадают' }); } try { const existing = await db.findUserByEmail(emailClean); if (existing) { return res.status(409).json({ error: 'Этот email уже зарегистрирован' }); } const hash = await bcrypt.hash(password, SALT_ROUNDS); const displayName = (name || '').trim() || emailClean.split('@')[0]; const userId = await db.createUser(emailClean, hash, displayName); // Regenerate session before authenticating (prevent session fixation), same as login. req.session.regenerate((err) => { if (err) { log.error('Session regenerate error (register)', { error: err.message }); return res.status(500).json({ error: 'Ошибка сервера' }); } req.session.user = { id: userId, email: emailClean, name: displayName, role: 'user' }; log.info(`Register: ${emailClean}`); res.json({ ok: true, redirect: '/dashboard' }); }); } catch (err) { log.error('Register error', { error: err.message }); res.status(500).json({ error: 'Ошибка сервера' }); } }); // Logout router.get('/logout', (req, res) => { req.session.destroy(() => { res.redirect('/'); }); }); // Current user info router.get('/me', (req, res) => { if (!req.session.user) return res.status(401).json({ error: 'Not authenticated' }); res.json(req.session.user); }); module.exports = router;