Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/controllers/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> => {
try {
Expand Down Expand Up @@ -49,6 +50,84 @@ class UsersController {
}
};

public requestEmailUpdate = async (req: Request, res: Response): Promise<Response> => {
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<Response> => {
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
Expand Down Expand Up @@ -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<void> => {
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;
5 changes: 4 additions & 1 deletion src/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
23 changes: 16 additions & 7 deletions src/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export const userSchema = new Schema({
email: {
type: String
},
pendingEmail: {
type: String
},
emailConfirmationToken: {
type: String
},
emailConfirmationExpires: {
type: Date
},
enrollment: {
type: String
},
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/routes/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/templates/emails/update-email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{#> layout }}
<div class="title" style="font-family:Helvetica, Arial, sans-serif;font-size:18px;font-weight:600;color:#374550">
Hola {{ nombre }}!</div>
<br>
<h4>
Recibiste este mensaje porque solicitaste cambiar tu dirección de email.
</h4>
<br>

Para confirmar este cambio, haz click en el siguiente enlace: <a href="{{url}}">Confirmar cambio de email</a>
<br>
Este enlace expirará en 24 horas.
{{/layout}}
2 changes: 1 addition & 1 deletion src/utils/roboSender/sendEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function sendMail(options: MailOptions) {
auth: {
user: `${process.env.EMAIL_USERNAME}`,
pass: `${process.env.EMAIL_PASSWORD}`
},
}
});

const mailOptions = {
Expand Down