feat: upgrade authentication to JWT with refresh tokens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
24
backend/hotel_hacienda/src/middlewares/authMiddleware.js
Normal file
24
backend/hotel_hacienda/src/middlewares/authMiddleware.js
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user