Ir al contenido

Invitación del staff

hop o dir invitan a un nuevo miembro del staff desde /dashboard/settings → Usuarios. La acción inviteOrgUser(email, role) genera un link de Supabase Auth, lo manda por Resend, y crea la membership activa en la org. El usuario cae en /auth/set-password?next=/dashboard.

  • Problema que resuelve: dar acceso a miembros del cuerpo técnico (S&C, fisio, médico, nutricionista, etc.) con scope correcto según rol, sin requerir setup manual.
  • Casos de uso típicos: alta inicial del staff al contratar, incorporación de un fisio nuevo, cambio de rol de un usuario existente (con confirmación explícita).
  • Planes que lo incluyen: todos. Esencial permite hasta 5 colaboradores activos (PLAN_LIMITS.esencial.collaborators = 5); Pro y Enterprise: ilimitado (Infinity).

Solo hop y dir ven la sección “Usuarios” en Settings.

  1. Settings → tab “Usuarios” → botón “Invitar”.
  2. Modal pide email y rol (uno de los roles oficiales en lib/roles.ts (hop, dir, coord_form, sc, ss, rtp, fisio, med, nut, psi, entrenador)).
  3. inviteOrgUser(email, role) ejecuta:
    • Si el email ya existe en user_profiles y tiene membership activa con otro rol, devuelve needsConfirmation: true con el rol vigente y el rol propuesto. La UI pide confirmación antes de continuar.
    • Genera el link con admin.auth.admin.generateLink({ type: 'invite' }), con fallback a type: 'magiclink' si el email ya está en Supabase Auth.
    • Upsertea user_profiles con organization_id, role, plan heredado de la org.
    • Inserta o actualiza la row en user_org_memberships (status active).
    • Manda el mail con sendEmail() y inviteEmailHtml({ forPlayer: false }).
  4. El usuario recibe el mail, fija contraseña en /auth/set-password, y cae en /dashboard con el rol asignado.

Si la UI muestra “Este usuario ya tiene rol X — confirmá si querés cambiarlo a Y”, el staff debe reinvocar la acción con { force: true }. Esto evita degradaciones silenciosas de privilegios que pasaban en versiones anteriores.

  • El plan que se asigna por defecto al nuevo usuario sale de organizations.plan.
  • El branding del mail (logo, nombre) sale de organizations.name y organizations.logo_url.
  • Rol inválido: la acción rechaza con “Rol inválido” si no matchea con VALID_ROLES de lib/roles.ts.
  • RESEND_API_KEY faltante: la acción devuelve "No se pudo enviar el email. Verificá la configuración de Resend." y loguea warning en Sentry. La membership igual queda creada, pero el usuario no recibe el link.
  • Revocación: revokeOrgUser(userId) baja la membership a status = 'revoked' (con revoked_at) sin borrar la cuenta en auth.users. Bloquea revocar al último admin (hop/dir) activo de la org.

Player surface: N/A. Este flujo es solo para staff.

  • Supabase Auth vía admin.auth.admin.generateLink.
  • Resend envía el mail final.
  • Tablas user_profiles y user_org_memberships se modifican atómicamente desde la action.
  • No hay UI para “reenviar invite” si el link expiró. El workaround es volver a invocar inviteOrgUser con el mismo email — Supabase reusa el auth.users row y genera un link nuevo.
  • Sin auditoría visible de cambios de rol — solo queda registro en los logs de la action. Para audit log estructurado, ver /dashboard/audit (interno).