create-bw-app 0.24.9 → 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,328 @@
1
+ -- Explicit client audiences and staff/client data boundaries for projects.
2
+
3
+ ALTER TABLE public.projects
4
+ ADD COLUMN IF NOT EXISTS archived_at timestamptz,
5
+ ADD COLUMN IF NOT EXISTS client_access_mode text NOT NULL DEFAULT 'hidden';
6
+
7
+ ALTER TABLE public.projects
8
+ DROP CONSTRAINT IF EXISTS projects_status_check;
9
+ ALTER TABLE public.projects
10
+ ADD CONSTRAINT projects_status_check
11
+ CHECK (status IN ('planned', 'active', 'paused', 'blocked', 'completed', 'canceled'));
12
+
13
+ ALTER TABLE public.projects
14
+ DROP CONSTRAINT IF EXISTS projects_client_access_mode_check;
15
+ ALTER TABLE public.projects
16
+ ADD CONSTRAINT projects_client_access_mode_check
17
+ CHECK (client_access_mode IN ('hidden', 'all_org_clients', 'selected_clients'));
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_projects_archived_at
20
+ ON public.projects (archived_at)
21
+ WHERE archived_at IS NOT NULL;
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_projects_client_access_mode
24
+ ON public.projects (client_access_mode)
25
+ WHERE client_access_mode <> 'hidden';
26
+
27
+ ALTER TABLE public.project_status_log
28
+ DROP CONSTRAINT IF EXISTS project_status_log_new_status_check;
29
+ ALTER TABLE public.project_status_log
30
+ ADD CONSTRAINT project_status_log_new_status_check
31
+ CHECK (new_status IN ('planned', 'active', 'paused', 'blocked', 'completed', 'canceled'));
32
+
33
+ CREATE OR REPLACE FUNCTION public.sync_project_lifecycle_dates()
34
+ RETURNS trigger
35
+ LANGUAGE plpgsql
36
+ SECURITY DEFINER
37
+ SET search_path = public
38
+ AS $$
39
+ BEGIN
40
+ IF NEW.status IN ('active', 'blocked', 'paused', 'completed')
41
+ AND NEW.activated_at IS NULL THEN
42
+ NEW.activated_at := now();
43
+ END IF;
44
+
45
+ IF NEW.status = 'completed'
46
+ AND NEW.completed_at IS NULL THEN
47
+ NEW.completed_at := now();
48
+ END IF;
49
+
50
+ RETURN NEW;
51
+ END;
52
+ $$;
53
+
54
+ ALTER TABLE public.project_milestones
55
+ ADD COLUMN IF NOT EXISTS visibility text NOT NULL DEFAULT 'staff';
56
+
57
+ ALTER TABLE public.project_milestones
58
+ DROP CONSTRAINT IF EXISTS project_milestones_visibility_check;
59
+ ALTER TABLE public.project_milestones
60
+ ADD CONSTRAINT project_milestones_visibility_check
61
+ CHECK (visibility IN ('staff', 'client'));
62
+
63
+ CREATE INDEX IF NOT EXISTS idx_project_milestones_client_visibility
64
+ ON public.project_milestones (project_id, position)
65
+ WHERE visibility = 'client';
66
+
67
+ CREATE TABLE IF NOT EXISTS public.project_client_organizations (
68
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
69
+ organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
70
+ added_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
71
+ added_at timestamptz NOT NULL DEFAULT now(),
72
+ PRIMARY KEY (project_id, organization_id)
73
+ );
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_project_client_organizations_org_project
76
+ ON public.project_client_organizations (organization_id, project_id);
77
+
78
+ CREATE TABLE IF NOT EXISTS public.project_client_profiles (
79
+ project_id uuid NOT NULL,
80
+ organization_id uuid NOT NULL,
81
+ profile_id uuid NOT NULL,
82
+ granted_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
83
+ granted_at timestamptz NOT NULL DEFAULT now(),
84
+ PRIMARY KEY (project_id, organization_id, profile_id),
85
+ CONSTRAINT project_client_profiles_project_org_fkey
86
+ FOREIGN KEY (project_id, organization_id)
87
+ REFERENCES public.project_client_organizations(project_id, organization_id)
88
+ ON DELETE CASCADE,
89
+ CONSTRAINT project_client_profiles_org_membership_fkey
90
+ FOREIGN KEY (organization_id, profile_id)
91
+ REFERENCES public.organization_members(organization_id, profile_id)
92
+ ON DELETE CASCADE
93
+ );
94
+
95
+ CREATE INDEX IF NOT EXISTS idx_project_client_profiles_profile_project
96
+ ON public.project_client_profiles (profile_id, project_id);
97
+
98
+ ALTER TABLE public.project_client_organizations ENABLE ROW LEVEL SECURITY;
99
+ ALTER TABLE public.project_client_profiles ENABLE ROW LEVEL SECURITY;
100
+
101
+ DROP POLICY IF EXISTS "Staff manage project client organizations"
102
+ ON public.project_client_organizations;
103
+ CREATE POLICY "Staff manage project client organizations"
104
+ ON public.project_client_organizations
105
+ FOR ALL
106
+ TO authenticated
107
+ USING (
108
+ public.is_admin()
109
+ OR (
110
+ public.is_staff()
111
+ AND public.is_project_owner_member(project_id)
112
+ )
113
+ )
114
+ WITH CHECK (
115
+ public.is_admin()
116
+ OR (
117
+ public.is_staff()
118
+ AND public.is_project_owner_member(project_id)
119
+ )
120
+ );
121
+
122
+ DROP POLICY IF EXISTS "Staff manage project client profiles"
123
+ ON public.project_client_profiles;
124
+ CREATE POLICY "Staff manage project client profiles"
125
+ ON public.project_client_profiles
126
+ FOR ALL
127
+ TO authenticated
128
+ USING (
129
+ public.is_admin()
130
+ OR (
131
+ public.is_staff()
132
+ AND public.is_project_owner_member(project_id)
133
+ )
134
+ )
135
+ WITH CHECK (
136
+ public.is_admin()
137
+ OR (
138
+ public.is_staff()
139
+ AND public.is_project_owner_member(project_id)
140
+ )
141
+ );
142
+
143
+ -- Existing projects retain their primary organization as the initial audience
144
+ -- candidate, but hidden remains the safe default until staff explicitly shares.
145
+ INSERT INTO public.project_client_organizations (project_id, organization_id)
146
+ SELECT project.id, project.organization_id
147
+ FROM public.projects project
148
+ ON CONFLICT (project_id, organization_id) DO NOTHING;
149
+
150
+ CREATE OR REPLACE FUNCTION public.can_current_client_view_project(target_project_id uuid)
151
+ RETURNS boolean
152
+ LANGUAGE sql
153
+ STABLE
154
+ SECURITY DEFINER
155
+ SET search_path = public
156
+ AS $$
157
+ SELECT COALESCE(public.current_global_role() = 'client', false)
158
+ AND EXISTS (
159
+ SELECT 1
160
+ FROM public.projects project
161
+ JOIN public.project_client_organizations client_org
162
+ ON client_org.project_id = project.id
163
+ JOIN public.organization_members org_member
164
+ ON org_member.organization_id = client_org.organization_id
165
+ AND org_member.profile_id = public.current_profile_id()
166
+ WHERE project.id = target_project_id
167
+ AND project.client_access_mode <> 'hidden'
168
+ AND (
169
+ project.client_access_mode = 'all_org_clients'
170
+ OR (
171
+ project.client_access_mode = 'selected_clients'
172
+ AND EXISTS (
173
+ SELECT 1
174
+ FROM public.project_client_profiles client_profile
175
+ WHERE client_profile.project_id = project.id
176
+ AND client_profile.organization_id = client_org.organization_id
177
+ AND client_profile.profile_id = public.current_profile_id()
178
+ )
179
+ )
180
+ )
181
+ )
182
+ $$;
183
+
184
+ REVOKE ALL ON FUNCTION public.can_current_client_view_project(uuid)
185
+ FROM PUBLIC, anon;
186
+ GRANT EXECUTE ON FUNCTION public.can_current_client_view_project(uuid)
187
+ TO authenticated, service_role;
188
+
189
+ CREATE OR REPLACE FUNCTION public.set_project_client_access(
190
+ target_project_id uuid,
191
+ target_mode text,
192
+ target_organization_ids uuid[],
193
+ target_profile_grants jsonb
194
+ )
195
+ RETURNS void
196
+ LANGUAGE plpgsql
197
+ SECURITY DEFINER
198
+ SET search_path = public
199
+ AS $$
200
+ DECLARE
201
+ v_actor_profile_id uuid := public.current_profile_id();
202
+ v_organization_ids uuid[] := COALESCE(target_organization_ids, ARRAY[]::uuid[]);
203
+ v_profile_grants jsonb := COALESCE(target_profile_grants, '[]'::jsonb);
204
+ BEGIN
205
+ IF target_mode IS NULL
206
+ OR target_mode NOT IN ('hidden', 'all_org_clients', 'selected_clients') THEN
207
+ RAISE EXCEPTION 'Invalid project client access mode.'
208
+ USING ERRCODE = '22023';
209
+ END IF;
210
+
211
+ IF jsonb_typeof(v_profile_grants) <> 'array' THEN
212
+ RAISE EXCEPTION 'Profile grants must be a JSON array.'
213
+ USING ERRCODE = '22023';
214
+ END IF;
215
+
216
+ PERFORM 1
217
+ FROM public.projects
218
+ WHERE id = target_project_id
219
+ FOR UPDATE;
220
+ IF NOT FOUND THEN
221
+ RAISE EXCEPTION 'Projeto não encontrado.'
222
+ USING ERRCODE = 'P0002';
223
+ END IF;
224
+
225
+ IF NOT (
226
+ public.is_admin()
227
+ OR (
228
+ public.is_staff()
229
+ AND public.is_project_owner_member(target_project_id)
230
+ )
231
+ ) THEN
232
+ RAISE EXCEPTION 'Only project owners or administrators can manage client project access.'
233
+ USING ERRCODE = '42501';
234
+ END IF;
235
+
236
+ IF EXISTS (
237
+ SELECT 1
238
+ FROM unnest(v_organization_ids) AS selected(organization_id)
239
+ WHERE selected.organization_id IS NULL
240
+ ) THEN
241
+ RAISE EXCEPTION 'Organization IDs cannot contain null values.'
242
+ USING ERRCODE = '22023';
243
+ END IF;
244
+
245
+ IF EXISTS (
246
+ SELECT 1
247
+ FROM unnest(v_organization_ids) AS selected(organization_id)
248
+ LEFT JOIN public.organizations organization
249
+ ON organization.id = selected.organization_id
250
+ WHERE organization.id IS NULL
251
+ ) THEN
252
+ RAISE EXCEPTION 'One or more organizations do not exist.'
253
+ USING ERRCODE = '22023';
254
+ END IF;
255
+
256
+ IF target_mode <> 'hidden'
257
+ AND COALESCE(array_length(v_organization_ids, 1), 0) = 0 THEN
258
+ RAISE EXCEPTION 'Visible client access requires at least one organization.'
259
+ USING ERRCODE = '22023';
260
+ END IF;
261
+
262
+ IF target_mode = 'selected_clients'
263
+ AND jsonb_array_length(v_profile_grants) = 0 THEN
264
+ RAISE EXCEPTION 'Selected-clients mode requires at least one profile grant.'
265
+ USING ERRCODE = '22023';
266
+ END IF;
267
+
268
+ IF target_mode = 'selected_clients'
269
+ AND EXISTS (
270
+ SELECT 1
271
+ FROM jsonb_to_recordset(v_profile_grants)
272
+ AS access_grant(organization_id uuid, profile_id uuid)
273
+ LEFT JOIN public.organization_members org_member
274
+ ON org_member.organization_id = access_grant.organization_id
275
+ AND org_member.profile_id = access_grant.profile_id
276
+ LEFT JOIN public.user_role_assignments role_assignment
277
+ ON role_assignment.profile_id = access_grant.profile_id
278
+ AND role_assignment.role_code = 'client'
279
+ WHERE access_grant.organization_id IS NULL
280
+ OR access_grant.profile_id IS NULL
281
+ OR NOT (access_grant.organization_id = ANY(v_organization_ids))
282
+ OR org_member.id IS NULL
283
+ OR role_assignment.id IS NULL
284
+ ) THEN
285
+ RAISE EXCEPTION 'Every selected profile must be an active client member of a selected organization.'
286
+ USING ERRCODE = '22023';
287
+ END IF;
288
+
289
+ DELETE FROM public.project_client_organizations
290
+ WHERE project_id = target_project_id;
291
+
292
+ INSERT INTO public.project_client_organizations (
293
+ project_id,
294
+ organization_id,
295
+ added_by_profile_id
296
+ )
297
+ SELECT target_project_id, organization_id, v_actor_profile_id
298
+ FROM (
299
+ SELECT DISTINCT selected.organization_id
300
+ FROM unnest(v_organization_ids) AS selected(organization_id)
301
+ ) selected_organizations;
302
+
303
+ IF target_mode = 'selected_clients' THEN
304
+ INSERT INTO public.project_client_profiles (
305
+ project_id,
306
+ organization_id,
307
+ profile_id,
308
+ granted_by_profile_id
309
+ )
310
+ SELECT DISTINCT
311
+ target_project_id,
312
+ access_grant.organization_id,
313
+ access_grant.profile_id,
314
+ v_actor_profile_id
315
+ FROM jsonb_to_recordset(v_profile_grants)
316
+ AS access_grant(organization_id uuid, profile_id uuid);
317
+ END IF;
318
+
319
+ UPDATE public.projects
320
+ SET client_access_mode = target_mode
321
+ WHERE id = target_project_id;
322
+ END;
323
+ $$;
324
+
325
+ REVOKE ALL ON FUNCTION public.set_project_client_access(uuid, text, uuid[], jsonb)
326
+ FROM PUBLIC, anon;
327
+ GRANT EXECUTE ON FUNCTION public.set_project_client_access(uuid, text, uuid[], jsonb)
328
+ TO authenticated;