M5: tenantship + admin members + accept_invite RPC
Modulo 5 da Fase 1 + quick wins fechados. features/tenantship/ com 2 services + 2 composables (members + invites). MembersPage.vue nova em views/pages/admin/ + rota /admin/members em routes.clinic. Migration 20260520000005 cria RPC accept_tenant_invite (SECURITY DEFINER + lock FOR UPDATE) — tenantInvitesRepository.acceptInvite agora chama RPC real (nao mais stub). SaasTenantFeaturesPage refatorada pra usar novo tenantFeatureAdminService. SetupWizardPage 2648 linhas deferido pra sessao dedicada. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
-- ============================================================================
|
||||
-- RPC accept_tenant_invite — destrava o fluxo de aceitar convite
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Recebe o token UUID do invite. Em uma transação (SECURITY DEFINER):
|
||||
-- 1. Lê invite ATIVO (não accepted, não revoked, não expired)
|
||||
-- 2. INSERT em tenant_members com role do invite + user_id = auth.uid()
|
||||
-- 3. UPDATE invite com accepted_at + accepted_by
|
||||
--
|
||||
-- Retorna jsonb { ok, tenant_id, role } em sucesso ou throw com mensagem PT-BR.
|
||||
--
|
||||
-- Chamada pelo features/tenantship/services/tenantInvitesRepository.acceptInvite().
|
||||
-- Stub anterior tava jogando erro PT-BR explicando isso. Agora funciona.
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.accept_tenant_invite(p_token uuid)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_uid uuid;
|
||||
v_invite record;
|
||||
v_existing_member record;
|
||||
BEGIN
|
||||
-- Quem está aceitando — auth.uid() pega do JWT
|
||||
v_uid := auth.uid();
|
||||
IF v_uid IS NULL THEN
|
||||
RAISE EXCEPTION 'Sessão inválida (sem user autenticado).';
|
||||
END IF;
|
||||
|
||||
-- 1. Lê invite ativo. Lock via FOR UPDATE pra evitar race.
|
||||
SELECT id, tenant_id, email, role, accepted_at, revoked_at, expires_at
|
||||
INTO v_invite
|
||||
FROM public.tenant_invites
|
||||
WHERE token = p_token
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Convite não encontrado. Verifique o link.';
|
||||
END IF;
|
||||
|
||||
IF v_invite.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Convite revogado pelo administrador.';
|
||||
END IF;
|
||||
|
||||
IF v_invite.accepted_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Convite já foi aceito anteriormente.';
|
||||
END IF;
|
||||
|
||||
IF v_invite.expires_at IS NOT NULL AND v_invite.expires_at < now() THEN
|
||||
RAISE EXCEPTION 'Convite expirado. Peça um novo ao administrador.';
|
||||
END IF;
|
||||
|
||||
-- 2. Idempotência: se já é membro do tenant, só marca invite aceito.
|
||||
SELECT id, role, status
|
||||
INTO v_existing_member
|
||||
FROM public.tenant_members
|
||||
WHERE tenant_id = v_invite.tenant_id
|
||||
AND user_id = v_uid
|
||||
LIMIT 1;
|
||||
|
||||
IF v_existing_member.id IS NULL THEN
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status)
|
||||
VALUES (v_invite.tenant_id, v_uid, v_invite.role, 'active');
|
||||
ELSIF v_existing_member.status <> 'active' THEN
|
||||
UPDATE public.tenant_members
|
||||
SET status = 'active', role = v_invite.role
|
||||
WHERE id = v_existing_member.id;
|
||||
END IF;
|
||||
-- (se já está ativo, deixa como tá — convite aceito não rebaixa)
|
||||
|
||||
-- 3. Marca invite como aceito
|
||||
UPDATE public.tenant_invites
|
||||
SET accepted_at = now(), accepted_by = v_uid
|
||||
WHERE id = v_invite.id;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', true,
|
||||
'tenant_id', v_invite.tenant_id,
|
||||
'role', v_invite.role
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.accept_tenant_invite(uuid) IS
|
||||
'Aceita convite de membership. SECURITY DEFINER pra criar tenant_members em nome do user logado. Lock FOR UPDATE no invite previne race condition.';
|
||||
|
||||
-- Permite que qualquer authenticated chame (precisa do token UUID válido pra entrar).
|
||||
REVOKE ALL ON FUNCTION public.accept_tenant_invite(uuid) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.accept_tenant_invite(uuid) TO authenticated;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user