const LoginService = require('../../services/login/LoginService'); const MailService = require('../../services/email/MailService'); const { checkAndIncrementRateLimit } = require('../../middleware/rateLimiter'); const { logger } = require('../../middleware/logger'); const UnitOfWork = require('../../database/UnitOfWork'); const db = require('../../database/database'); class LoginController { static async login(req, res) { try { const { email, password, lang } = req.body; logger.info('[LOGIN][REQ] Received lang from frontend', { lang }); // Step 1: Check rate limit BEFORE processing credentials logger.info('[LOGIN][RATE LIMIT] Checking rate limit for IP', { ip: req.ip }); const rateLimitExceeded = await checkAndIncrementRateLimit({ rateKey: `login:${req.ip}`, max: 10, windowSeconds: 600 }, null); // null for res, just check, don't send response if (rateLimitExceeded) { logger.warn('[RATE LIMIT] Blocked login attempt', { ip: req.ip, time: new Date() }); return res.status(429).json({ success: false, message: 'Rate limit exceeded. Please try again later.' }); } let result; const uow = new UnitOfWork(); // No need to pass pool try { // Step 3: If not blocked, check credentials logger.info('[LOGIN][AUTH] Checking credentials', { email }); result = await LoginService.login(email, password); logger.info('[LOGIN][AUTH] Credentials valid', { email }); } catch (error) { // Step 4: If credentials invalid, increment rate limit logger.warn('[LOGIN][AUTH] Invalid credentials', { email, ip: req.ip }); await checkAndIncrementRateLimit({ rateKey: `login:${req.ip}`, max: 10, windowSeconds: 600 }, res); // increment and send response if needed // If rate limit exceeded, response is already sent if (res.headersSent) { logger.warn('[RATE LIMIT] Rollback after failed login attempt', { ip: req.ip, time: new Date() }); return; } if (error.status) { logger.warn('[AUTH] Rollback due to invalid credentials', { email, ip: req.ip, time: new Date() }); return res.status(error.status).json({ success: false, message: error.message }); } logger.error('Login error', { error }); return res.status(500).json({ success: false, message: 'Internal server error' }); } // Step 5: If credentials valid, allow login (do NOT increment rate limit) // Set refresh token as HTTP-only cookie res.cookie('refreshToken', result.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', expires: result.refreshTokenExpires }); // --- Send login notification email --- try { await MailService.sendLoginNotificationEmail({ email: result.user.email, ip: req.ip, loginTime: new Date(), userAgent: req.headers['user-agent'] || '', lang: lang // <-- pass lang from request body }); } catch (mailError) { logger.error('Error sending login notification email', { error: mailError }); // Do not block login } logger.info('[LOGIN][SUCCESS] Login successful', { email, ip: req.ip }); return res.status(200).json({ success: true, accessToken: result.accessToken, user: result.user }); } catch (error) { // Should not reach here, but fallback logger.error('Login error (outer catch)', { error }); if (error && error.stack) { logger.error('Error stack', { stack: error.stack }); } return res.status(500).json({ success: false, message: 'Internal server error' }); } } static async refresh(req, res) { try { const refreshToken = req.cookies.refreshToken; if (!refreshToken) { logger.warn('No refresh token provided'); return res.status(401).json({ success: false, message: 'No refresh token provided' }); } const result = await LoginService.refresh(refreshToken); // FIX: Set new refresh token as HTTP-only cookie after rotation res.cookie('refreshToken', result.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', expires: result.refreshTokenExpires }); logger.info('Refresh token rotated', { userId: result.user.id }); return res.status(200).json({ success: true, accessToken: result.accessToken, user: result.user }); } catch (error) { if (error.status) { logger.warn('Refresh error', { error }); return res.status(error.status).json({ success: false, message: error.message }); } logger.error('Refresh error', { error }); return res.status(500).json({ success: false, message: 'Internal server error' }); } } static async logout(req, res) { try { // Very detailed request-level logs logger.info('[LOGOUT][REQ] Logout request received', { ip: req.ip, method: req.method, url: req.originalUrl, userAgent: req.headers['user-agent'], cookieKeys: Object.keys(req.cookies || {}) }); // helper to mask tokens in logs (avoid leaking full token) const mask = (s) => { if (!s || typeof s !== 'string') return null; if (s.length <= 12) return `${s.slice(0, 3)}...${s.slice(-3)}`; return `${s.slice(0, 6)}...${s.slice(-6)}`; }; const refreshToken = req.cookies.refreshToken; logger.debug('[LOGOUT] refreshToken presence', { present: !!refreshToken, tokenMasked: mask(refreshToken) }); if (!refreshToken) { logger.warn('[LOGOUT] No refresh token provided for logout; still attempting to clear cookie on backend'); // Try to set expired cookie header explicitly (backend-only for httpOnly) try { res.cookie('refreshToken', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/', expires: new Date(0) }); const setCookieHeader = res.getHeader && res.getHeader('Set-Cookie'); logger.info('[LOGOUT] Set-Cookie header after expiring (no token)', { setCookieHeader }); } catch (cookieErr) { logger.error('[LOGOUT] Error while setting expired cookie (no token)', { error: cookieErr }); } try { res.clearCookie('refreshToken', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/' }); logger.info('[LOGOUT] res.clearCookie called (no token)'); } catch (clearErr) { logger.error('[LOGOUT] Error while calling res.clearCookie (no token)', { error: clearErr }); } logger.info('[LOGOUT] Response headersSent (no token)', { headersSent: !!res.headersSent }); return res.status(400).json({ success: false, message: 'No refresh token provided' }); } logger.info('[LOGOUT] Calling LoginService.logout', { tokenMasked: mask(refreshToken), ip: req.ip }); try { await LoginService.logout(refreshToken); logger.info('[LOGOUT] LoginService.logout completed', { ip: req.ip }); } catch (serviceErr) { logger.error('[LOGOUT] LoginService.logout failed', { error: serviceErr }); // continue to attempt to clear cookie even if service logout fails } // Explicitly expire the refresh token cookie try { res.cookie('refreshToken', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/', expires: new Date(0) }); const setCookieHeader = res.getHeader && res.getHeader('Set-Cookie'); logger.info('[LOGOUT] Set-Cookie header after expiring cookie', { setCookieHeader }); } catch (cookieErr) { logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr }); } // Clear the cookie as well try { res.clearCookie('refreshToken', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/' }); logger.info('[LOGOUT] res.clearCookie called', { headersSent: !!res.headersSent }); } catch (clearErr) { logger.error('[LOGOUT] Error while calling res.clearCookie', { error: clearErr }); } logger.info('[LOGOUT][SUCCESS] Logged out successfully (response about to be sent)', { ip: req.ip }); return res.status(200).json({ success: true, message: 'Logged out successfully' }); } catch (error) { logger.error('[LOGOUT][ERROR] Unexpected logout error', { error, stack: error && error.stack }); return res.status(500).json({ success: false, message: 'Internal server error' }); } } } module.exports = LoginController;