@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
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.9.0-next.0",
6
+ "version": "0.9.0",
7
7
  "description": "Fayz SDK database layer — Drizzle schema primitives, spine references, and migration helpers shared across plugins.",
8
8
  "type": "module",
9
9
  "sideEffects": false,