feat(invitations): flujo de invitacion de clientes por email
Backend:
- Nuevo modelo Prisma ClientInvitation con token unico, expiracion
y estados (pending/accepted/expired).
- Migracion: 20260511213955_add_client_invitations
- Service client-invitations.service.ts: crear invitacion,
validar token, registrar desde invitacion (reutiliza logica
de creacion de tenant + usuario de despacho.service).
- Controller + routes: POST /invitations/client (admin),
GET /invitations/client/validate/:token (publico),
POST /invitations/client/register/:token (publico),
GET /invitations/client (admin).
- Email template client-invitation.ts con link a
/invitacion/registro/{token}.
- Agregado sendClientInvitation a email.service.
Frontend:
- Pagina /invitacion/registro/[token] para que el invitado
complete registro (nombre, password, despacho, RFC, perfil).
- Pagina /admin/invitar-cliente para que admin global envie
invitaciones y vea el historial.
- Hooks useCreateInvitation, useValidateInvitationToken,
useRegisterFromInvitation, useClientInvitations.
- API client lib/api/client-invitations.ts.
Infra:
- PM2 ecosystem.config.js: usa node --import tsx con
kill_timeout aumentado a 15s para evitar EADDRINUSE.
- React Query retry=2 con delay exponencial para resiliencia.
Refs: docs/CAMBIOS-2026-05-09.md
This commit is contained in:
83
apps/api/src/controllers/client-invitations.controller.ts
Normal file
83
apps/api/src/controllers/client-invitations.controller.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import * as clientInvitationService from '../services/client-invitations.service.js';
|
||||
import { hasAnyPlatformRole } from '../utils/platform-admin.js';
|
||||
|
||||
export async function createInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { email, nombreDespacho, rfc } = req.body;
|
||||
if (!email) {
|
||||
return res.status(400).json({ message: 'El email es requerido' });
|
||||
}
|
||||
|
||||
// Solo platform_admin puede crear invitaciones
|
||||
const isAdmin = await hasAnyPlatformRole(req.user!.userId, 'platform_admin');
|
||||
if (!isAdmin) {
|
||||
return res.status(403).json({ message: 'Solo administradores pueden crear invitaciones' });
|
||||
}
|
||||
|
||||
const invitation = await clientInvitationService.createInvitation({
|
||||
email,
|
||||
invitedBy: req.user!.userId,
|
||||
invitedByName: (req.user as any)?.nombre || 'Horux Despachos',
|
||||
nombreDespacho,
|
||||
rfc,
|
||||
});
|
||||
|
||||
res.status(201).json({ message: 'Invitación enviada', invitation });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateToken(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const token = String(req.params.token);
|
||||
const invitation = await clientInvitationService.validateInvitationToken(token);
|
||||
res.json({
|
||||
email: invitation.email,
|
||||
nombreDespacho: invitation.nombreDespacho,
|
||||
rfc: invitation.rfc,
|
||||
expiresAt: invitation.expiresAt,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerFromInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const token = String(req.params.token);
|
||||
const { nombre, password, nombreDespacho, rfc, verticalProfile, codigoPostal } = req.body;
|
||||
|
||||
if (!nombre || !password || !nombreDespacho || !rfc || !verticalProfile) {
|
||||
return res.status(400).json({ message: 'Todos los campos son requeridos' });
|
||||
}
|
||||
|
||||
const result = await clientInvitationService.registerFromInvitation(token, {
|
||||
nombre,
|
||||
password,
|
||||
nombreDespacho,
|
||||
rfc,
|
||||
verticalProfile,
|
||||
codigoPostal,
|
||||
});
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function listInvitations(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const isAdmin = await hasAnyPlatformRole(req.user!.userId, 'platform_admin');
|
||||
if (!isAdmin) {
|
||||
return res.status(403).json({ message: 'No autorizado' });
|
||||
}
|
||||
|
||||
const invitations = await clientInvitationService.listInvitations();
|
||||
res.json(invitations);
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user