diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 0948690..55701c7 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import IUser from '../interfaces/user.interface'; import User from '../models/user.model'; import { renderHTML, MailOptions, sendMail } from '../utils/roboSender/sendEmail'; +import crypto from 'crypto'; class UsersController { public index = async (req: Request, res: Response): Promise => { try { @@ -49,6 +50,84 @@ class UsersController { } }; + public requestEmailUpdate = async (req: Request, res: Response): Promise => { + try { + const { email, userId } = req.body; + + if (!email) { + return res.status(400).json({ mensaje: 'Email requerido' }); + } + + // Verificar si el email ya existe en otro usuario + const emailExists = await this.validateEmailUniqueness(email, userId); + if (emailExists) { + return res.status(400).json({ mensaje: 'El email ya está registrado por otro usuario' }); + } + + // Generar token y expiración + const token = crypto.randomBytes(20).toString('hex'); + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 horas + + const user = await User.findByIdAndUpdate(userId, { + pendingEmail: email, + emailConfirmationToken: token, + emailConfirmationExpires: expires + }, { new: true }); + + if (!user) { + return res.status(404).json({ mensaje: 'Usuario no encontrado' }); + } + + // Enviar email de confirmación + await this.sendEmailUpdateConfirmation(user, email, token); + + return res.status(200).json({ mensaje: 'Se ha enviado un correo de confirmación a la nueva dirección.' }); + + } catch (e) { + return res.status(500).json({ mensaje: `${e}` }); + } + }; + + public confirmEmailUpdate = async (req: Request, res: Response): Promise => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ mensaje: 'Token requerido' }); + } + + // Buscar usuario con el token y verificar expiración + const user = await User.findOne({ + emailConfirmationToken: token, + emailConfirmationExpires: { $gt: new Date() } + }).populate('roles'); + + if (!user) { + return res.status(400).json({ mensaje: 'Token inválido o expirado' }); + } + + // Aplicar el cambio + user.email = user.pendingEmail!; + + // Si es farmacia, actualizamos también el username + const isPharmacy = user.roles.some((role: any) => role.role === 'pharmacist'); + if (isPharmacy) { + user.username = user.pendingEmail!; + } + + user.pendingEmail = undefined; + user.emailConfirmationToken = undefined; + user.emailConfirmationExpires = undefined; + + await user.save(); + + return res.status(200).json({ mensaje: 'Email actualizado correctamente' }); + + } catch (e) { + return res.status(500).json({ mensaje: `${e}` }); + } + }; + /** * Valida que el email no esté siendo usado por otro usuario * @param email - Email a validar @@ -156,6 +235,35 @@ class UsersController { console.error('Error enviando notificación de cambio de email:', error); } }; + + /** + * Envía un email con el token para confirmar el cambio de email + * @param user - Usuario + * @param newEmail - Nuevo email + * @param token - Token de confirmación + */ + private sendEmailUpdateConfirmation = async (user: IUser, newEmail: string, token: string): Promise => { + try { + const extras: any = { + titulo: 'Confirmar cambio de email', + usuario: user, + url: `${process.env.APP_DOMAIN || 'https://recetar.andes.gob.ar'}/auth/confirm-update/${token}` + }; + + const htmlToSend = await renderHTML('emails/update-email.html', extras); + const options: MailOptions = { + from: `${process.env.EMAIL_USERNAME}`, + to: newEmail, + subject: 'Confirmar cambio de email - RecetAR', + text: '', + html: htmlToSend, + attachments: null + }; + await sendMail(options); + } catch (error) { + console.error('Error enviando confirmación de cambio de email:', error); + } + }; }; export default new UsersController; diff --git a/src/interfaces/user.interface.ts b/src/interfaces/user.interface.ts index 965edb8..9fa7626 100644 --- a/src/interfaces/user.interface.ts +++ b/src/interfaces/user.interface.ts @@ -1,9 +1,12 @@ import { Document } from 'mongoose'; import IRole from './role.interface'; import IProfesionAutorizada from './profesionAutorizada.interface'; -export default interface IUser extends Document{ +export default interface IUser extends Document { username: string; email: string; + pendingEmail?: string; + emailConfirmationToken?: string; + emailConfirmationExpires?: Date; businessName: string; enrollment?: string; cuil?: string; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index eea0da5..83583c7 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -39,6 +39,15 @@ export const userSchema = new Schema({ email: { type: String }, + pendingEmail: { + type: String + }, + emailConfirmationToken: { + type: String + }, + emailConfirmationExpires: { + type: Date + }, enrollment: { type: String }, @@ -84,18 +93,18 @@ export const userSchema = new Schema({ }, profesionGrado: [{ profesion: { - type: String, - required: '{PATH} is required' + type: String, + required: '{PATH} is required' }, codigoProfesion: { - type: String, - required: '{PATH} is required' + type: String, + required: '{PATH} is required' }, numeroMatricula: { - type: String, - required: '{PATH} is required' + type: String, + required: '{PATH} is required' }, - }] + }] }); // Model diff --git a/src/routes/private.ts b/src/routes/private.ts index bca58d4..fb39633 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -105,6 +105,8 @@ class PrivateRoutes { // Users this.router.get('/users/index', hasPermissionIn('readAny', 'user'), usersController.index); this.router.post('/users/update', hasPermissionIn('updateOwn', 'user'), usersController.update); + this.router.post('/users/request-update', hasPermissionIn('updateOwn', 'user'), usersController.requestEmailUpdate); + this.router.post('/users/confirm-update', usersController.confirmEmailUpdate); this.router.get('/users/:id', hasPermissionIn('updateOwn', 'user'), usersController.getUserInfo); // pharmacy diff --git a/src/templates/emails/update-email.html b/src/templates/emails/update-email.html new file mode 100644 index 0000000..d871745 --- /dev/null +++ b/src/templates/emails/update-email.html @@ -0,0 +1,13 @@ +{{#> layout }} +
+ Hola {{ nombre }}!
+
+

+ Recibiste este mensaje porque solicitaste cambiar tu dirección de email. +

+
+ +Para confirmar este cambio, haz click en el siguiente enlace: Confirmar cambio de email +
+Este enlace expirará en 24 horas. +{{/layout}} diff --git a/src/utils/roboSender/sendEmail.ts b/src/utils/roboSender/sendEmail.ts index 15e601a..7641281 100644 --- a/src/utils/roboSender/sendEmail.ts +++ b/src/utils/roboSender/sendEmail.ts @@ -27,7 +27,7 @@ export function sendMail(options: MailOptions) { auth: { user: `${process.env.EMAIL_USERNAME}`, pass: `${process.env.EMAIL_PASSWORD}` - }, + } }); const mailOptions = {