CentralBackend/controller/login/LoginController.js
2025-09-08 16:05:37 +02:00

231 lines
9.0 KiB
JavaScript

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;