create-bw-app 0.24.8 → 0.25.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.
Files changed (32) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/src/bw.mjs +1 -1
  4. package/src/constants.mjs +14 -9
  5. package/src/generator.mjs +3 -6
  6. package/src/migrations.mjs +43 -5
  7. package/src/upgrade.mjs +54 -2
  8. package/template/base/app/(shell)/dashboard/dashboard-page-mount.tsx +7 -0
  9. package/template/base/app/(shell)/dashboard/page.tsx +1 -1
  10. package/template/base/app/(shell)/shell-layout-client.tsx +28 -7
  11. package/template/modules/crm/app/(shell)/crm/organizations/[id]/page.tsx +1 -1
  12. package/template/modules/crm/app/(shell)/crm/organizations/page.tsx +1 -3
  13. package/template/modules/crm/app/(shell)/crm/page.tsx +1 -5
  14. package/template/modules/crm/app/(shell)/crm/report/page.tsx +1 -5
  15. package/template/modules/projects/app/(shell)/account/page.tsx +5 -1
  16. package/template/modules/projects/app/(shell)/account/perfil/page.tsx +1 -0
  17. package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +3 -2
  18. package/template/modules/projects/app/api/account/projects/[id]/route.ts +2 -0
  19. package/template/modules/projects/app/api/account/projects/route.ts +2 -0
  20. package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +2 -0
  21. package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +2 -0
  22. package/template/modules/projects/app/api/projects/_handlers.ts +24 -0
  23. package/template/modules/projects/app/api/projects/setup-options/route.ts +2 -0
  24. package/template/supabase/modules/orgs/migrations/20260812120000_staff_only_organization_access_management.sql +7 -0
  25. package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql +328 -0
  26. package/template/supabase/modules/projects/migrations/20260811120000_project_client_access_expand.sql +1061 -0
  27. package/template/supabase/modules/projects/migrations/20260811120500_project_member_sync_hardening.sql +156 -0
  28. package/template/supabase/modules/projects/migrations/20260811121000_project_client_access_enforcement.sql +651 -0
  29. package/template/supabase/modules/projects/migrations/20260811121500_project_client_organization_memberships.sql +29 -0
  30. package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql +193 -0
  31. package/template/supabase/modules/projects/migrations/20260811122000_project_client_access_identity_cleanup.sql +201 -0
  32. package/template/supabase/modules/projects/migrations/20260811122500_remove_project_client_next_steps.sql +681 -0
@@ -0,0 +1,156 @@
1
+ -- Make project team and owner changes a single explicitly authorized operation.
2
+
3
+ CREATE OR REPLACE FUNCTION public.sync_project_members_exact(
4
+ p_project_id uuid,
5
+ p_members jsonb,
6
+ p_owner_profile_id uuid DEFAULT NULL
7
+ )
8
+ RETURNS void
9
+ LANGUAGE plpgsql
10
+ SECURITY DEFINER
11
+ SET search_path = public
12
+ AS $$
13
+ DECLARE
14
+ v_actor_profile_id uuid := public.current_profile_id();
15
+ v_current_owner_profile_id uuid;
16
+ v_client_contact_profile_id uuid;
17
+ v_members jsonb := COALESCE(p_members, '[]'::jsonb);
18
+ BEGIN
19
+ IF jsonb_typeof(v_members) <> 'array' THEN
20
+ RAISE EXCEPTION 'Project members must be a JSON array.' USING ERRCODE = '22023';
21
+ END IF;
22
+
23
+ SELECT project.owner_profile_id, project.client_contact_profile_id
24
+ INTO v_current_owner_profile_id, v_client_contact_profile_id
25
+ FROM public.projects project
26
+ WHERE project.id = p_project_id
27
+ FOR UPDATE;
28
+ IF NOT FOUND THEN
29
+ RAISE EXCEPTION 'Projeto não encontrado.' USING ERRCODE = 'P0002';
30
+ END IF;
31
+
32
+ IF NOT (
33
+ public.is_admin()
34
+ OR (
35
+ public.is_staff()
36
+ AND (
37
+ v_current_owner_profile_id = v_actor_profile_id
38
+ OR EXISTS (
39
+ SELECT 1
40
+ FROM public.project_members member
41
+ WHERE member.project_id = p_project_id
42
+ AND member.profile_id = v_actor_profile_id
43
+ AND member.role = 'owner'
44
+ )
45
+ )
46
+ )
47
+ ) THEN
48
+ RAISE EXCEPTION 'Only project owners or administrators can manage the internal project team.'
49
+ USING ERRCODE = '42501';
50
+ END IF;
51
+
52
+ IF EXISTS (
53
+ SELECT 1
54
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
55
+ WHERE member.profile_id IS NULL
56
+ OR member.role NOT IN ('owner', 'contributor', 'observer')
57
+ ) THEN
58
+ RAISE EXCEPTION 'Every project member requires a valid profile and role.'
59
+ USING ERRCODE = '22023';
60
+ END IF;
61
+
62
+ IF (
63
+ SELECT count(*) <> count(DISTINCT member.profile_id)
64
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
65
+ ) THEN
66
+ RAISE EXCEPTION 'Each internal profile can appear only once in the project team.'
67
+ USING ERRCODE = '22023';
68
+ END IF;
69
+
70
+ IF (SELECT count(*) FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text) WHERE member.role = 'owner') <> 1
71
+ OR NOT EXISTS (
72
+ SELECT 1
73
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
74
+ WHERE member.profile_id = p_owner_profile_id
75
+ AND member.role = 'owner'
76
+ ) THEN
77
+ RAISE EXCEPTION 'Exactly one internal project owner is required and must match the project owner.'
78
+ USING ERRCODE = '22023';
79
+ END IF;
80
+
81
+ IF EXISTS (
82
+ SELECT 1
83
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
84
+ LEFT JOIN public.user_role_assignments role_assignment
85
+ ON role_assignment.profile_id = member.profile_id
86
+ AND role_assignment.role_code IN ('staff', 'admin')
87
+ WHERE role_assignment.id IS NULL
88
+ ) THEN
89
+ RAISE EXCEPTION 'Project members must be internal staff.' USING ERRCODE = '22023';
90
+ END IF;
91
+
92
+ IF v_client_contact_profile_id IS NOT NULL AND NOT EXISTS (
93
+ SELECT 1
94
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
95
+ WHERE member.profile_id = v_client_contact_profile_id
96
+ ) THEN
97
+ RAISE EXCEPTION 'The responsible client contact must remain in the internal project team.'
98
+ USING ERRCODE = '22023';
99
+ END IF;
100
+
101
+ INSERT INTO public.project_members(project_id, profile_id, role)
102
+ SELECT p_project_id, member.profile_id, member.role
103
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
104
+ ON CONFLICT (project_id, profile_id) DO UPDATE SET role = EXCLUDED.role;
105
+
106
+ DELETE FROM public.project_members existing
107
+ WHERE existing.project_id = p_project_id
108
+ AND NOT EXISTS (
109
+ SELECT 1
110
+ FROM jsonb_to_recordset(v_members) AS member(profile_id uuid, role text)
111
+ WHERE member.profile_id = existing.profile_id
112
+ );
113
+
114
+ UPDATE public.projects
115
+ SET owner_profile_id = p_owner_profile_id
116
+ WHERE id = p_project_id;
117
+ END;
118
+ $$;
119
+
120
+ REVOKE ALL ON FUNCTION public.sync_project_members_exact(uuid, jsonb, uuid)
121
+ FROM PUBLIC, anon, service_role;
122
+ GRANT EXECUTE ON FUNCTION public.sync_project_members_exact(uuid, jsonb, uuid)
123
+ TO authenticated;
124
+
125
+ CREATE OR REPLACE FUNCTION public.prevent_internal_project_role_downgrade()
126
+ RETURNS trigger
127
+ LANGUAGE plpgsql
128
+ SECURITY DEFINER
129
+ SET search_path = public
130
+ AS $$
131
+ BEGIN
132
+ IF OLD.role_code IN ('staff', 'admin')
133
+ AND (TG_OP = 'DELETE' OR NEW.role_code NOT IN ('staff', 'admin'))
134
+ AND (
135
+ EXISTS (
136
+ SELECT 1 FROM public.projects project
137
+ WHERE project.owner_profile_id = OLD.profile_id
138
+ )
139
+ OR EXISTS (
140
+ SELECT 1 FROM public.project_members member
141
+ WHERE member.profile_id = OLD.profile_id
142
+ )
143
+ ) THEN
144
+ RAISE EXCEPTION 'Reassign or remove this profile from every project before changing its role to client.'
145
+ USING ERRCODE = '23514';
146
+ END IF;
147
+ RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
148
+ END;
149
+ $$;
150
+
151
+ DROP TRIGGER IF EXISTS trg_prevent_internal_project_role_downgrade
152
+ ON public.user_role_assignments;
153
+ CREATE TRIGGER trg_prevent_internal_project_role_downgrade
154
+ BEFORE UPDATE OF role_code OR DELETE ON public.user_role_assignments
155
+ FOR EACH ROW
156
+ EXECUTE FUNCTION public.prevent_internal_project_role_downgrade();