diff --git a/.env.example b/.env.example index a0266be..79c035a 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,7 @@ EMAIL_PASS=tu_api_key # Token de Banxico para tipo de cambio BANXICO_TOKEN=tu_token_banxico + +# JWT Authentication +JWT_SECRET=change-this-secret-in-production +JWT_REFRESH_SECRET=change-this-refresh-secret-in-production diff --git a/backend/hotel_hacienda/package.json b/backend/hotel_hacienda/package.json index 9611451..32d595e 100644 --- a/backend/hotel_hacienda/package.json +++ b/backend/hotel_hacienda/package.json @@ -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", diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js index 7a9762e..96f2e8d 100644 --- a/backend/hotel_hacienda/src/app.js +++ b/backend/hotel_hacienda/src/app.js @@ -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; diff --git a/backend/hotel_hacienda/src/controllers/auth.controller.js b/backend/hotel_hacienda/src/controllers/auth.controller.js index 626578e..be37854 100644 --- a/backend/hotel_hacienda/src/controllers/auth.controller.js +++ b/backend/hotel_hacienda/src/controllers/auth.controller.js @@ -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: ` -
Se generó una nueva contraseña para el correo ${user_mail}
-Contraseña: ${new_pass}
+Se genero una nueva contrasena para el correo ${user_mail}
+Contrasena: ${new_pass}
Por favor mantenla y borra el correo una vez resguardada.
`, }; @@ -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 }; diff --git a/backend/hotel_hacienda/src/middlewares/authMiddleware.js b/backend/hotel_hacienda/src/middlewares/authMiddleware.js new file mode 100644 index 0000000..f707b17 --- /dev/null +++ b/backend/hotel_hacienda/src/middlewares/authMiddleware.js @@ -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 }; diff --git a/backend/hotel_hacienda/src/routes/auth.routes.js b/backend/hotel_hacienda/src/routes/auth.routes.js index 757ffda..52548dd 100644 --- a/backend/hotel_hacienda/src/routes/auth.routes.js +++ b/backend/hotel_hacienda/src/routes/auth.routes.js @@ -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; \ No newline at end of file +module.exports = router; diff --git a/frontend/Frontend-Hotel/src/context/AuthContext.jsx b/frontend/Frontend-Hotel/src/context/AuthContext.jsx index 115a608..042a3ad 100644 --- a/frontend/Frontend-Hotel/src/context/AuthContext.jsx +++ b/frontend/Frontend-Hotel/src/context/AuthContext.jsx @@ -1,85 +1,54 @@ -// context/AuthContext.jsx import { createContext, useState, useEffect } from "react"; +import api from "../services/api"; export const AuthContext = createContext(); function AuthProvider({ children }) { - const [user, setUser] = useState(null); // { id, name, role } + const [user, setUser] = useState(null); const [userData, setUserData] = useState(null); - - // Recuperar user de localStorage al iniciar + const [loading, setLoading] = useState(true); + useEffect(() => { - const savedUser = localStorage.getItem("rol"); - if (savedUser) { + const token = sessionStorage.getItem("accessToken"); + const savedUser = sessionStorage.getItem("userRole"); + const savedUserData = sessionStorage.getItem("userData"); + if (token && savedUser) { try { setUser(JSON.parse(savedUser)); - } catch (error) { - console.error("Error parsing user data:", error); - localStorage.removeItem("rol"); + if (savedUserData) setUserData(JSON.parse(savedUserData)); + } catch (e) { + console.error("Error restoring session:", e); } } + setLoading(false); }, []); - const login = (userData, data) => { + const login = (role, data, accessToken) => { + setUser(role); setUserData(data); - setUser(userData); - localStorage.setItem("rol", JSON.stringify(userData)); + sessionStorage.setItem("accessToken", accessToken); + sessionStorage.setItem("userRole", JSON.stringify(role)); + sessionStorage.setItem("userData", JSON.stringify(data)); }; - const logout = () => { + const logout = async () => { + try { + await api.post("/auth/logout"); + } catch (e) { + // ignore logout errors + } setUser(null); - localStorage.removeItem("rol"); + setUserData(null); + sessionStorage.removeItem("accessToken"); + sessionStorage.removeItem("userRole"); + sessionStorage.removeItem("userData"); }; return ( -