feat: invitar a un proyecto por correo a alguien que aun no es usuario

Al agregar un miembro, el popup ahora permite elegir un usuario existente
o escribir un correo nuevo. Si ese correo no pertenece a nadie, se crea
una cuenta en estado 'invited' (nombre/clave placeholder) y se le manda
un correo con un enlace de un solo uso para que complete su registro
(elige su nombre y clave). La fila de project_members se crea de una vez
al invitar, asi que en cuanto acepta ya es miembro sin pasos adicionales.
A diferencia del autorregistro publico, no pasa por aprobacion de un
admin -- quien lo agrego a su proyecto ya lo vetó.

Nuevos endpoints publicos GET /api/auth/invite y POST /api/auth/accept-invite.
Nueva pagina AcceptInvitePage (/aceptar-invitacion). Migracion 004 agrega
el status 'invited' al enum de users.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-09 23:55:02 -05:00
parent 53c7bd7f95
commit d7478d0f21
20 changed files with 320 additions and 23 deletions
+9 -2
View File
@@ -59,10 +59,17 @@
editables directo por el cliente: actividad con subtareas = % de subtareas hechas; actividad sin
subtareas = manual (0-100); proyecto = promedio del avance de sus actividades.
- `users.password_hash` con bcrypt, nunca texto plano ni reversible.
- `users.status` (`pending_verification`/`pending_approval`/`active`) es el ciclo de vida del
autorregistro, independiente de `is_active` (que sigue siendo el interruptor manual del admin
- `users.status` (`pending_verification`/`pending_approval`/`active`/`invited`) es el ciclo de vida
de la cuenta, independiente de `is_active` (que sigue siendo el interruptor manual del admin
para desactivar a alguien ya activo). Un usuario `pending_approval` rechazado se **borra**
físicamente (no hay estado "rechazado" — se audita el email en `audit_log` antes de borrar).
- `invited`: cuenta placeholder creada al agregar por correo a alguien que aún no es usuario desde
Miembros de un proyecto (`name`=`email`, `password_hash` aleatorio inservible, reutiliza
`verification_token`/`verification_token_expires` — 7 días en vez de 24h). Al aceptar la
invitación (`POST /api/auth/accept-invite`) pasa directo a `active` **sin pasar por
`pending_approval`** — a diferencia del autorregistro público, aquí quien lo invitó ya lo vetó al
agregarlo a su proyecto. La fila de `project_members` se crea en el momento de invitar, no al
aceptar, así que en cuanto acepta ya aparece como miembro sin pasos adicionales.
- `system_settings.value` para `smtp_pass_enc` va **cifrado** (AES-256-GCM, `ENCRYPTION_KEY` del
`.env`) — nunca en texto plano, nunca se devuelve por la API (solo `configured: true/false`).
- Borrado lógico donde aplique (`users.is_active`); nunca DELETE físico de usuarios **activos** con