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:
58
apps/web/lib/api/client-invitations.ts
Normal file
58
apps/web/lib/api/client-invitations.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface ClientInvitation {
|
||||
id: string;
|
||||
email: string;
|
||||
invitedBy: string;
|
||||
nombreDespacho: string | null;
|
||||
rfc: string | null;
|
||||
status: string;
|
||||
token: string;
|
||||
sentAt: string;
|
||||
expiresAt: string;
|
||||
acceptedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export async function createInvitation(data: {
|
||||
email: string;
|
||||
nombreDespacho?: string;
|
||||
rfc?: string;
|
||||
}): Promise<{ message: string; invitation: ClientInvitation }> {
|
||||
const res = await apiClient.post('/invitations/client', data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function validateInvitationToken(token: string): Promise<{
|
||||
email: string;
|
||||
nombreDespacho: string | null;
|
||||
rfc: string | null;
|
||||
expiresAt: string;
|
||||
}> {
|
||||
const res = await apiClient.get(`/invitations/client/validate/${token}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function registerFromInvitation(
|
||||
token: string,
|
||||
data: {
|
||||
nombre: string;
|
||||
password: string;
|
||||
nombreDespacho: string;
|
||||
rfc: string;
|
||||
verticalProfile: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA';
|
||||
codigoPostal?: string;
|
||||
}
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: any;
|
||||
}> {
|
||||
const res = await apiClient.post(`/invitations/client/register/${token}`, data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getClientInvitations(): Promise<ClientInvitation[]> {
|
||||
const res = await apiClient.get('/invitations/client');
|
||||
return res.data;
|
||||
}
|
||||
Reference in New Issue
Block a user