Fix session expiration handling
This commit is contained in:
@@ -11,6 +11,24 @@ const requireEnv = (name) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const parseDurationMs = (value, fallbackMs) => {
|
||||
if (!value) return fallbackMs;
|
||||
const match = String(value).trim().match(/^(\d+)(ms|s|m|h|d)?$/i);
|
||||
if (!match) return fallbackMs;
|
||||
|
||||
const amount = Number(match[1]);
|
||||
const unit = (match[2] || 'ms').toLowerCase();
|
||||
const multipliers = {
|
||||
ms: 1,
|
||||
s: 1000,
|
||||
m: 60 * 1000,
|
||||
h: 60 * 60 * 1000,
|
||||
d: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
return amount * multipliers[unit];
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
[
|
||||
'JWT_SECRET',
|
||||
@@ -53,7 +71,8 @@ const env = {
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'change_me',
|
||||
accessExpires: process.env.JWT_ACCESS_EXPIRES || '15m',
|
||||
refreshExpires: process.env.JWT_REFRESH_EXPIRES || '7d',
|
||||
refreshExpires: process.env.JWT_REFRESH_EXPIRES || '15m',
|
||||
refreshExpiresMs: parseDurationMs(process.env.JWT_REFRESH_EXPIRES || '15m', 15 * 60 * 1000),
|
||||
},
|
||||
|
||||
storagePath: process.env.STORAGE_PATH || '/tmp/linkedsign_storage',
|
||||
|
||||
@@ -3,7 +3,12 @@ const { verify } = require('../utils/jwt');
|
||||
const authenticate = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ success: false, data: null, error: 'Authorization token required.' });
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
data: null,
|
||||
error: 'La sesión expiró. Vuelve a iniciar sesión.',
|
||||
code: 'SESSION_EXPIRED',
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
@@ -12,7 +17,12 @@ const authenticate = (req, res, next) => {
|
||||
req.user = { id: decoded.sub, email: decoded.email, role: decoded.role, name: decoded.name };
|
||||
return next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, data: null, error: 'Invalid or expired token.' });
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
data: null,
|
||||
error: 'La sesión expiró. Vuelve a iniciar sesión.',
|
||||
code: 'SESSION_EXPIRED',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const getRefreshCookieOptions = () => ({
|
||||
secure: env.nodeEnv === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
maxAge: env.jwt.refreshExpiresMs,
|
||||
});
|
||||
|
||||
const readRefreshToken = (req) => {
|
||||
@@ -51,7 +51,7 @@ const register = async (req, res, next) => {
|
||||
certificateFile: req.file,
|
||||
}, getContext(req));
|
||||
return sendSuccess(res, {
|
||||
message: 'Registration successful. Please check your email to verify your account.',
|
||||
message: 'Registro exitoso. Revisa tu correo para verificar tu cuenta.',
|
||||
userId: result.userId,
|
||||
}, 201);
|
||||
} catch (err) {
|
||||
@@ -83,7 +83,7 @@ const logout = async (req, res, next) => {
|
||||
try {
|
||||
await authService.logout(readRefreshToken(req));
|
||||
res.clearCookie(env.refreshCookieName, getRefreshCookieOptions());
|
||||
return sendSuccess(res, { message: 'Logged out.' }, 200);
|
||||
return sendSuccess(res, { message: 'Sesión cerrada.' }, 200);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -92,7 +92,7 @@ const logout = async (req, res, next) => {
|
||||
const changePassword = async (req, res, next) => {
|
||||
try {
|
||||
await authService.changePassword(req.user.id, req.body, getContext(req));
|
||||
return sendSuccess(res, { message: 'Password change requested. Please confirm the change from the email sent to your account.' }, 200);
|
||||
return sendSuccess(res, { message: 'Cambio de contraseña solicitado. Confirma el cambio desde el correo enviado a tu cuenta.' }, 200);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ const changePassword = async (req, res, next) => {
|
||||
const confirmPasswordChange = async (req, res, next) => {
|
||||
try {
|
||||
await authService.confirmPasswordChange(req.params.token);
|
||||
return sendSuccess(res, { message: 'Password changed successfully.' }, 200);
|
||||
return sendSuccess(res, { message: 'Contraseña cambiada correctamente.' }, 200);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -128,7 +128,7 @@ const requestUnlockPasswordChange = async (req, res, next) => {
|
||||
const verifyEmail = async (req, res, next) => {
|
||||
try {
|
||||
await authService.verifyEmail(req.params.token);
|
||||
return sendSuccess(res, { message: 'Email verified successfully.' }, 200);
|
||||
return sendSuccess(res, { message: 'Correo verificado correctamente.' }, 200);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const { getCertificatePath } = require('../../utils/storagePath');
|
||||
const { signAccess, signRefresh } = require('../../utils/jwt');
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
const env = require('../../config/env');
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
const MAX_FAILED_LOGIN_ATTEMPTS = 3;
|
||||
@@ -157,7 +158,7 @@ const login = async ({ email, password }, context = {}) => {
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
await auditService.logEvent({ type: 'LOGIN_FAILED', email, ip: context.ip, userAgent: context.userAgent });
|
||||
const err = new Error('Invalid credentials.');
|
||||
const err = new Error('Credenciales inválidas.');
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
@@ -210,7 +211,7 @@ const login = async ({ email, password }, context = {}) => {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const err = new Error('Invalid credentials.');
|
||||
const err = new Error('Credenciales inválidas.');
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
@@ -222,7 +223,7 @@ const login = async ({ email, password }, context = {}) => {
|
||||
try {
|
||||
({ verificationUrl } = await issueVerificationEmail(user));
|
||||
} catch (error) {
|
||||
const err = new Error('Your email is not verified and the verification email could not be resent. Please contact support.');
|
||||
const err = new Error('Tu correo no está verificado y no se pudo reenviar el correo de confirmación. Contacta soporte.');
|
||||
err.statusCode = 503;
|
||||
err.code = 'MAIL_SEND_FAILED';
|
||||
throw err;
|
||||
@@ -235,7 +236,7 @@ const login = async ({ email, password }, context = {}) => {
|
||||
userAgent: context.userAgent,
|
||||
metadata: { source: 'login' },
|
||||
});
|
||||
const err = new Error('Your email is not verified yet. A new verification link has been sent to your email.');
|
||||
const err = new Error('Tu correo aún no está verificado. Te enviamos un nuevo enlace de confirmación.');
|
||||
err.statusCode = 403;
|
||||
err.code = 'EMAIL_NOT_VERIFIED';
|
||||
err.data = { verificationUrl };
|
||||
@@ -246,7 +247,7 @@ const login = async ({ email, password }, context = {}) => {
|
||||
const accessToken = signAccess(payload);
|
||||
const refreshToken = signRefresh(payload);
|
||||
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
const expiresAt = new Date(Date.now() + env.jwt.refreshExpiresMs);
|
||||
await authRepository.saveRefreshToken(user.id, refreshToken, expiresAt);
|
||||
|
||||
await auditService.logEvent({ type: 'LOGIN_SUCCESS', userId: user.id, email, ip: context.ip, userAgent: context.userAgent });
|
||||
@@ -268,21 +269,22 @@ const login = async ({ email, password }, context = {}) => {
|
||||
|
||||
const refresh = async (token) => {
|
||||
if (!token) {
|
||||
const err = new Error('Invalid or expired refresh token.');
|
||||
const err = new Error('La sesión expiró. Vuelve a iniciar sesión.');
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const record = await authRepository.findRefreshToken(token);
|
||||
if (!record) {
|
||||
const err = new Error('Invalid or expired refresh token.');
|
||||
await authRepository.deleteRefreshToken(token);
|
||||
const err = new Error('La sesión expiró. Vuelve a iniciar sesión.');
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!record.is_active) {
|
||||
await authRepository.deleteUserRefreshTokens(record.user_id);
|
||||
const err = new Error('Session invalidated.');
|
||||
const err = new Error('La sesión fue invalidada. Vuelve a iniciar sesión.');
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
@@ -292,7 +294,7 @@ const refresh = async (token) => {
|
||||
const payload = { sub: record.uid, email: record.email, role: record.role };
|
||||
const accessToken = signAccess(payload);
|
||||
const newRefreshToken = signRefresh(payload);
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
const expiresAt = new Date(Date.now() + env.jwt.refreshExpiresMs);
|
||||
await authRepository.saveRefreshToken(record.uid, newRefreshToken, expiresAt);
|
||||
|
||||
return { accessToken, refreshToken: newRefreshToken };
|
||||
|
||||
Reference in New Issue
Block a user