@fayz-ai/db 0.9.0-next.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 022_invite_acceptance.sql — o convite finalmente vira membro da org.
|
|
3
|
+
--
|
|
4
|
+
-- `createInvite` sempre gravou a linha em `invitations` e mandou o magic link,
|
|
5
|
+
-- mas NADA nunca converteu essa linha em `tenant_members`. O convidado entrava,
|
|
6
|
+
-- `listUserOrgs` voltava vazio, e o autoCreate do OrgProvider (ligado por
|
|
7
|
+
-- padrão) criava uma "My Organization" nova com role `owner` — a org que
|
|
8
|
+
-- convidou caía no chão em silêncio.
|
|
9
|
+
--
|
|
10
|
+
-- Duas mudanças:
|
|
11
|
+
--
|
|
12
|
+
-- 1. public.accept_pending_invitations() — o resgate que faltava. O client
|
|
13
|
+
-- chama logo depois do sign-in, antes de listar as orgs.
|
|
14
|
+
--
|
|
15
|
+
-- A fonte é a linha de `invitations`, NÃO `auth.users.raw_user_meta_data`.
|
|
16
|
+
-- O metadata também carrega tenant_id/role, mas vem de um
|
|
17
|
+
-- `signInWithOtp({ data })` do browser — qualquer um com a anon key se
|
|
18
|
+
-- carimbaria dentro de qualquer tenant. `invitations` é RLS-gated a admin
|
|
19
|
+
-- do tenant, então é o único sinal confiável.
|
|
20
|
+
--
|
|
21
|
+
-- 2. `invites_select` deixa de ser `USING (true)`. Num pool de indústria
|
|
22
|
+
-- compartilhado isso deixava qualquer usuário autenticado ler a lista de
|
|
23
|
+
-- convites de todos os tenants — e-mails inclusive. Agora: os seus
|
|
24
|
+
-- tenants, mais os convites endereçados a você (que o resgate precisa ver).
|
|
25
|
+
--
|
|
26
|
+
-- Idempotente: seguro re-rodar, no-op quando já aplicada.
|
|
27
|
+
-- ============================================================================
|
|
28
|
+
|
|
29
|
+
CREATE OR REPLACE FUNCTION public.accept_pending_invitations()
|
|
30
|
+
RETURNS integer
|
|
31
|
+
LANGUAGE plpgsql
|
|
32
|
+
SECURITY DEFINER
|
|
33
|
+
SET search_path = public
|
|
34
|
+
AS $$
|
|
35
|
+
DECLARE
|
|
36
|
+
v_uid uuid := auth.uid();
|
|
37
|
+
v_email text;
|
|
38
|
+
v_count integer := 0;
|
|
39
|
+
r record;
|
|
40
|
+
BEGIN
|
|
41
|
+
IF v_uid IS NULL THEN
|
|
42
|
+
RETURN 0;
|
|
43
|
+
END IF;
|
|
44
|
+
|
|
45
|
+
-- Lido de auth.users, não do JWT: sobrevive a token sem claim de e-mail.
|
|
46
|
+
SELECT u.email INTO v_email FROM auth.users u WHERE u.id = v_uid;
|
|
47
|
+
IF v_email IS NULL OR v_email = '' THEN
|
|
48
|
+
RETURN 0;
|
|
49
|
+
END IF;
|
|
50
|
+
|
|
51
|
+
FOR r IN
|
|
52
|
+
SELECT i.id, i.tenant_id, i.role, i.location_ids
|
|
53
|
+
FROM public.invitations i
|
|
54
|
+
WHERE lower(i.email) = lower(v_email)
|
|
55
|
+
AND i.status = 'pending'
|
|
56
|
+
AND i.expires_at > now()
|
|
57
|
+
ORDER BY i.created_at
|
|
58
|
+
LOOP
|
|
59
|
+
-- DO NOTHING e não DO UPDATE: um convite nunca troca o papel de quem já é membro.
|
|
60
|
+
INSERT INTO public.tenant_members (tenant_id, user_id, role)
|
|
61
|
+
VALUES (r.tenant_id, v_uid, r.role)
|
|
62
|
+
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
|
63
|
+
|
|
64
|
+
IF r.location_ids IS NOT NULL AND array_length(r.location_ids, 1) > 0 THEN
|
|
65
|
+
INSERT INTO public.location_members (location_id, user_id, role)
|
|
66
|
+
SELECT l.id, v_uid, r.role
|
|
67
|
+
FROM public.locations l
|
|
68
|
+
WHERE l.id = ANY(r.location_ids) AND l.tenant_id = r.tenant_id
|
|
69
|
+
ON CONFLICT (location_id, user_id) DO NOTHING;
|
|
70
|
+
END IF;
|
|
71
|
+
|
|
72
|
+
UPDATE public.invitations
|
|
73
|
+
SET status = 'accepted', accepted_at = now()
|
|
74
|
+
WHERE id = r.id;
|
|
75
|
+
|
|
76
|
+
v_count := v_count + 1;
|
|
77
|
+
END LOOP;
|
|
78
|
+
|
|
79
|
+
RETURN v_count;
|
|
80
|
+
END; $$;
|
|
81
|
+
|
|
82
|
+
REVOKE ALL ON FUNCTION public.accept_pending_invitations() FROM public;
|
|
83
|
+
REVOKE ALL ON FUNCTION public.accept_pending_invitations() FROM anon;
|
|
84
|
+
GRANT EXECUTE ON FUNCTION public.accept_pending_invitations() TO authenticated, service_role;
|
|
85
|
+
|
|
86
|
+
DROP POLICY IF EXISTS "invites_select" ON public.invitations;
|
|
87
|
+
CREATE POLICY "invites_select" ON public.invitations FOR SELECT TO authenticated
|
|
88
|
+
USING (
|
|
89
|
+
tenant_id IN (SELECT public.user_tenant_ids())
|
|
90
|
+
OR lower(email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
|
91
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 023_team_visibility.sql — a equipe finalmente enxerga a equipe.
|
|
3
|
+
--
|
|
4
|
+
-- `001_core` fechou dois SELECTs no próprio usuário:
|
|
5
|
+
--
|
|
6
|
+
-- profiles_select USING (id = auth.uid()) -- "own profile only"
|
|
7
|
+
-- members_select USING (user_id = auth.uid()) -- "see own"
|
|
8
|
+
--
|
|
9
|
+
-- Com isso a tela de Equipe é impossível por construção: nenhum usuário, em
|
|
10
|
+
-- nenhum tenant, jamais vê mais de um membro. O sintoma muda com o modo da
|
|
11
|
+
-- TeamTab, mas a causa é a mesma:
|
|
12
|
+
--
|
|
13
|
+
-- • modo legado (lista vem de `listMembers` → tenant_members): "1 membro
|
|
14
|
+
-- nesta organização", sempre — só a própria linha atravessa a RLS.
|
|
15
|
+
--
|
|
16
|
+
-- • person-mode (lista vem de `people`, com o vínculo sobreposto a partir de
|
|
17
|
+
-- tenant_members): as pessoas aparecem, mas `p.membership` volta undefined
|
|
18
|
+
-- para todo mundo menos você → `hasAccess = false` → a linha exibe
|
|
19
|
+
-- "sem acesso" + botão Convidar MESMO para quem já tem conta e papel.
|
|
20
|
+
-- Quem tem acesso e quem não tem ficam indistinguíveis na tela.
|
|
21
|
+
--
|
|
22
|
+
-- Não é decisão de projeto que valha manter: o resto do próprio 001_core já
|
|
23
|
+
-- usa o padrão tenant-wide (`subs_select`, `invoices_select`,
|
|
24
|
+
-- `loc_members_select` todos fazem tenant_id IN (... WHERE user_id = auth.uid())).
|
|
25
|
+
-- Só members/profiles ficaram no `= auth.uid()`. Esta migration alinha os dois
|
|
26
|
+
-- com o padrão que o core já adota — não inventa política nova.
|
|
27
|
+
--
|
|
28
|
+
-- O alcance é deliberadamente o mínimo que a tela precisa: quem divide tenant
|
|
29
|
+
-- com você passa a ver seu nome, e-mail e avatar. Nada atravessa tenant.
|
|
30
|
+
--
|
|
31
|
+
-- EFEITO COLATERAL ESPERADO — seat cap. O limite 'users'
|
|
32
|
+
-- (limits-registry.ts → { key: 'users', table: 'tenant_members' }) conta linhas
|
|
33
|
+
-- de tenant_members, e hoje essa contagem volta 1 para todo mundo: o cap de
|
|
34
|
+
-- assentos nunca foi de fato aplicado. Depois desta migration ele passa a
|
|
35
|
+
-- contar certo, e tenants acima do plano vão bater no UpgradeModal ao convidar.
|
|
36
|
+
-- É a regra funcionando pela primeira vez, não uma regressão — mas conferir os
|
|
37
|
+
-- planos antes de aplicar em produção evita susto.
|
|
38
|
+
--
|
|
39
|
+
-- Idempotente: seguro re-rodar, no-op quando já aplicada.
|
|
40
|
+
-- ============================================================================
|
|
41
|
+
|
|
42
|
+
-- Os membros dos SEUS tenants, não só você.
|
|
43
|
+
--
|
|
44
|
+
-- `user_tenant_ids()` (002) é SECURITY DEFINER, então roda por fora da RLS e a
|
|
45
|
+
-- policy de tenant_members pode consultar tenant_members sem recursão. Trocar
|
|
46
|
+
-- por um EXISTS inline sobre a mesma tabela reintroduz o loop — não simplifique.
|
|
47
|
+
DROP POLICY IF EXISTS "members_select" ON public.tenant_members;
|
|
48
|
+
CREATE POLICY "members_select" ON public.tenant_members FOR SELECT TO authenticated
|
|
49
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
50
|
+
|
|
51
|
+
-- O profile de quem divide tenant com você. Mesmo motivo do SECURITY DEFINER
|
|
52
|
+
-- acima: a checagem lê tenant_members, que agora tem policy própria.
|
|
53
|
+
CREATE OR REPLACE FUNCTION public.shares_tenant_with(p_user_id uuid)
|
|
54
|
+
RETURNS boolean
|
|
55
|
+
LANGUAGE sql
|
|
56
|
+
STABLE
|
|
57
|
+
SECURITY DEFINER
|
|
58
|
+
SET search_path = public
|
|
59
|
+
AS $$
|
|
60
|
+
SELECT EXISTS (
|
|
61
|
+
SELECT 1
|
|
62
|
+
FROM public.tenant_members mine
|
|
63
|
+
JOIN public.tenant_members theirs ON theirs.tenant_id = mine.tenant_id
|
|
64
|
+
WHERE mine.user_id = auth.uid()
|
|
65
|
+
AND theirs.user_id = p_user_id
|
|
66
|
+
);
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
REVOKE ALL ON FUNCTION public.shares_tenant_with(uuid) FROM public;
|
|
70
|
+
REVOKE ALL ON FUNCTION public.shares_tenant_with(uuid) FROM anon;
|
|
71
|
+
GRANT EXECUTE ON FUNCTION public.shares_tenant_with(uuid) TO authenticated, service_role;
|
|
72
|
+
|
|
73
|
+
DROP POLICY IF EXISTS "profiles_select" ON public.profiles;
|
|
74
|
+
CREATE POLICY "profiles_select" ON public.profiles FOR SELECT TO authenticated
|
|
75
|
+
USING (id = auth.uid() OR public.shares_tenant_with(id));
|
package/package.json
CHANGED