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.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/bw.mjs +1 -1
- package/src/constants.mjs +14 -9
- package/src/generator.mjs +3 -6
- package/src/migrations.mjs +43 -5
- package/src/upgrade.mjs +54 -2
- package/template/base/app/(shell)/dashboard/dashboard-page-mount.tsx +7 -0
- package/template/base/app/(shell)/dashboard/page.tsx +1 -1
- package/template/base/app/(shell)/shell-layout-client.tsx +28 -7
- package/template/modules/crm/app/(shell)/crm/organizations/[id]/page.tsx +1 -1
- package/template/modules/crm/app/(shell)/crm/organizations/page.tsx +1 -3
- package/template/modules/crm/app/(shell)/crm/page.tsx +1 -5
- package/template/modules/crm/app/(shell)/crm/report/page.tsx +1 -5
- package/template/modules/projects/app/(shell)/account/page.tsx +5 -1
- package/template/modules/projects/app/(shell)/account/perfil/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +3 -2
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +2 -0
- package/template/modules/projects/app/api/account/projects/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +2 -0
- package/template/modules/projects/app/api/projects/_handlers.ts +24 -0
- package/template/modules/projects/app/api/projects/setup-options/route.ts +2 -0
- package/template/supabase/modules/orgs/migrations/20260812120000_staff_only_organization_access_management.sql +7 -0
- package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql +328 -0
- package/template/supabase/modules/projects/migrations/20260811120000_project_client_access_expand.sql +1061 -0
- package/template/supabase/modules/projects/migrations/20260811120500_project_member_sync_hardening.sql +156 -0
- package/template/supabase/modules/projects/migrations/20260811121000_project_client_access_enforcement.sql +651 -0
- package/template/supabase/modules/projects/migrations/20260811121500_project_client_organization_memberships.sql +29 -0
- package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql +193 -0
- package/template/supabase/modules/projects/migrations/20260811122000_project_client_access_identity_cleanup.sql +201 -0
- package/template/supabase/modules/projects/migrations/20260811122500_remove_project_client_next_steps.sql +681 -0
package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql
ADDED
|
@@ -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;
|