feat: upgrade authentication to JWT with refresh tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 01:20:13 +00:00
parent ca7b816c0e
commit 6b5e270984
9 changed files with 245 additions and 146 deletions

View File

@@ -13,11 +13,14 @@
"description": "",
"dependencies": {
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csv-parser": "^3.2.0",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.10",
"pg": "^8.16.3",
"stripe": "^20.1.2",

View File

@@ -1,16 +1,21 @@
const express = require('express');
const app = express();
const cors = require('cors');
const cookieParser = require('cookie-parser');
require("dotenv").config();
const { authMiddleware } = require('./middlewares/authMiddleware');
const corsOptions = {
origin: process.env.URL_CORS, // sin slash al final
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"]
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true
};
app.use(cors(corsOptions));
app.use(express.json());
app.use(cookieParser());
//rutas
const authRoutes = require('./routes/auth.routes');
@@ -30,22 +35,24 @@ const hotelRoutes = require('./routes/hotelpl.routes');
const restaurantRoutes = require('./routes/restaurantpl.routes');
const incomehrxRoutes = require('./routes/incomehrx.routes');
//Prefijo
app.use('/api/employees', employeeRoutes);
app.use('/api/contracts', contractsRoutes);
app.use('/api/reportcontracts', reportcontractRoutes);
app.use('/api/products', productRoutes);
//Prefijo - Auth routes are public (no middleware)
app.use('/api/auth', authRoutes);
app.use('/api/expenses', expenseRoutes);
app.use('/api/status', statusRoutes);
app.use('/api/payment', paymentRoutes);
app.use('/api/settings',settingsRoutes);
app.use('/api/emails',emailRoutes);
app.use('/api/incomes',incomeRoutes);
app.use('/api/incomeshrx',incomehrxRoutes);
app.use('/api/purchases',purchaseRoutes);
app.use('/api/exchange',exchangeRoutes);
app.use('/api/hotelpl',hotelRoutes);
app.use('/api/restaurantpl',restaurantRoutes);
//Prefijo - All other routes are protected with JWT auth middleware
app.use('/api/employees', authMiddleware, employeeRoutes);
app.use('/api/contracts', authMiddleware, contractsRoutes);
app.use('/api/reportcontracts', authMiddleware, reportcontractRoutes);
app.use('/api/products', authMiddleware, productRoutes);
app.use('/api/expenses', authMiddleware, expenseRoutes);
app.use('/api/status', authMiddleware, statusRoutes);
app.use('/api/payment', authMiddleware, paymentRoutes);
app.use('/api/settings', authMiddleware, settingsRoutes);
app.use('/api/emails', authMiddleware, emailRoutes);
app.use('/api/incomes', authMiddleware, incomeRoutes);
app.use('/api/incomeshrx', authMiddleware, incomehrxRoutes);
app.use('/api/purchases', authMiddleware, purchaseRoutes);
app.use('/api/exchange', authMiddleware, exchangeRoutes);
app.use('/api/hotelpl', authMiddleware, hotelRoutes);
app.use('/api/restaurantpl', authMiddleware, restaurantRoutes);
module.exports = app;

View File

@@ -1,14 +1,19 @@
const jwt = require('jsonwebtoken');
const pool = require('../db/connection');
const transporter = require('../services/mailService');
const crypto = require('crypto');
const JWT_SECRET = process.env.JWT_SECRET || 'hotel-system-secret-key-change-in-production';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'hotel-system-refresh-secret-key-change-in-production';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
const login = async (req, res) => {
const { name_mail_user, user_pass } = req.body;
try {
const result = await pool.query('SELECT * from validarusuario($1, $2)', [name_mail_user, user_pass]);
const { status, rol, user_id,user_name } = result.rows[0];
const { status, rol, user_id, user_name } = result.rows[0];
let message = '';
switch (status) {
@@ -16,34 +21,132 @@ const login = async (req, res) => {
message = 'Usuario autenticado correctamente';
break;
case 2:
message = 'Usuario o contraseña incorrectos';
message = 'Usuario o contrasena incorrectos';
break;
default:
message = 'Error desconocido en la validación';
message = 'Error desconocido en la validacion';
}
res.json({
if (status !== 1) {
return res.json({ rol: 0, user_id, user_name, message });
}
// Generate JWT access token
const accessToken = jwt.sign(
{ user_id, user_name, rol },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
// Generate refresh token
const refreshToken = jwt.sign(
{ user_id, user_name, rol },
JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
// Store refresh token in database
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await pool.query(
'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)',
[user_id, refreshToken, expiresAt]
);
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/'
});
res.json({
rol,
user_id,
user_name,
message });
accessToken,
message
});
} catch (error) {
console.error(error);
res.status(500).json({ status: 500, message: 'Error interno del servidor' });
}
};
const refreshTokenHandler = async (req, res) => {
const refreshToken = req.cookies?.refreshToken;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token requerido' });
}
try {
// Verify the refresh token
const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
// Check token exists in DB and is not revoked
const tokenResult = await pool.query(
'SELECT * FROM refresh_tokens WHERE token = $1 AND revoked = false AND expires_at > NOW()',
[refreshToken]
);
if (tokenResult.rows.length === 0) {
return res.status(401).json({ message: 'Refresh token invalido o revocado' });
}
// Issue new access token
const accessToken = jwt.sign(
{ user_id: decoded.user_id, user_name: decoded.user_name, rol: decoded.rol },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
res.json({ accessToken });
} catch (error) {
console.error('Refresh token error:', error);
return res.status(401).json({ message: 'Refresh token invalido' });
}
};
const logoutUser = async (req, res) => {
const refreshToken = req.cookies?.refreshToken;
if (refreshToken) {
try {
// Revoke refresh token in DB
await pool.query(
'UPDATE refresh_tokens SET revoked = true WHERE token = $1',
[refreshToken]
);
} catch (error) {
console.error('Error revoking token:', error);
}
}
// Clear cookie
res.clearCookie('refreshToken', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/'
});
res.json({ message: 'Sesion cerrada correctamente' });
};
const createuser = async (req, res) => {
const { name_user,id_rol, email,user_pass } = req.body;
const { name_user, id_rol, email, user_pass } = req.body;
try {
const result = await pool.query('SELECT createuser($1, $2, $3, $4) AS status', [name_user, id_rol, email, user_pass]);
const status = result.rows[0].status;
res.json({
res.json({
message: "Se agrego el usuario correctamente",
status});
status
});
} catch (error) {
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo agregar al usuario' });
@@ -52,7 +155,7 @@ const createuser = async (req, res) => {
const passRecover = async (req, res) => {
try {
const {user_mail } = req.body;
const { user_mail } = req.body;
const new_pass = crypto
.randomBytes(12)
@@ -75,11 +178,11 @@ const passRecover = async (req, res) => {
const mailOptions = {
from: 'soporte@horuxfin.com',
to: user_mail,
subject: `Nueva contraseña`,
subject: `Nueva contrasena`,
html: `
<h2>Recuperación de contraseña</h2>
<p>Se generó una nueva contraseña para el correo <b>${user_mail}</b></p>
<p><b>Contraseña:</b> ${new_pass}</p>
<h2>Recuperacion de contrasena</h2>
<p>Se genero una nueva contrasena para el correo <b>${user_mail}</b></p>
<p><b>Contrasena:</b> ${new_pass}</p>
<p>Por favor mantenla y borra el correo una vez resguardada.</p>
`,
};
@@ -87,16 +190,16 @@ const passRecover = async (req, res) => {
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con nueva contraseña.',
message: 'Correo enviado con nueva contrasena.',
status
});
} catch (error) {
console.error(error);
res.status(500).json({
message: 'No se pudo mandar el correo de recuperación'
message: 'No se pudo mandar el correo de recuperacion'
});
}
};
module.exports = { login,createuser,passRecover};
module.exports = { login, createuser, passRecover, refreshTokenHandler, logoutUser };

View File

@@ -0,0 +1,24 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'hotel-system-secret-key-change-in-production';
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Token de acceso requerido' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ message: 'Token expirado', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ message: 'Token invalido' });
}
};
module.exports = { authMiddleware, JWT_SECRET };

View File

@@ -4,8 +4,10 @@ const authController = require('../controllers/auth.controller');
const { loginValidator } = require('../middlewares/validators');
const handleValidation = require('../middlewares/handleValidation');
router.post('/login',loginValidator, handleValidation, authController.login);
router.post('/createuser',authController.createuser);
router.post('/recoverpass',authController.passRecover);
router.post('/login', loginValidator, handleValidation, authController.login);
router.post('/createuser', authController.createuser);
router.post('/recoverpass', authController.passRecover);
router.post('/refresh', authController.refreshTokenHandler);
router.post('/logout', authController.logoutUser);
module.exports = router;
module.exports = router;