From 014ac23192f8d81404ad81e02a7d890027544b74 Mon Sep 17 00:00:00 2001 From: AgustinRodriguez-Andes <63318331+agustin1996ra@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:10:14 -0300 Subject: [PATCH] =?UTF-8?q?REC-208:=20Vencimiento=20de=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda-ui.ts | 5 +- src/agenda/agenda.service.ts | 1 - src/config/config.ts | 13 +- src/controllers/auth.controller.ts | 1012 +++++++++-------- src/database/dbconfig.ts | 3 +- src/jobs/user-migration/index.ts | 3 +- .../passport-config-andes.middleware.ts | 67 +- src/middlewares/passport-config.middleware.ts | 86 +- src/middlewares/roles.middleware.ts | 140 +-- src/migration-script/index.ts | 3 +- .../rename-prescriptions-collection.ts | 3 +- src/scripts/patient-ANDES/index.ts | 3 +- src/server.ts | 10 +- src/utils/session-timeout.ts | 26 + 14 files changed, 726 insertions(+), 649 deletions(-) create mode 100644 src/utils/session-timeout.ts diff --git a/src/agenda-ui.ts b/src/agenda-ui.ts index cdd718a2..c55266d3 100644 --- a/src/agenda-ui.ts +++ b/src/agenda-ui.ts @@ -1,9 +1,12 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + import express from 'express'; import Agenda from 'agenda'; import jobsRoutes from './routes/jobs'; import AgendaService from './agenda/agenda.service'; import * as db from './database/dbconfig'; -import './config/config'; // Importación dinámica de Agendash const Agendash = require('agendash'); diff --git a/src/agenda/agenda.service.ts b/src/agenda/agenda.service.ts index c1a2d526..d098852e 100644 --- a/src/agenda/agenda.service.ts +++ b/src/agenda/agenda.service.ts @@ -1,5 +1,4 @@ import Agenda, { Job } from 'agenda'; -import { env } from '../config/config'; import mongoose from 'mongoose'; import testJob from './jobs/testJob'; import sendPrescriptions from './jobs/sendPrecriptions'; diff --git a/src/config/config.ts b/src/config/config.ts index 0a9f3ce6..fc9d5a51 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,14 +1,3 @@ -import dotenv from 'dotenv'; - -dotenv.config(); - -export const env = { - API_URI_PREFIX: '/api', - JWT_SECRET: 'e18a33b0-9866-4867-800a-d6ffcd8f1cbd', - TOKEN_LIFETIME: 1, - MONGODB_CONNECTION: 'mongodb://localhost/recetar' -}; - export const httpCodes = { UNAUTHORIZED: 401, FORBIDDEN: 403, @@ -19,4 +8,4 @@ export const httpCodes = { INTERNAL_SERVER_ERROR: 500, OK: 200, NOT_FOUND: 404, - }; +}; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index f91c62f6..7cf9fb0f 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as JWT from 'jsonwebtoken'; -import { env, httpCodes } from '../config/config'; +import { httpCodes } from '../config/config'; import { v4 as uuidv4 } from 'uuid'; import IUser from '../interfaces/user.interface'; import User from '../models/user.model'; @@ -9,548 +9,554 @@ import Role from '../models/role.model'; import IProfesionAutorizada from '../interfaces/profesionAutorizada.interface'; import ProfesionAutorizada from '../models/profesionAutorizada.model'; import { renderHTML, MailOptions, sendMail } from '../utils/roboSender/sendEmail'; +import { isSessionExpired, SESSION_EXPIRED_MESSAGE } from '../utils/session-timeout'; import needle from 'needle'; import moment from 'moment'; class AuthController { - public register = async (req: Request, res: Response): Promise => { - try { - const { username, email, enrollment, cuil, businessName, password, roleType, captcha, profesion, fechaEgreso, fechaMatVencimiento } = req.body; + public register = async (req: Request, res: Response): Promise => { + try { + const { username, email, enrollment, cuil, businessName, password, roleType, captcha, profesion, fechaEgreso, fechaMatVencimiento } = req.body; + + // Verificación Token captcha + if (!captcha) { return res.status(403).json({ message: 'Body incompleto' }); } + const captchaResp: any = await needle('post', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { secret: process.env.CF_SECRET_KEY, response: captcha }); + if (!captchaResp || captchaResp.body.success === false) { return res.status(403).json('Conexión invalida'); } + + // Verificación de rol + const role: IRole | null = await Role.findOne({ role: roleType }); + if (!role) { return res.status(400).json({ message: 'No es posible registrar el usuario' }); } + + // Verificación de usuario ya registrado + const posibleExistingUser: IUser | null = await User.findOne({ username }); + if (posibleExistingUser) { return res.status(400).json({ message: 'No es posible registrar, el usuario ya existe' }); } + + if (roleType === 'professional') { + const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/profesionales/guia?documento=${username}`); + if (!(resp.body && resp.body.length > 0 && resp.body[0].profesiones && resp.body[0].profesiones.length > 0)) { + return res.status(400).json({ message: 'No se encuentra el profesional.' }); + } + const professionalAndes = resp.body[0]; + const { profesiones } = professionalAndes; + const profAut = profesiones.filter((p: any) => { + const validCodes = [1, 2, 23]; + if (!validCodes.includes(p.profesion.codigo)) { return false; } + const lastMatriculacion = p.matriculacion[p.matriculacion.length - 1]; + return lastMatriculacion && moment(lastMatriculacion.fin) > moment(); + }); + if (!profAut) { + return res.status(400).json({ message: 'No es posible registrar, profesional sin matricula válida' }); + } + + const matchesProfesional = profAut.some((p: any) => { + const lastMat = p.matriculacion && p.matriculacion.length + ? p.matriculacion[p.matriculacion.length - 1] + : null; + const codigoMatches = p.profesion && profesion && p.profesion.codigo.toString() === profesion.codigoProfesion; + const fechaEgresoMatches = moment(fechaEgreso).startOf('day').isSame( + moment(p.fechaEgreso).startOf('day') + ); + const fechaMatVencimientoMatches = lastMat && moment(fechaMatVencimiento).isSame(moment(lastMat.fin), 'day'); + const matriculaMatches = lastMat && lastMat.matriculaNumero && enrollment + ? lastMat.matriculaNumero.toString() === enrollment.toString() + : false; + return codigoMatches && matriculaMatches && fechaEgresoMatches && fechaMatVencimientoMatches; + }); + + if (!matchesProfesional) { + return res.status(400).json({ message: 'No es posible registrar, los datos del profesional no coinciden' }); + } + + if (profAut && cuil === professionalAndes.cuit) { + const apellidoYNombre = `${professionalAndes.apellido} ${professionalAndes.nombre}`; + const profesionG = profAut.map((p: any) => ({ profesion: p.profesion.nombre, codigoProfesion: p.profesion.codigo, numeroMatricula: p.matriculacion[p.matriculacion.length - 1].matriculaNumero })); + const lastProfesion = profesiones.find((p: any) => ['1', '23', '2'].includes(String(p.profesion.codigo))); + const lastMatriculacion = lastProfesion.matriculacion[lastProfesion.matriculacion.length - 1]; + + if (lastMatriculacion && (moment(lastMatriculacion.fin)) > moment() && lastMatriculacion.matriculaNumero.toString() === enrollment && cuil === professionalAndes.cuit) { + const newUser = new User({ username, email, password, enrollment, cuil, businessName: apellidoYNombre, profesionGrado: profesionG, isActive: true }); + + newUser.roles.push(role); + role.users.push(newUser); + await newUser.save(); + await role.save(); + this.sendEmailNewUser(newUser); + return res.status(200).json({ + newUser + }); + } + } else { + return res.status(400).json({ message: 'No es posible registrar, el CUIT/CUIL del profesional no coincide' }); + } + } else if (roleType === 'pharmacist') { + const { disposicionHabilitacion, vencimientoHabilitacion } = req.body; + const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/farmacias?cuit=${username}&disposicionHabilitacion=${disposicionHabilitacion}`, { headers: { Authorization: process.env.JWT_MPI_TOKEN } }); + + if (!(resp.body && resp.body.length > 0)) { + return res.status(400).json({ message: 'No se encuentra el farmacia.' }); + } + const pharmacyAndes = resp.body[0]; + const checkDisposicionFarmacia = pharmacyAndes.disposicionHabilitacion === disposicionHabilitacion ? true : false; + const checkMatricula = pharmacyAndes.matriculaDTResponsable === enrollment ? true : false; + const diferencia = moment(vencimientoHabilitacion).diff(pharmacyAndes.vencimientoHabilitacion, 'days'); + if (checkDisposicionFarmacia && checkMatricula && diferencia === 0) { + const newUser = new User({ username, email, password, enrollment, cuil, businessName, isActive: true }); + newUser.roles.push(role); + role.users.push(newUser); + await newUser.save(); + await role.save(); + this.sendEmailNewUser(newUser); + return res.status(200).json({ + newUser + }); + } + } + return res.status(403).json('No se puede registrar el usuario'); + } catch (errors) { + return res.status(422).json({ errors }); + } + }; - // Verificación Token captcha - if (!captcha) { return res.status(403).json({ message: 'Body incompleto' }); } - const captchaResp: any = await needle('post', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { secret: process.env.CF_SECRET_KEY, response: captcha }); - if (!captchaResp || captchaResp.body.success === false) { return res.status(403).json('Conexión invalida'); } + public resetPassword = async (req: Request, res: Response): Promise => { + const { _id } = req.user as IUser; + const { oldPassword, newPassword } = req.body; + try { + const user: IUser | null = await User.findOne({ _id }); + if (!user) {return res.status(404).json('No se ha encontrado el usuario');} - // Verificación de rol - const role: IRole | null = await Role.findOne({ role: roleType }); - if (!role) { return res.status(400).json({ message: 'No es posible registrar el usuario' }); } + const isMatch: boolean = await User.schema.methods.isValidPassword(user, oldPassword); + if (!isMatch) {return res.status(401).json({ message: 'Su contraseña actual no coincide con nuestros registros' });} - // Verificación de usuario ya registrado - const posibleExistingUser: IUser | null = await User.findOne({ username }); - if (posibleExistingUser) { return res.status(400).json({ message: 'No es posible registrar, el usuario ya existe' }); } - if (roleType === 'professional') { - const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/profesionales/guia?documento=${username}`); - if (!(resp.body && resp.body.length > 0 && resp.body[0].profesiones && resp.body[0].profesiones.length > 0)) { - return res.status(400).json({ message: 'No se encuentra el profesional.' }); - } - const professionalAndes = resp.body[0]; - const { profesiones } = professionalAndes; - const profAut = profesiones.filter((p: any) => { - const validCodes = [1, 2, 23]; - if (!validCodes.includes(p.profesion.codigo)) { return false; } - const lastMatriculacion = p.matriculacion[p.matriculacion.length - 1]; - return lastMatriculacion && moment(lastMatriculacion.fin) > moment(); - }); - if (!profAut) { - return res.status(400).json({ message: 'No es posible registrar, profesional sin matricula válida' }); - } + // Actualizar contraseña y fechas de vencimiento + const now = moment(); - const matchesProfesional = profAut.some((p: any) => { - const lastMat = p.matriculacion && p.matriculacion.length - ? p.matriculacion[p.matriculacion.length - 1] - : null; - const codigoMatches = p.profesion && profesion && p.profesion.codigo.toString() === profesion.codigoProfesion; - const fechaEgresoMatches = moment(fechaEgreso).startOf('day').isSame( - moment(p.fechaEgreso).startOf('day') - ); - const fechaMatVencimientoMatches = lastMat && moment(fechaMatVencimiento).isSame(moment(lastMat.fin), 'day'); - const matriculaMatches = lastMat && lastMat.matriculaNumero && enrollment - ? lastMat.matriculaNumero.toString() === enrollment.toString() - : false; - return codigoMatches && matriculaMatches && fechaEgresoMatches && fechaMatVencimientoMatches; - }); + await user.update({ + password: newPassword, + passwordCreatedAt: now, + authenticationToken: null, + passwordChangeTokenExpiry: null + }); - if (!matchesProfesional) { - return res.status(400).json({ message: 'No es posible registrar, los datos del profesional no coinciden' }); + return res.status(200).json('Se ha modificado la contraseña correctamente!'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); } + }; - if (profAut && cuil === professionalAndes.cuit) { - const apellidoYNombre = `${professionalAndes.apellido} ${professionalAndes.nombre}`; - const profesionG = profAut.map((p: any) => ({ profesion: p.profesion.nombre, codigoProfesion: p.profesion.codigo, numeroMatricula: p.matriculacion[p.matriculacion.length - 1].matriculaNumero })); - const lastProfesion = profesiones.find((p: any) => p.profesion.codigo == '1' || p.profesion.codigo == '23' || p.profesion.codigo == '2'); - const lastMatriculacion = lastProfesion.matriculacion[lastProfesion.matriculacion.length - 1]; - - if (lastMatriculacion && (moment(lastMatriculacion.fin)) > moment() && lastMatriculacion.matriculaNumero.toString() === enrollment && cuil === professionalAndes.cuit) { - const newUser = new User({ username, email, password, enrollment, cuil, businessName: apellidoYNombre, profesionGrado: profesionG, isActive: true }); - - newUser.roles.push(role); - role.users.push(newUser); - await newUser.save(); - await role.save(); - this.sendEmailNewUser(newUser); - return res.status(200).json({ - newUser + public recoverPassword = async (req: Request, res: Response): Promise => { + const { authenticationToken, newPassword } = req.body; + try { + const errorMessage = 'El link al que ha ingresado es inválido o ha expirado'; + const user: IUser | null = await User.findOne({ authenticationToken }); + + if (!user) {return res.status(404).json({ message: errorMessage });} + + if (!user || moment(user.passwordChangeTokenExpiry).isBefore(moment())) { + return res.status(400).json({ message: errorMessage }); + } + + const isMatch: boolean = await User.schema.methods.isValidPassword(user, newPassword); + if (isMatch) {return res.status(400).json({ message: 'La nueva contraseña debe ser distinta a la utilizada anteriormente.' });} + + // Actualizar contraseña y fechas de vencimiento + const now = moment(); + + await user.update({ + password: newPassword, + passwordCreatedAt: now, + authenticationToken: null, + passwordChangeTokenExpiry: null }); - } - } else { - return res.status(400).json({ message: 'No es posible registrar, el CUIT/CUIL del profesional no coincide' }); - } - } else if (roleType === 'pharmacist') { - const { disposicionHabilitacion, vencimientoHabilitacion } = req.body; - const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/farmacias?cuit=${username}&disposicionHabilitacion=${disposicionHabilitacion}`, { headers: { Authorization: process.env.JWT_MPI_TOKEN } }); - if (!(resp.body && resp.body.length > 0)) { - return res.status(400).json({ message: 'No se encuentra el farmacia.' }); + return res.status(200).json('Se ha modificado la contraseña correctamente.'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('No es posible modificar la contraseña.'); } - const pharmacyAndes = resp.body[0]; - const checkDisposicionFarmacia = pharmacyAndes.disposicionHabilitacion === disposicionHabilitacion ? true : false; - const checkMatricula = pharmacyAndes.matriculaDTResponsable === enrollment ? true : false; - const diferencia = moment(vencimientoHabilitacion).diff(pharmacyAndes.vencimientoHabilitacion, 'days'); - if (checkDisposicionFarmacia && checkMatricula && diferencia === 0) { - const newUser = new User({ username, email, password, enrollment, cuil, businessName, isActive: true }); - newUser.roles.push(role); - role.users.push(newUser); - await newUser.save(); - await role.save(); - this.sendEmailNewUser(newUser); - return res.status(200).json({ - newUser - }); + }; + + public login = async (req: Request, res: Response): Promise => { + const { _id } = req.user as IUser; + try { + + const user: IUser | null = await User.findOne({ _id }).populate({ path: 'roles', select: 'role' }); + + if (user && user.isActive) { + // Verificar si la contraseña está vencida + const now = moment(); + + if (!user.passwordCreatedAt) { + user.passwordCreatedAt = now.toDate(); + await user.save(); + } + + const passwordExpired = moment(user.passwordCreatedAt).add(3, 'months').isBefore(moment()); + + if (passwordExpired) { + // Enviar correo de cambio de contraseña + await this.sendPasswordExpiryNotification(user.username); + + return res.status(401).json({ + message: 'Su contraseña ha vencido. Se ha enviado un correo electrónico con instrucciones para cambiarla.' + }); + } + + const roles: string | string[] = []; + await Promise.all(user.roles.map(async (role) => { + roles.push(role.role); + })); + const token = this.signInToken(user._id, user.username, user.businessName, roles); + const refreshToken = uuidv4(); + + await User.updateOne({ _id: user._id }, { refreshToken, lastLogin: now }); + return res.status(200).json({ + jwt: token, + refreshToken + }); + } + + return res.status(httpCodes.EXPECTATION_FAILED).json('Debe iniciar sesión'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); } - } - return res.status(403).json('No se puede registrar el usuario'); - } catch (errors) { - return res.status(422).json({ errors }); - } - }; - - public resetPassword = async (req: Request, res: Response): Promise => { - const { _id } = req.user as IUser; - const { oldPassword, newPassword } = req.body; - try { - const user: IUser | null = await User.findOne({ _id }); - if (!user) return res.status(404).json('No se ha encontrado el usuario'); - - const isMatch: boolean = await User.schema.methods.isValidPassword(user, oldPassword); - if (!isMatch) return res.status(401).json({ message: 'Su contraseña actual no coincide con nuestros registros' }); - - - - // Actualizar contraseña y fechas de vencimiento - const now = moment(); - - await user.update({ - password: newPassword, - passwordCreatedAt: now, - authenticationToken: null, - passwordChangeTokenExpiry: null - }); - - return res.status(200).json('Se ha modificado la contraseña correctamente!'); - } catch (err) { - console.log(err); - return res.status(500).json('Server Error'); - } - } - - public recoverPassword = async (req: Request, res: Response): Promise => { - const { authenticationToken, newPassword } = req.body; - try { - const errorMessage = "El link al que ha ingresado es inválido o ha expirado"; - const user: IUser | null = await User.findOne({ authenticationToken: authenticationToken }); - - if (!user) return res.status(404).json({ message: errorMessage }); - - if (!user || moment(user.passwordChangeTokenExpiry).isBefore(moment())) { - return res.status(400).json({ message: errorMessage }); - } - - const isMatch: boolean = await User.schema.methods.isValidPassword(user, newPassword); - if (isMatch) return res.status(400).json({ message: 'La nueva contraseña debe ser distinta a la utilizada anteriormente.' }); - - // Actualizar contraseña y fechas de vencimiento - const now = moment(); - - await user.update({ - password: newPassword, - passwordCreatedAt: now, - authenticationToken: null, - passwordChangeTokenExpiry: null - }); - - return res.status(200).json('Se ha modificado la contraseña correctamente.'); - } catch (err) { - console.log(err); - return res.status(500).json('No es posible modificar la contraseña.'); - } - } - - public login = async (req: Request, res: Response): Promise => { - const { _id } = req.user as IUser; - try { - - const user: IUser | null = await User.findOne({ _id }).populate({ path: 'roles', select: 'role' }); - - if (user && user.isActive) { - // Verificar si la contraseña está vencida - const now = moment(); - - if (!user.passwordCreatedAt) { - user.passwordCreatedAt = now.toDate(); - await user.save(); + }; + + public logout = async (req: Request, res: Response): Promise => { + const { refreshToken } = req.body; + try { + await User.findOneAndUpdate({ refreshToken }, { refreshToken: '' }); + return res.status(204).json('Logged out successfully!'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server error'); } + }; - const passwordExpired = moment(user.passwordCreatedAt).add(3, 'months').isBefore(moment()); + public refresh = async (req: Request, res: Response): Promise => { + const refreshToken = req.body.refreshToken; + try { + const user: IUser | null = await User.findOne({ refreshToken }).populate({ path: 'roles', select: 'role' }); + + if (user) { + // in next version, should embed roles information + const roles: string | string[] = []; + await Promise.all(user.roles.map(async (role) => { + roles.push(role.role); + })); + + const token = this.signInToken(user._id, user.username, user.businessName, roles); + + // generate a new refresh_token + const nextRefreshToken = uuidv4(); + await User.updateOne({ _id: user._id }, { refreshToken: nextRefreshToken }); + return res.status(200).json({ + jwt: token, + refreshToken: nextRefreshToken + }); + } + + return res.status(httpCodes.EXPECTATION_FAILED).json('Debe iniciar sesión');// in the case that not found user + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server error'); + } - if (passwordExpired) { - // Enviar correo de cambio de contraseña - await this.sendPasswordExpiryNotification(user.username); + }; - return res.status(401).json({ - message: 'Su contraseña ha vencido. Se ha enviado un correo electrónico con instrucciones para cambiarla.' - }); + public updateUser = async (req: Request, res: Response): Promise => { + const { id } = req.params; + const values: any = {}; + try { + + if (typeof req.body.email !== 'undefined') { values.email = req.body.email; } + if (typeof req.body.username !== 'undefined') { values.username = req.body.username; } + if (typeof req.body.enrollment !== 'undefined') { values.enrollment = req.body.enrollment; } + if (typeof req.body.cuil !== 'undefined') { values.cuil = req.body.cuil; } + if (typeof req.body.businessName !== 'undefined') { values.businessName = req.body.businessName; } + if (typeof req.body.password !== 'undefined') { + values.password = req.body.password; + values.passwordCreatedAt = moment(); + } + + const opts: any = { runValidators: true, new: true, context: 'query' }; + const user: IUser | null = await User.findOneAndUpdate({ _id: id }, values, opts).select('username email cuil enrollment businessName'); + + return res.status(200).json(user); + } catch (e) { + // formateamos los errores de validacion + if (e.name !== 'undefined' && e.name === 'ValidationError') { + const errors: { [key: string]: string } = {}; + Object.keys(e.errors).forEach(prop => { + errors[prop] = e.errors[prop].message; + }); + return res.status(422).json(errors); + } + // eslint-disable-next-line no-console + console.log(e); + return res.status(500).json('Server Error'); } + }; - const roles: string | string[] = []; - await Promise.all(user.roles.map(async (role) => { - roles.push(role.role); - })); - const token = this.signInToken(user._id, user.username, user.businessName, roles); - const refreshToken = uuidv4(); - - await User.updateOne({ _id: user._id }, { refreshToken: refreshToken, lastLogin: now }); - return res.status(200).json({ - jwt: token, - refreshToken: refreshToken - }); - } - - return res.status(httpCodes.EXPECTATION_FAILED).json('Debe iniciar sesión'); - } catch (err) { - console.log(err); - return res.status(500).json('Server Error'); - } - } - - public logout = async (req: Request, res: Response): Promise => { - const { refreshToken } = req.body; - try { - await User.findOneAndUpdate({ refreshToken }, { refreshToken: '' }); - return res.status(204).json('Logged out successfully!'); - } catch (err) { - // eslint-disable-next-line no-console - console.log(err); - return res.status(500).json('Server error'); - } - }; - - public refresh = async (req: Request, res: Response): Promise => { - const refreshToken = req.body.refreshToken; - try { - const user: IUser | null = await User.findOne({ refreshToken: refreshToken }).populate({ path: 'roles', select: 'role' }); - - if (user) { - // in next version, should embed roles information - const roles: string | string[] = []; - await Promise.all(user.roles.map(async (role) => { - roles.push(role.role); - })); - - const token = this.signInToken(user._id, user.username, user.businessName, roles); - - // generate a new refresh_token - const refreshToken = uuidv4(); - await User.updateOne({ _id: user._id }, { refreshToken: refreshToken }); - return res.status(200).json({ - jwt: token, - refreshToken: refreshToken - }); - } - - return res.status(httpCodes.EXPECTATION_FAILED).json('Debe iniciar sesión');//in the case that not found user - } catch (err) { - console.log(err); - return res.status(500).json('Server error'); - } - - } - - public updateUser = async (req: Request, res: Response): Promise => { - const { id } = req.params; - const values: any = {}; - try { - - if (typeof req.body.email !== 'undefined') { values.email = req.body.email; } - if (typeof req.body.username !== 'undefined') { values.username = req.body.username; } - if (typeof req.body.enrollment !== 'undefined') { values.enrollment = req.body.enrollment; } - if (typeof req.body.cuil !== 'undefined') { values.cuil = req.body.cuil; } - if (typeof req.body.businessName !== 'undefined') { values.businessName = req.body.businessName; } - if (typeof req.body.password !== 'undefined') { - values.password = req.body.password; - values.passwordCreatedAt = moment(); - } - - const opts: any = { runValidators: true, new: true, context: 'query' }; - const user: IUser | null = await User.findOneAndUpdate({ _id: id }, values, opts).select('username email cuil enrollment businessName'); - - return res.status(200).json(user); - } catch (e) { - // formateamos los errores de validacion - if (e.name !== 'undefined' && e.name === 'ValidationError') { - const errors: { [key: string]: string } = {}; - Object.keys(e.errors).forEach(prop => { - errors[prop] = e.errors[prop].message; - }); - return res.status(422).json(errors); - } - // eslint-disable-next-line no-console - console.log(e); - return res.status(500).json('Server Error'); - } - }; - - public getUser = async (req: Request, res: Response): Promise => { + public getUser = async (req: Request, res: Response): Promise => { // obtenemos los datos del usuario, buscando por: "email" / "username" / "cuil" - const { email, username, cuil } = req.body; - try { - const users: IUser[] | null = await User.find({ - $or: [{ email }, { username }, { cuil }] - }).select('username email cuil enrollment, businessName'); - - if (!users) { return res.status(400).json('Usuario no encontrado'); } - - return res.status(200).json(users); - } catch (err) { - // eslint-disable-next-line no-console - console.log(err); - return res.status(500).json('Server Error'); - } - }; - - public assignRole = async (req: Request, res: Response): Promise => { - const { id } = req.params; - const { roleId } = req.body; - try { - const role: IRole | null = await Role.findOne({ _id: roleId }); - if (role) { - await User.findByIdAndUpdate({ _id: id }, { - roles: role + const { email, username, cuil } = req.body; + try { + const users: IUser[] | null = await User.find({ + $or: [{ email }, { username }, { cuil }] + }).select('username email cuil enrollment, businessName'); + + if (!users) { return res.status(400).json('Usuario no encontrado'); } + + return res.status(200).json(users); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); + } + }; + + public assignRole = async (req: Request, res: Response): Promise => { + const { id } = req.params; + const { roleId } = req.body; + try { + const role: IRole | null = await Role.findOne({ _id: roleId }); + if (role) { + await User.findByIdAndUpdate({ _id: id }, { + roles: role + }); + } + const user: IUser | null = await User.findOne({ _id: id }); + return res.status(200).json(user); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); + } + }; + + /* modificar toke */ + public getToken = async (req: Request, res: Response): Promise => { + const { username } = req.body; + + try { + const user: IUser | null = await User.findOne({ username }).populate({ path: 'roles', select: 'role' }); + if (!user) { + return res.status(422).json({ message: 'Usuario no encontrado.' }); + } + // in next version, should embed roles information + const roles: string | string[] = []; + await Promise.all(user.roles.map(async (role) => { + roles.push(role.role); + })); + + const token = JWT.sign({ + iss: 'recetar.andes', + sub: user._id, + usrn: user.username, + bsname: user.businessName, + rl: roles, + iat: new Date().getTime() + }, (process.env.JWT_SECRET || ''), { + algorithm: 'HS256' + }); + return res.status(200).json({ jwt: token }); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); + } + }; + + private signInToken = (userId: string, username: string, businessName: string, role: string | string[]): any => { + const now = moment().unix(); + const token = JWT.sign({ + iss: 'recetar.andes', + sub: userId, + usrn: username, + bsname: businessName, + rl: role, + iat: now, + exp: now + moment.duration(Number(process.env.TOKEN_LIFETIME || 12), 'hours').asSeconds() + }, (process.env.JWT_SECRET || ''), { + algorithm: 'HS256' }); - } - const user: IUser | null = await User.findOne({ _id: id }); - return res.status(200).json(user); - } catch (err) { - // eslint-disable-next-line no-console - console.log(err); - return res.status(500).json('Server Error'); - } - }; - - /* modificar toke */ - public getToken = async (req: Request, res: Response): Promise => { - const { username } = req.body; - - try { - const user: IUser | null = await User.findOne({ username }).populate({ path: 'roles', select: 'role' }); - if (!user) { - return res.status(422).json({ message: 'Usuario no encontrado.' }); - } - // in next version, should embed roles information - const roles: string | string[] = []; - await Promise.all(user.roles.map(async (role) => { - roles.push(role.role); - })); - - const token = JWT.sign({ - iss: 'recetar.andes', - sub: user._id, - usrn: user.username, - bsname: user.businessName, - rl: roles, - iat: new Date().getTime() - }, (process.env.JWT_SECRET || env.JWT_SECRET), { - algorithm: 'HS256' - }); - return res.status(200).json({ jwt: token }); - } catch (err) { - // eslint-disable-next-line no-console - console.log(err); - return res.status(500).json('Server Error'); - } - }; - - private signInToken = (userId: string, username: string, businessName: string, role: string | string[]): any => { - const now = moment().unix(); - const token = JWT.sign({ - iss: "recetar.andes", - sub: userId, - usrn: username, - bsname: businessName, - rl: role, - iat: now, - exp: now + moment.duration(env.TOKEN_LIFETIME, 'hours').asSeconds() - }, (process.env.JWT_SECRET || env.JWT_SECRET), { - algorithm: 'HS256' - }); - return token; - } - - /** + return token; + }; + + /** * Envía un link para recuperar la contraseña en caso qeu sea un usuario temporal con email (fuera de onelogin). * AuthUser */ - public setValidationTokenAndNotify = async (username: string) => { - try { - let usuario: any = await User.findOne({ username }); - if (usuario) { - usuario.authenticationToken = uuidv4(); - usuario.passwordChangeTokenExpiry = null; - - await usuario.save(); + public setValidationTokenAndNotify = async (username: string) => { + try { + const usuario: any = await User.findOne({ username }); + if (usuario) { + usuario.authenticationToken = uuidv4(); + usuario.passwordChangeTokenExpiry = null; + + await usuario.save(); + + const extras: any = { + titulo: 'Recuperación de contraseña', + usuario, + nombre: usuario.businessName || usuario.username, + url: `${process.env.APP_DOMAIN}/auth/recovery-password/${usuario.authenticationToken}`, + }; + const htmlToSend = await renderHTML('emails/recover-password.html', extras); + const options: MailOptions = { + from: `${process.env.EMAIL_USERNAME}`, + to: usuario.email.toString(), + subject: 'Recuperación de contraseña', + text: '', + html: htmlToSend, + attachments: null + }; + + await sendMail(options); + return usuario; + } else { + return null; + } + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + throw err; + } + }; + public sendEmailNewUser = async (newUser: any) => { const extras: any = { - titulo: 'Recuperación de contraseña', - usuario, - nombre: usuario.businessName || usuario.username, - url: `${process.env.APP_DOMAIN}/auth/recovery-password/${usuario.authenticationToken}`, + titulo: 'Nuevo usuario', + usuario: newUser, + nombre: newUser.businessName || newUser.username, + username: newUser.username, }; - const htmlToSend = await renderHTML('emails/recover-password.html', extras); + const htmlToSend = await renderHTML('emails/new-user.html', extras); const options: MailOptions = { - from: `${process.env.EMAIL_USERNAME}`, - to: usuario.email.toString(), - subject: 'Recuperación de contraseña', - text: '', - html: htmlToSend, - attachments: null + from: `${process.env.EMAIL_HOST}`, + to: newUser.email.toString(), + subject: 'Nuevo Usuario RecetAR', + text: '', + html: htmlToSend, + attachments: null }; - await sendMail(options); - return usuario; - } else { - return null; - } - } catch (err) { - console.log(err); - throw err; - } - } - - public sendEmailNewUser = async (newUser: any) => { - const extras: any = { - titulo: 'Nuevo usuario', - usuario: newUser, - nombre: newUser.businessName || newUser.username, - username: newUser.username, }; - const htmlToSend = await renderHTML('emails/new-user.html', extras); - const options: MailOptions = { - from: `${process.env.EMAIL_HOST}`, - to: newUser.email.toString(), - subject: 'Nuevo Usuario RecetAR', - text: '', - html: htmlToSend, - attachments: null - }; - await sendMail(options); - } - public sendPasswordExpiryNotification = async (username: string) => { - try { - let usuario: any = await User.findOne({ username }); - if (usuario) { - // Generar token de cambio de contraseña con vencimiento de 24 horas - const authenticationToken = uuidv4(); - const tokenExpiry = moment().endOf('day').add(72, 'hours').toDate(); + public sendPasswordExpiryNotification = async (username: string) => { + try { + const usuario: any = await User.findOne({ username }); + if (usuario) { + // Generar token de cambio de contraseña con vencimiento de 24 horas + const authenticationToken = uuidv4(); + const tokenExpiry = moment().endOf('day').add(72, 'hours').toDate(); + + usuario.authenticationToken = authenticationToken; + usuario.passwordChangeTokenExpiry = tokenExpiry; + await usuario.save(); + + const extras: any = { + titulo: 'Contraseña Vencida - Cambio Requerido', + nombre: usuario.businessName || usuario.username, + url: `${process.env.APP_DOMAIN}/auth/recovery-password/${authenticationToken}`, + expiryDate: moment(tokenExpiry).format('DD/MM/YYYY').toString() + }; + + const htmlToSend = await renderHTML('emails/password-expired.html', extras); + const options: MailOptions = { + from: `${process.env.EMAIL_USERNAME}`, + to: usuario.email.toString(), + subject: 'Contraseña Vencida - Cambio Requerido', + text: 'Su contraseña ha vencido y debe ser cambiada. Por favor, haga clic en el enlace proporcionado para cambiar su contraseña.', + html: htmlToSend, + attachments: null + }; + + await sendMail(options); + return usuario; + } else { + return null; + } + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + throw err; + } + }; - usuario.authenticationToken = authenticationToken; - usuario.passwordChangeTokenExpiry = tokenExpiry; - await usuario.save(); + public getPharmacyAndes = async (req: Request, res: Response): Promise => { + try { + const cuil = req.query.cuil; + const disposicionHabilitacion = req.query.disposicionHabilitacion; + const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/farmacias?cuit=${cuil}&disposicionHabilitacion=${disposicionHabilitacion}`, { headers: { Authorization: process.env.JWT_MPI_TOKEN } }); + return res.status(200).json(resp.body); + } catch (err) { + return res.status(500).json('Server Error'); + } + }; - const extras: any = { - titulo: 'Contraseña Vencida - Cambio Requerido', - nombre: usuario.businessName || usuario.username, - url: `${process.env.APP_DOMAIN}/auth/recovery-password/${authenticationToken}`, - expiryDate: moment(tokenExpiry).format('DD/MM/YYYY').toString() - }; + private validateProfessional = (profesional: any, enrollment: string, cuil: string, graduationDate: string, enrollmentExpiration: string, profesionCodigo: string): boolean => { + if (!profesional || !profesional.profesiones || profesional.profesiones.length === 0 || !profesionCodigo) { + return false; + } + try { + const lastProfesion = profesional.profesiones.find((p: any) => p.profesion.codigo === profesionCodigo); + const lastMatriculacion = lastProfesion && lastProfesion.matriculacion && lastProfesion.matriculacion.length ? lastProfesion.matriculacion[lastProfesion.matriculacion.length - 1] : null; + if (lastMatriculacion) { + let res = (moment(lastMatriculacion.fin) > moment()); + res = res && lastMatriculacion.matriculaNumero.toString() === enrollment; + res = res && profesional.cuit === cuil; + res = res && moment(lastMatriculacion.fin).format('DD-MM-YYYY') === enrollmentExpiration; + res = res && moment(lastProfesion.fechaEgreso).format('DD-MM-YYYY') === graduationDate; + return res; + } else { + return false; + } + } catch (error) { + return false; + } + }; - const htmlToSend = await renderHTML('emails/password-expired.html', extras); - const options: MailOptions = { - from: `${process.env.EMAIL_USERNAME}`, - to: usuario.email.toString(), - subject: 'Contraseña Vencida - Cambio Requerido', - text: 'Su contraseña ha vencido y debe ser cambiada. Por favor, haga clic en el enlace proporcionado para cambiar su contraseña.', - html: htmlToSend, - attachments: null - }; + public getProfessionalsAndes = async (req: Request, res: Response): Promise => { + try { + const dni = req.query.documento; + const enrollment = req.query.matricula; + const cuil = req.query.cuil; + const graduationDate = req.query.fechaEgreso; + const enrollmentExpiration = req.query.fechaMatVencimiento; + const profesionCodigo = req.query.profesionCodigo; + const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/profesionales/guia?documento=${dni}`); + if (!resp.body || resp.body.length === 0) { + return res.status(404).json({ message: 'No se encuentra el profesional.' }); + } + if (!this.validateProfessional(resp.body[0], enrollment, cuil, graduationDate, enrollmentExpiration, profesionCodigo)) { + return res.status(500).json({ message: 'No se encuentra el profesional.' }); + } + return res.status(200).json(resp.body); + } catch (err) { + return res.status(500).json({ message: 'Server Error' }); + } + }; - await sendMail(options); - return usuario; - } else { - return null; - } - } catch (err) { - console.log(err) - throw err; - } - } - - public getPharmacyAndes = async (req: Request, res: Response): Promise => { - try { - const cuil = req.query.cuil; - const disposicionHabilitacion = req.query.disposicionHabilitacion; - const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/farmacias?cuit=${cuil}&disposicionHabilitacion=${disposicionHabilitacion}`, { headers: { Authorization: process.env.JWT_MPI_TOKEN } }); - return res.status(200).json(resp.body); - } catch (err) { - return res.status(500).json('Server Error'); - } - }; - - private validateProfessional = (profesional: any, enrollment: string, cuil: string, graduationDate: string, enrollmentExpiration: string, profesionCodigo: string): boolean => { - if (!profesional || !profesional.profesiones || profesional.profesiones.length === 0 || !profesionCodigo) { - return false; - } - try { - const lastProfesion = profesional.profesiones.find((p: any) => p.profesion.codigo === profesionCodigo); - const lastMatriculacion = lastProfesion && lastProfesion.matriculacion && lastProfesion.matriculacion.length ? lastProfesion.matriculacion[lastProfesion.matriculacion.length - 1] : null; - if (lastMatriculacion) { - let res = (moment(lastMatriculacion.fin) > moment()); - res = res && lastMatriculacion.matriculaNumero.toString() === enrollment; - res = res && profesional.cuit === cuil; - res = res && moment(lastMatriculacion.fin).format('DD-MM-YYYY') === enrollmentExpiration; - res = res && moment(lastProfesion.fechaEgreso).format('DD-MM-YYYY') === graduationDate; - return res; - } else { - return false; - } - } catch (error) { - return false; - } - }; - - public getProfessionalsAndes = async (req: Request, res: Response): Promise => { - try { - const dni = req.query.documento; - const enrollment = req.query.matricula; - const cuil = req.query.cuil; - const graduationDate = req.query.fechaEgreso; - const enrollmentExpiration = req.query.fechaMatVencimiento; - const profesionCodigo = req.query.profesionCodigo; - const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/profesionales/guia?documento=${dni}`); - if (!resp.body || resp.body.length === 0) { - return res.status(404).json({ message: 'No se encuentra el profesional.' }); - } - if (!this.validateProfessional(resp.body[0], enrollment, cuil, graduationDate, enrollmentExpiration, profesionCodigo)) { - return res.status(500).json({ message: 'No se encuentra el profesional.' }); - } - return res.status(200).json(resp.body); - } catch (err) { - return res.status(500).json({ message: 'Server Error' }); - } - }; - - public getAuthorizedProfessions = async (req: Request, res: Response): Promise => { - try { - const profesionesAutorizadas: IProfesionAutorizada[] = await ProfesionAutorizada.find(); - return res.status(200).json(profesionesAutorizadas); - } catch (err) { - // eslint-disable-next-line no-console - console.log(err); - return res.status(500).json('Server Error'); - } - }; + public getAuthorizedProfessions = async (req: Request, res: Response): Promise => { + try { + const profesionesAutorizadas: IProfesionAutorizada[] = await ProfesionAutorizada.find(); + return res.status(200).json(profesionesAutorizadas); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + return res.status(500).json('Server Error'); + } + }; } export default new AuthController(); diff --git a/src/database/dbconfig.ts b/src/database/dbconfig.ts index a39d448d..8a756502 100644 --- a/src/database/dbconfig.ts +++ b/src/database/dbconfig.ts @@ -1,8 +1,7 @@ import mongoose from 'mongoose'; -import { env } from '../config/config'; export const initializeMongo = async (): Promise => { - const MONGO_URI = `${(process.env.MONGODB_URI || env.MONGODB_CONNECTION)}`; + const MONGO_URI = `${(process.env.MONGODB_URI)}`; try { await mongoose.connect(MONGO_URI, { useNewUrlParser: true, diff --git a/src/jobs/user-migration/index.ts b/src/jobs/user-migration/index.ts index 60faeed7..dc8daff3 100644 --- a/src/jobs/user-migration/index.ts +++ b/src/jobs/user-migration/index.ts @@ -1,12 +1,11 @@ import mongoose from 'mongoose'; -import { env } from '../../config/config'; import IUserOld from './user-deprecated.interface'; import { UserClass } from './user.class'; const initializeMongo = (): void => { - const MONGO_URI = `${(process.env.MONGODB_URI || env.MONGODB_CONNECTION)}`; + const MONGO_URI = `${(process.env.MONGODB_URI)}`; mongoose.Promise = Promise; mongoose.connect(MONGO_URI, { useNewUrlParser: true, diff --git a/src/middlewares/passport-config-andes.middleware.ts b/src/middlewares/passport-config-andes.middleware.ts index 465331ac..55b6c067 100644 --- a/src/middlewares/passport-config-andes.middleware.ts +++ b/src/middlewares/passport-config-andes.middleware.ts @@ -1,55 +1,82 @@ -import {Request, Response, NextFunction} from 'express'; +import { Request, Response, NextFunction } from 'express'; import passport from 'passport'; import passportJwt from 'passport-jwt'; -import { env, httpCodes } from '../config/config'; +import { httpCodes } from '../config/config'; import User from '../models/user.model'; import IUser from '../interfaces/user.interface'; +import { isSessionExpired, SESSION_EXPIRED_MESSAGE } from '../utils/session-timeout'; const JwtStrategy = passportJwt.Strategy; const ExtractJwt = passportJwt.ExtractJwt; +const IAT_FUTURE_SKEW_SECONDS = 60; // Config passport JWT strategy // We will use Bearer token to authenticate // This configuration checks: // -token expiration // -user exists +// -token iat is not in the future passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: (process.env.JWT_SECRET || env.JWT_SECRET) -}, async (payload, done: (err?: any, user?: IUser | boolean, info?: {code: number, message: string}) => any | Response) => { - try{ + secretOrKey: (process.env.JWT_SECRET) +}, async (payload, done: (err?: any, user?: IUser | boolean, info?: { code: number; message: string }) => any | Response) => { + try { // find the user specified in token - const user = await User.findOne({ _id: payload.sub }).select('_id'); - + const user = await User.findOne({ _id: payload.sub }).select('_id lastLogin isActive'); // if user doesn't exists, handle it - if(!user){ - return done(null, false, {code: httpCodes.EXPECTATION_FAILED, message: 'Debe iniciar sesión'}); + if (!user) { + return done(null, false, { code: httpCodes.EXPECTATION_FAILED, message: 'Debe iniciar sesión' }); + } + + if (!user.isActive) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'El usuario está inactivo, debe volver a iniciar sesión' }); + } + + if (isSessionExpired(user.lastLogin)) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: SESSION_EXPIRED_MESSAGE }); + } + + const nowUnix = Math.floor(Date.now() / 1000); + if (typeof payload.exp === 'number' && payload.exp < nowUnix) { + return done(null, false, { code: httpCodes.EXPIRED_TOKEN, message: 'El token ha expirado' }); + } + + if (typeof payload.iat === 'number' && payload.iat > nowUnix + IAT_FUTURE_SKEW_SECONDS) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'Sesión inválida, debe volver a iniciar sesión' }); } // otherwise, return the user done(null, user); - }catch(err){ + } catch (err) { + // eslint-disable-next-line no-console console.log('in error'); done(err, false); } })); - const authenticationMiddleware = (req: Request, res: Response, next: NextFunction, authenticationType: string) => { - passport.authenticate(authenticationType, {session: false}, (err, user: IUser | boolean, info?: {code: number, message: string}): any | Response => { - try{ + passport.authenticate(authenticationType, { session: false }, (err, user: IUser | boolean, info?: { code: number; message: string }): any | Response => { + try { - if (err) return next(err) + if (err) { return next(err); } - if(typeof(info) !== 'undefined') return res.status(info.code).json({message: info.message}); + if (typeof (info) !== 'undefined') { return res.status(info.code).json({ message: info.message }); } req.user = user; next(); - }catch(error){ - if(error.code == 'ERR_HTTP_INVALID_STATUS_CODE') return res.status(httpCodes.EXPECTATION_FAILED).json({message: 'Debe iniciar sesión'}); - - return res.status(500).json('Server Error') + } catch (error) { + if (error.code === 'ERR_HTTP_INVALID_STATUS_CODE') {return res.status(httpCodes.EXPECTATION_FAILED).json({ message: 'Debe iniciar sesión' });} + return res.status(500).json('Server Error'); } })(req, res, next); @@ -57,4 +84,4 @@ const authenticationMiddleware = (req: Request, res: Response, next: NextFunctio export const checkAuthAndes = (req: Request, res: Response, next: NextFunction) => { authenticationMiddleware(req, res, next, 'jwt'); -} +}; diff --git a/src/middlewares/passport-config.middleware.ts b/src/middlewares/passport-config.middleware.ts index a64c8018..15e4490f 100644 --- a/src/middlewares/passport-config.middleware.ts +++ b/src/middlewares/passport-config.middleware.ts @@ -1,40 +1,65 @@ -import {Request, Response, NextFunction} from 'express'; +import { Request, Response, NextFunction } from 'express'; import passport from 'passport'; import passportJwt from 'passport-jwt'; import passportLocal from 'passport-local'; -import { env, httpCodes } from '../config/config'; +import { httpCodes } from '../config/config'; import User from '../models/user.model'; import IUser from '../interfaces/user.interface'; +import { isSessionExpired, SESSION_EXPIRED_MESSAGE } from '../utils/session-timeout'; const JwtStrategy = passportJwt.Strategy; const LocalStrategy = passportLocal.Strategy; const ExtractJwt = passportJwt.ExtractJwt; +const IAT_FUTURE_SKEW_SECONDS = 60; // Config passport JWT strategy // We will use Bearer token to authenticate // This configuration checks: // -token expiration // -user exists -passport.use(new JwtStrategy({ +passport.use('jwt', new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: (process.env.JWT_SECRET || env.JWT_SECRET) -}, async (payload, done: (err?: any, user?: IUser | boolean, info?: {code: number, message: string}) => any | Response) => { - try{ - const expirationDate = new Date(payload.exp); - if(expirationDate < new Date()) { - return done(null, false, {code: httpCodes.EXPIRED_TOKEN, message: 'El token ha expirado'}); + secretOrKey: (process.env.JWT_SECRET) +}, async (payload, done: (err?: any, user?: IUser | boolean, info?: { code: number; message: string }) => any | Response) => { + try { + const nowUnix = Math.floor(Date.now() / 1000); + if (typeof payload.exp === 'number' && payload.exp < nowUnix) { + return done(null, false, { code: httpCodes.EXPIRED_TOKEN, message: 'El token ha expirado' }); } + + if (typeof payload.iat === 'number' && payload.iat > nowUnix + IAT_FUTURE_SKEW_SECONDS) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'Sesión inválida, debe volver a iniciar sesión' }); + } + // find the user specified in token - const user = await User.findOne({ _id: payload.sub }).select('_id'); + const user = await User.findOne({ _id: payload.sub }).select('_id lastLogin isActive'); // if user doesn't exists, handle it - if(!user){ - return done(null, false, {code: httpCodes.EXPECTATION_FAILED, message: 'Debe iniciar sesión'}); + if (!user) { + return done(null, false, { code: httpCodes.EXPECTATION_FAILED, message: 'Debe iniciar sesión' }); + } + + if (!user.isActive) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'El usuario está inactivo, debe volver a iniciar sesión' }); + } + + if (isSessionExpired(user.lastLogin)) { + if (payload.sub) { + await User.updateOne({ _id: payload.sub }, { refreshToken: '' }); + } + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: SESSION_EXPIRED_MESSAGE }); } // otherwise, return the user done(null, user); - }catch(err){ + } catch (err) { + // eslint-disable-next-line no-console console.log('in error'); done(err, false); } @@ -47,16 +72,16 @@ passport.use(new JwtStrategy({ passport.use(new LocalStrategy({ usernameField: 'identifier', passwordField: 'password' -}, async (identifier, password, done: (err?: any, user?: IUser | boolean, info?: {code: number, message: string}) => any | Response ) => { - try{ +}, async (identifier, password, done: (err?: any, user?: IUser | boolean, info?: { code: number; message: string }) => any | Response) => { + try { // find the user given the identifier let user = await User.findOne({ email: identifier }); // if not, handle it - if(!user){ + if (!user) { user = await User.findOne({ username: identifier }); - if(!user){ - return done(null, false, {code: httpCodes.UNAUTHORIZED, message: 'El usuario o contraseña que has ingresado es incorrecto. Por favor intenta de nuevo.'}); + if (!user) { + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'El usuario o contraseña que has ingresado es incorrecto. Por favor intenta de nuevo.' }); } } @@ -64,30 +89,33 @@ passport.use(new LocalStrategy({ const isMatch = await user.schema.methods.isValidPassword(user, password); // if not, handle it - if(!isMatch){ - return done(null, false, {code: httpCodes.UNAUTHORIZED, message: 'El usuario o contraseña que has ingresado es incorrecto. Por favor intenta de nuevo.'}); + if (!isMatch) { + return done(null, false, { code: httpCodes.UNAUTHORIZED, message: 'El usuario o contraseña que has ingresado es incorrecto. Por favor intenta de nuevo.' }); } // otherwise, return the user done(null, user); - }catch(err){ + } catch (err) { done(err, false); } })); const authenticationMiddleware = (req: Request, res: Response, next: NextFunction, authenticationType: string) => { - passport.authenticate(authenticationType, {session: false}, (err, user: IUser | boolean, info?: {code: number, message: string}): any | Response => { - try{ + passport.authenticate(authenticationType, { session: false }, (err, user: IUser | boolean, info?: { code: number; message: string }): any | Response => { + try { - if (err) return next(err) + if (err) { return next(err); } - if(typeof(info) !== 'undefined') return res.status(info.code).json({message: info.message}); + if (typeof (info) !== 'undefined') { return res.status(info.code).json({ message: info.message }); } req.user = user; next(); - }catch(error){ - if(error.code == 'ERR_HTTP_INVALID_STATUS_CODE') return res.status(httpCodes.EXPECTATION_FAILED).json({message: 'Debe iniciar sesión'}); + } catch (error) { + const typedError = error as { code?: string }; + if (typedError.code === 'ERR_HTTP_INVALID_STATUS_CODE') { + return res.status(httpCodes.EXPECTATION_FAILED).json({ message: 'Debe iniciar sesión' }); + } - return res.status(500).json('Server Error') + return res.status(500).json({ message: 'Server Error' }); } })(req, res, next); @@ -100,4 +128,4 @@ export const passportMiddlewareLocal = (req: Request, res: Response, next: NextF export const checkAuth = (req: Request, res: Response, next: NextFunction) => { authenticationMiddleware(req, res, next, 'jwt'); -} +}; diff --git a/src/middlewares/roles.middleware.ts b/src/middlewares/roles.middleware.ts index a6a7473f..a8664c7d 100644 --- a/src/middlewares/roles.middleware.ts +++ b/src/middlewares/roles.middleware.ts @@ -5,90 +5,92 @@ import User from '../models/user.model'; export const pharmacistRoleMiddleware = async (req: Request, res: Response, next: NextFunction) => { - const reqUser: IUser | null = req.user; - if (!reqUser) { - return res.status(401).json('No Autorizado'); - } + const reqUser: IUser | null = req.user; + if (!reqUser) { + return res.status(401).json('No Autorizado'); + } - const isMatch: boolean = await User.schema.methods.hasRole(reqUser._id, 'pharmacist'); - console.log(isMatch); - if (!isMatch) { - return res.status(403).json({ message: 'No Autorizado' }); - } + const isMatch: boolean = await User.schema.methods.hasRole(reqUser._id, 'pharmacist'); + // eslint-disable-next-line no-console + console.log(isMatch); + if (!isMatch) { + return res.status(403).json({ message: 'No Autorizado' }); + } - next(); -} + next(); +}; export const professionalRoleMiddleware = async (req: Request, res: Response, next: NextFunction) => { - const reqUser: IUser | null = req.user; - if (!reqUser) { - return res.status(401).json('No Autorizado'); - } + const reqUser: IUser | null = req.user; + if (!reqUser) { + return res.status(401).json('No Autorizado'); + } - const isMatch: boolean = await User.schema.methods.hasRole(reqUser._id, 'professional'); - console.log(isMatch); - if (!isMatch) { - return res.status(403).json({ message: 'No Autorizado' }); - } + const isMatch: boolean = await User.schema.methods.hasRole(reqUser._id, 'professional'); + // eslint-disable-next-line no-console + console.log(isMatch); + if (!isMatch) { + return res.status(403).json({ message: 'No Autorizado' }); + } - next(); -} + next(); +}; const sleep = (ms: number) => { - return new Promise(resolve => setTimeout(resolve, ms)) -} + return new Promise(resolve => setTimeout(resolve, ms)); +}; // middleware with param export const hasPermissionIn = (action: string, resource: string) => { - return async function (req: Request, res: Response, next: NextFunction): Promise { - const { _id } = req.user as IUser; - const user: IUser | null = await User.findOne({ _id }).populate({ path: 'roles', select: 'role' }); //get users roles - const roles: string[] = []; - if (user) { - // prepare an roles array - await Promise.all(user.roles.map(async (rol: any) => { - roles.push(rol.role); - // await sleep(3000).then(() => console.log('sleep into')); //test timer - })); + return async function (req: Request, res: Response, next: NextFunction): Promise { + const { _id } = req.user as IUser; + const user: IUser | null = await User.findOne({ _id }).populate({ path: 'roles', select: 'role' }); // get users roles + const roles: string[] = []; + if (user) { + // prepare an roles array + await Promise.all(user.roles.map(async (rol: any) => { + roles.push(rol.role); + // await sleep(3000).then(() => console.log('sleep into')); //test timer + })); - } - const permissions: boolean = checkByAction(roles, action, resource); - if (!permissions) return res.status(406).json({ mensaje: 'No tiene los permisos suficientes para llevar a acabo esta acción.' }); + } + const permissions: boolean = checkByAction(roles, action, resource); + if (!permissions) {return res.status(406).json({ mensaje: 'No tiene los permisos suficientes para llevar a acabo esta acción.' });} - next(); - } -} + next(); + }; +}; const checkByAction = (role: string | string[], action: string, resource: string) => { - let permission; - switch (action) { - case 'createAny': - permission = accessControl.can(role).createAny(resource); - break; - case 'createOwn': - permission = accessControl.can(role).createAny(resource); - break; - case 'readAny': - permission = accessControl.can(role).readAny(resource); - break; - case 'readOwn': - permission = accessControl.can(role).readOwn(resource); - break; - case 'updateAny': - permission = accessControl.can(role).updateAny(resource); - break; - case 'updateOwn': - permission = accessControl.can(role).updateOwn(resource); - break; - case 'deleteAny': - permission = accessControl.can(role).deleteAny(resource); - break; - case 'deleteOwn': - permission = accessControl.can(role).deleteOwn(resource); - break; - } - return permission ? permission.granted : true; -} + let permission; + switch (action) { + case 'createAny': + permission = accessControl.can(role).createAny(resource); + break; + case 'createOwn': + permission = accessControl.can(role).createAny(resource); + break; + case 'readAny': + permission = accessControl.can(role).readAny(resource); + break; + case 'readOwn': + permission = accessControl.can(role).readOwn(resource); + break; + case 'updateAny': + permission = accessControl.can(role).updateAny(resource); + break; + case 'updateOwn': + permission = accessControl.can(role).updateOwn(resource); + break; + case 'deleteAny': + permission = accessControl.can(role).deleteAny(resource); + break; + case 'deleteOwn': + permission = accessControl.can(role).deleteOwn(resource); + break; + } + return permission ? permission.granted : true; +}; diff --git a/src/migration-script/index.ts b/src/migration-script/index.ts index d2406e8b..2efbb7cf 100644 --- a/src/migration-script/index.ts +++ b/src/migration-script/index.ts @@ -1,12 +1,11 @@ import mongoose from 'mongoose'; -import { env } from '../config/config'; // interface import { PrescriptionClass } from './classes/prescriptions.class'; import IPrescriptionOld from './interfaces/prescription-deprecated.interface'; //old prescriptions // init db connections const initializeMongo = (): void => { - const MONGO_URI = `${(process.env.MONGODB_URI || env.MONGODB_CONNECTION)}`; + const MONGO_URI = `${(process.env.MONGODB_URI)}`; mongoose.Promise = Promise; mongoose.connect(MONGO_URI, { useNewUrlParser: true, diff --git a/src/migration-script/rename-prescriptions-collection.ts b/src/migration-script/rename-prescriptions-collection.ts index abf997dd..bacd0786 100644 --- a/src/migration-script/rename-prescriptions-collection.ts +++ b/src/migration-script/rename-prescriptions-collection.ts @@ -1,8 +1,7 @@ import mongoose from 'mongoose'; -import { env } from '../config/config'; const initializeMongo = (): void => { - const MONGO_URI = `${(process.env.MONGODB_URI || env.MONGODB_CONNECTION)}`; + const MONGO_URI = `${(process.env.MONGODB_URI)}`; mongoose.Promise = Promise; mongoose.connect(MONGO_URI, {}) .then( mongoose => { diff --git a/src/scripts/patient-ANDES/index.ts b/src/scripts/patient-ANDES/index.ts index 7e3725b9..e839084d 100644 --- a/src/scripts/patient-ANDES/index.ts +++ b/src/scripts/patient-ANDES/index.ts @@ -1,5 +1,4 @@ import mongoose from 'mongoose'; -import { env } from '../../config/config'; // interface import { PatientClass } from './patient.class'; import IPatient from '../../interfaces/patient.interface'; @@ -8,7 +7,7 @@ import Patient from '../../models/patient.model'; // init db connections const initializeMongo = (): void => { - const MONGO_URI = `${(process.env.MONGODB_URI || env.MONGODB_CONNECTION)}`; + const MONGO_URI = `${(process.env.MONGODB_URI)}`; mongoose.Promise = Promise; mongoose.connect(MONGO_URI, { useNewUrlParser: true, diff --git a/src/server.ts b/src/server.ts index fe001cd2..8a0bc3cc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,7 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + import express from 'express'; import morgan from 'morgan'; import helmet from 'helmet'; @@ -6,8 +10,6 @@ import compression from 'compression'; import { errorHandler } from './middlewares/error.middleware'; import { notFoundHandler } from './middlewares/notFound.middleware'; import * as db from './database/dbconfig'; -// config -import { env } from './config/config'; // services import routes from './routes/routes'; @@ -41,7 +43,7 @@ class Server { } routes() { - this.app.use(`${(process.env.API_URI_PRFIX || env.API_URI_PREFIX)}`, routes); + this.app.use(`${(process.env.API_URI_PREFIX)}`, routes); } async start() { @@ -50,7 +52,7 @@ class Server { // eslint-disable-next-line no-console console.log(`🚀 API Server running on port ${this.app.get('port')}`); // eslint-disable-next-line no-console - console.log(`📋 API disponible en: http://localhost:${this.app.get('port')}${env.API_URI_PREFIX}`); + console.log(`📋 API disponible en: http://localhost:${this.app.get('port')}${process.env.API_URI_PREFIX}`); }); } diff --git a/src/utils/session-timeout.ts b/src/utils/session-timeout.ts new file mode 100644 index 00000000..3b8cb3b5 --- /dev/null +++ b/src/utils/session-timeout.ts @@ -0,0 +1,26 @@ +const DEFAULT_MAX_SESSION_AGE_HOURS = 12; +export const SESSION_EXPIRED_MESSAGE = 'La sesión expiró, debe volver a iniciar sesión'; + +export const getMaxSessionAgeHours = (): number => { + const configuredValue = Number.parseFloat((process.env.MAX_TOKEN_AGE_HOURS || `${DEFAULT_MAX_SESSION_AGE_HOURS}`).trim()); + + if (!Number.isFinite(configuredValue) || configuredValue <= 0) { + return DEFAULT_MAX_SESSION_AGE_HOURS; + } + + return configuredValue; +}; + +export const isSessionExpired = (lastLogin?: Date | string | null): boolean => { + if (!lastLogin) { + return true; + } + + const sessionStartMs = new Date(lastLogin).getTime(); + if (!Number.isFinite(sessionStartMs)) { + return true; + } + + const maxSessionAgeMs = getMaxSessionAgeHours() * 60 * 60 * 1000; + return (Date.now() - sessionStartMs) >= maxSessionAgeMs; +}; \ No newline at end of file