feat: version 2.10.3 -- reenviar correo de invitacion/verificacion desde Usuarios

Nueva opcion "Reenviar correo" en el menu de 3 puntos de Organizacion -> Usuarios, para
usuarios en estado 'invited' o 'pending_verification'. Reusa las mismas columnas
(verification_token/verification_token_expires) y plantillas que el envio original
(inviteUser / auth/service.js#register), solo genera un token nuevo -- el anterior pudo
vencer o perderse en spam.

De paso se corrige que el estado "invited" no tenia rama propia en el menu de acciones (caia
en el branch generico de editar/activar-desactivar, que no aplica a una cuenta placeholder sin
clave real todavia) ni traduccion de su badge (user_status_invited faltaba en i18n).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-12 16:02:24 -05:00
parent 98d9196791
commit 2d1bfc0bfd
11 changed files with 90 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
"version": "2.10.2",
"version": "2.10.3",
"private": true,
"type": "module",
"main": "src/server.js",
+4
View File
@@ -28,3 +28,7 @@ export const postRejectUser = asyncHandler(async (req, res) => {
await service.rejectUser(Number(req.params.id), req.currentUser.id);
ok(res, { rejected: true });
});
export const postResendUserEmail = asyncHandler(async (req, res) => {
ok(res, await service.resendUserEmail(Number(req.params.id), req.currentUser.id));
});
+9
View File
@@ -77,6 +77,15 @@ export async function remove(id) {
await pool.query('DELETE FROM users WHERE id = ?', [id]);
}
// Reusa verification_token/verification_token_expires -- misma columna que invited y
// pending_verification ya usan (ver inviteUser/register), solo se refresca con un token nuevo.
export async function refreshVerificationToken(id, token, expiresHours) {
await pool.query(
'UPDATE users SET verification_token = ?, verification_token_expires = DATE_ADD(NOW(), INTERVAL ? HOUR) WHERE id = ?',
[token, expiresHours, id]
);
}
export async function listRoles() {
const [rows] = await pool.query('SELECT id, code, name FROM roles ORDER BY id');
return rows;
+4 -1
View File
@@ -1,6 +1,8 @@
import { Router } from 'express';
import { requireAuth, requireRole, CAN_MANAGE_USERS } from '../../middleware/auth.js';
import { getUsers, getRoles, postUser, putUser, patchUserActive, postApproveUser, postRejectUser } from './controller.js';
import {
getUsers, getRoles, postUser, putUser, patchUserActive, postApproveUser, postRejectUser, postResendUserEmail,
} from './controller.js';
const router = Router();
@@ -12,5 +14,6 @@ router.put('/:id', requireRole(...CAN_MANAGE_USERS), putUser);
router.patch('/:id/active', requireRole(...CAN_MANAGE_USERS), patchUserActive);
router.post('/:id/approve', requireRole(...CAN_MANAGE_USERS), postApproveUser);
router.post('/:id/reject', requireRole(...CAN_MANAGE_USERS), postRejectUser);
router.post('/:id/resend', requireRole(...CAN_MANAGE_USERS), postResendUserEmail);
export default router;
+38
View File
@@ -79,6 +79,44 @@ export async function approveUser(id, payload, actorId) {
return user;
}
// Reenvia el correo pendiente (invitacion o verificacion) con un token nuevo -- el anterior
// pudo vencer, perderse, o caer en spam. Mismas plantillas que el envio original (inviteUser /
// auth/service.js#register), solo cambia el token y su vencimiento.
const RESENDABLE_STATUSES = new Set(['invited', 'pending_verification']);
export async function resendUserEmail(id, actorId) {
const existing = await repo.findById(id);
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
if (!RESENDABLE_STATUSES.has(existing.status)) {
throw new HttpError(400, 'Este usuario no tiene un correo pendiente de confirmar');
}
const token = crypto.randomBytes(32).toString('hex');
if (existing.status === 'invited') {
await repo.refreshVerificationToken(id, token, 24 * 7);
const inviteUrl = `${env.corsOrigin}/aceptar-invitacion?token=${token}`;
await sendMail({
to: existing.email,
subject: 'Te invitaron a project.linkedcorp.net',
html: `<p>Te agregaron como miembro de un proyecto en project.linkedcorp.net.</p>
<p>Completa tu registro para acceder (elige tu nombre y una clave):</p>
<p><a href="${inviteUrl}">${inviteUrl}</a></p><p>Este enlace vence en 7 dias.</p>`,
});
} else {
await repo.refreshVerificationToken(id, token, 24);
const verifyUrl = `${env.corsOrigin}/verificar-correo?token=${token}`;
await sendMail({
to: existing.email,
subject: 'Confirma tu correo — project.linkedcorp.net',
html: `<p>Hola ${existing.name},</p><p>Confirma tu correo para continuar tu registro:</p><p><a href="${verifyUrl}">${verifyUrl}</a></p><p>Este enlace vence en 24 horas.</p>`,
});
}
await logAudit({ userId: actorId, action: 'resend_email', entityType: 'user', entityId: id, detail: { status: existing.status } });
return { sent: true };
}
// Rechaza y borra una cuenta que aun no esta activa: tanto la que ya paso la verificacion de
// correo y espera aprobacion, como la que nunca llego a confirmar su correo (quedo abandonada en
// 'pending_verification' — antes no habia forma de quitarla desde la UI).