From de9e35c5015b0f16ba433f053b146eac2d8e64c0 Mon Sep 17 00:00:00 2001 From: Ernesto Herrera Date: Fri, 17 Jul 2026 16:12:11 -0600 Subject: [PATCH] =?UTF-8?q?feat(core):=20bot=C3=B3n=20"dar=20de=20alta=20u?= =?UTF-8?q?suario"=20(invitaci=C3=B3n)=20en=20Usuarios=20+=20fix=20hub=5Fa?= =?UTF-8?q?dmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend Usuarios: formulario para dar de alta (email + rol) → invitación; muestra el enlace copiable por si el correo no llega. - Backend invites: resuelve tenant_slug desde la compañía cuando el token no lo trae (hub_admin) y usa el token KC de la sesión (valkey) para crear el invite en el Hub. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/api/v1/modules/core/invites/routes.py | 22 ++++- .../src/routes/dashboard/users/+page.svelte | 83 ++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/backend/api/v1/modules/core/invites/routes.py b/backend/api/v1/modules/core/invites/routes.py index a134440..3ed0883 100644 --- a/backend/api/v1/modules/core/invites/routes.py +++ b/backend/api/v1/modules/core/invites/routes.py @@ -37,13 +37,33 @@ async def create_invite( required_permissions=["user.create"], ) + # tenant_slug: del token si viene; si el usuario es hub_admin (sin tenant en el + # token), se resuelve desde la compañía destino (a76.company → core.tenants). tenant_slug: str = current_user.get("tenant_slug") or "" + if not tenant_slug: + from sqlalchemy import text as _text + row = db.execute( + _text( + "SELECT t.slug FROM a76.company c " + "JOIN core.tenants t ON t.id = c.tenant_id WHERE c.id = :c" + ), + {"c": data.company_id}, + ).first() + if row and row[0]: + tenant_slug = row[0] + created_by: str = current_user.get("sub") or "" + # El invite se crea en el Hub: se necesita el token KC (la sesión local no la + # acepta el Hub). Se toma de la sesión (valkey) y se refresca si hace falta. + from core.hub_token import get_hub_access_token + + kc_token = await get_hub_access_token(request) + service = InviteService(db) return await service.create_invite( data=data, created_by=created_by, tenant_slug=tenant_slug, - user_access_token=credentials.credentials, + user_access_token=kc_token or credentials.credentials, ) diff --git a/frontend/src/routes/dashboard/users/+page.svelte b/frontend/src/routes/dashboard/users/+page.svelte index ec059e8..67f14a9 100644 --- a/frontend/src/routes/dashboard/users/+page.svelte +++ b/frontend/src/routes/dashboard/users/+page.svelte @@ -1,6 +1,7 @@
@@ -112,6 +158,41 @@ {#if !companyId} Selecciona una compañía activa. {:else} + + + Dar de alta usuario + Se envía una invitación por email; el usuario crea su contraseña y queda en la compañía con el rol elegido. + + +
+ + +
+
+ {#if roles.length === 0} + Primero crea roles en "Roles y permisos". + {:else}{/if} + +
+ {#if inviteUrl} +
+ Si el correo no llega, comparte: + + +
+ {/if} +
+
+ Usuarios ({users.length})