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.
- 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
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
-- Install fail-closed project boundaries before any destructive identity
|
|
2
|
+
-- cleanup. This migration is safe to deploy while older project rows still
|
|
3
|
+
-- contain external observer memberships.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION public.enforce_project_client_content_manager()
|
|
6
|
+
RETURNS trigger
|
|
7
|
+
LANGUAGE plpgsql
|
|
8
|
+
SECURITY DEFINER
|
|
9
|
+
SET search_path = public
|
|
10
|
+
AS $$
|
|
11
|
+
BEGIN
|
|
12
|
+
IF NEW.client_contact_profile_id IS DISTINCT FROM OLD.client_contact_profile_id
|
|
13
|
+
AND NEW.client_contact_profile_id IS NOT NULL
|
|
14
|
+
AND NOT EXISTS (
|
|
15
|
+
SELECT 1
|
|
16
|
+
FROM public.project_members member
|
|
17
|
+
JOIN public.user_role_assignments role_assignment
|
|
18
|
+
ON role_assignment.profile_id = member.profile_id
|
|
19
|
+
AND role_assignment.role_code IN ('staff', 'admin')
|
|
20
|
+
WHERE member.project_id = NEW.id
|
|
21
|
+
AND member.profile_id = NEW.client_contact_profile_id
|
|
22
|
+
) THEN
|
|
23
|
+
RAISE EXCEPTION 'The client contact must be an internal project team member.'
|
|
24
|
+
USING ERRCODE = '23514';
|
|
25
|
+
END IF;
|
|
26
|
+
|
|
27
|
+
IF (
|
|
28
|
+
NEW.client_access_mode IS DISTINCT FROM OLD.client_access_mode
|
|
29
|
+
OR NEW.client_summary IS DISTINCT FROM OLD.client_summary
|
|
30
|
+
OR NEW.client_scope IS DISTINCT FROM OLD.client_scope
|
|
31
|
+
OR NEW.client_contact_profile_id IS DISTINCT FROM OLD.client_contact_profile_id
|
|
32
|
+
OR NEW.client_next_steps IS DISTINCT FROM OLD.client_next_steps
|
|
33
|
+
) AND NOT (
|
|
34
|
+
public.is_admin()
|
|
35
|
+
OR (public.is_staff() AND public.is_project_owner_member(OLD.id))
|
|
36
|
+
) THEN
|
|
37
|
+
RAISE EXCEPTION 'Only project owners or administrators can publish client content.'
|
|
38
|
+
USING ERRCODE = '42501';
|
|
39
|
+
END IF;
|
|
40
|
+
RETURN NEW;
|
|
41
|
+
END;
|
|
42
|
+
$$;
|
|
43
|
+
|
|
44
|
+
DROP TRIGGER IF EXISTS trg_enforce_project_client_content_manager ON public.projects;
|
|
45
|
+
CREATE TRIGGER trg_enforce_project_client_content_manager
|
|
46
|
+
BEFORE UPDATE OF client_access_mode, client_summary, client_scope,
|
|
47
|
+
client_contact_profile_id, client_next_steps ON public.projects
|
|
48
|
+
FOR EACH ROW
|
|
49
|
+
EXECUTE FUNCTION public.enforce_project_client_content_manager();
|
|
50
|
+
|
|
51
|
+
CREATE OR REPLACE FUNCTION public.enforce_client_visible_project_item_manager()
|
|
52
|
+
RETURNS trigger
|
|
53
|
+
LANGUAGE plpgsql
|
|
54
|
+
SECURITY DEFINER
|
|
55
|
+
SET search_path = public
|
|
56
|
+
AS $$
|
|
57
|
+
DECLARE
|
|
58
|
+
v_project_id uuid := COALESCE(NEW.project_id, OLD.project_id);
|
|
59
|
+
v_previous_visibility text := CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE OLD.visibility END;
|
|
60
|
+
v_next_visibility text := CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE NEW.visibility END;
|
|
61
|
+
BEGIN
|
|
62
|
+
IF (v_previous_visibility = 'client' OR v_next_visibility = 'client')
|
|
63
|
+
AND NOT (
|
|
64
|
+
public.is_admin()
|
|
65
|
+
OR (public.is_staff() AND public.is_project_owner_member(v_project_id))
|
|
66
|
+
) THEN
|
|
67
|
+
RAISE EXCEPTION 'Only project owners or administrators can publish client-visible items.'
|
|
68
|
+
USING ERRCODE = '42501';
|
|
69
|
+
END IF;
|
|
70
|
+
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
71
|
+
END;
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
DROP TRIGGER IF EXISTS trg_enforce_client_milestone_manager ON public.project_milestones;
|
|
75
|
+
CREATE TRIGGER trg_enforce_client_milestone_manager
|
|
76
|
+
BEFORE INSERT OR UPDATE OR DELETE ON public.project_milestones
|
|
77
|
+
FOR EACH ROW
|
|
78
|
+
EXECUTE FUNCTION public.enforce_client_visible_project_item_manager();
|
|
79
|
+
|
|
80
|
+
DROP TRIGGER IF EXISTS trg_enforce_client_link_manager ON public.project_links;
|
|
81
|
+
CREATE TRIGGER trg_enforce_client_link_manager
|
|
82
|
+
BEFORE INSERT OR UPDATE OR DELETE ON public.project_links
|
|
83
|
+
FOR EACH ROW
|
|
84
|
+
EXECUTE FUNCTION public.enforce_client_visible_project_item_manager();
|
|
85
|
+
|
|
86
|
+
-- Primary contacts remain organization admins only. Project membership is
|
|
87
|
+
-- always an explicit internal staff decision.
|
|
88
|
+
CREATE OR REPLACE FUNCTION public.sync_org_primary_contact_admin_membership()
|
|
89
|
+
RETURNS trigger
|
|
90
|
+
LANGUAGE plpgsql
|
|
91
|
+
SECURITY DEFINER
|
|
92
|
+
SET search_path = public
|
|
93
|
+
AS $$
|
|
94
|
+
BEGIN
|
|
95
|
+
IF TG_OP = 'UPDATE' AND OLD.primary_contact_id IS DISTINCT FROM NEW.primary_contact_id THEN
|
|
96
|
+
IF OLD.primary_contact_id IS NOT NULL THEN
|
|
97
|
+
DELETE FROM public.organization_members
|
|
98
|
+
WHERE organization_id = NEW.id
|
|
99
|
+
AND profile_id = OLD.primary_contact_id
|
|
100
|
+
AND role = 'admin';
|
|
101
|
+
END IF;
|
|
102
|
+
END IF;
|
|
103
|
+
|
|
104
|
+
IF NEW.primary_contact_id IS NOT NULL THEN
|
|
105
|
+
INSERT INTO public.organization_members (organization_id, profile_id, role)
|
|
106
|
+
VALUES (NEW.id, NEW.primary_contact_id, 'admin')
|
|
107
|
+
ON CONFLICT (organization_id, profile_id)
|
|
108
|
+
DO UPDATE SET role = 'admin';
|
|
109
|
+
END IF;
|
|
110
|
+
RETURN NEW;
|
|
111
|
+
END;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
REVOKE ALL ON FUNCTION public.create_project_with_client_access(uuid, jsonb, uuid[], jsonb, jsonb)
|
|
115
|
+
FROM PUBLIC, anon, service_role;
|
|
116
|
+
REVOKE ALL ON FUNCTION public.set_project_client_configuration(uuid, jsonb)
|
|
117
|
+
FROM PUBLIC, anon, service_role;
|
|
118
|
+
REVOKE ALL ON FUNCTION public.get_project_access_configuration(uuid)
|
|
119
|
+
FROM PUBLIC, anon, service_role;
|
|
120
|
+
REVOKE ALL ON FUNCTION public.get_project_client_access_summary(uuid)
|
|
121
|
+
FROM PUBLIC, anon, service_role;
|
|
122
|
+
REVOKE ALL ON FUNCTION public.list_current_client_projects()
|
|
123
|
+
FROM PUBLIC, anon, service_role;
|
|
124
|
+
REVOKE ALL ON FUNCTION public.get_current_client_project(uuid)
|
|
125
|
+
FROM PUBLIC, anon, service_role;
|
|
126
|
+
|
|
127
|
+
-- RLS can decide which project rows a contributor may edit, but it cannot
|
|
128
|
+
-- safely express column-level identity rules. Protect ownership and the
|
|
129
|
+
-- primary organization before enabling the broader staff update policy.
|
|
130
|
+
CREATE OR REPLACE FUNCTION public.protect_project_identity_fields()
|
|
131
|
+
RETURNS trigger
|
|
132
|
+
LANGUAGE plpgsql
|
|
133
|
+
SECURITY DEFINER
|
|
134
|
+
SET search_path = public
|
|
135
|
+
AS $$
|
|
136
|
+
BEGIN
|
|
137
|
+
IF (
|
|
138
|
+
NEW.owner_profile_id IS DISTINCT FROM OLD.owner_profile_id
|
|
139
|
+
OR NEW.organization_id IS DISTINCT FROM OLD.organization_id
|
|
140
|
+
) AND NOT (
|
|
141
|
+
public.is_admin()
|
|
142
|
+
OR (
|
|
143
|
+
public.is_staff()
|
|
144
|
+
AND (
|
|
145
|
+
OLD.owner_profile_id = public.current_profile_id()
|
|
146
|
+
OR EXISTS (
|
|
147
|
+
SELECT 1
|
|
148
|
+
FROM public.project_members member
|
|
149
|
+
WHERE member.project_id = OLD.id
|
|
150
|
+
AND member.profile_id = public.current_profile_id()
|
|
151
|
+
AND member.role = 'owner'
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
) THEN
|
|
156
|
+
RAISE EXCEPTION 'Only project owners or administrators can change project ownership or the primary organization.'
|
|
157
|
+
USING ERRCODE = '42501';
|
|
158
|
+
END IF;
|
|
159
|
+
|
|
160
|
+
IF NEW.owner_profile_id IS NOT NULL AND NOT EXISTS (
|
|
161
|
+
SELECT 1
|
|
162
|
+
FROM public.user_role_assignments role_assignment
|
|
163
|
+
WHERE role_assignment.profile_id = NEW.owner_profile_id
|
|
164
|
+
AND role_assignment.role_code IN ('staff', 'admin')
|
|
165
|
+
) THEN
|
|
166
|
+
RAISE EXCEPTION 'Project owners must be internal staff.' USING ERRCODE = '23514';
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
RETURN NEW;
|
|
170
|
+
END;
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
DROP TRIGGER IF EXISTS trg_protect_project_identity_fields ON public.projects;
|
|
174
|
+
CREATE TRIGGER trg_protect_project_identity_fields
|
|
175
|
+
BEFORE UPDATE OF owner_profile_id, organization_id ON public.projects
|
|
176
|
+
FOR EACH ROW
|
|
177
|
+
EXECUTE FUNCTION public.protect_project_identity_fields();
|
|
178
|
+
|
|
179
|
+
-- Keep owner_profile_id and the internal owner membership atomic before the
|
|
180
|
+
-- later legacy cleanup runs. This blocks direct PostgREST owner promotion while
|
|
181
|
+
-- still allowing sync_project_members_exact() to update both in one transaction.
|
|
182
|
+
CREATE OR REPLACE FUNCTION public.validate_project_owner_consistency()
|
|
183
|
+
RETURNS trigger
|
|
184
|
+
LANGUAGE plpgsql
|
|
185
|
+
SECURITY DEFINER
|
|
186
|
+
SET search_path = public
|
|
187
|
+
AS $$
|
|
188
|
+
DECLARE
|
|
189
|
+
v_project_id uuid;
|
|
190
|
+
v_owner_profile_id uuid;
|
|
191
|
+
v_owner_count integer;
|
|
192
|
+
v_matching_owner_count integer;
|
|
193
|
+
BEGIN
|
|
194
|
+
v_project_id := CASE
|
|
195
|
+
WHEN TG_TABLE_NAME = 'projects' THEN COALESCE(NEW.id, OLD.id)
|
|
196
|
+
ELSE COALESCE(NEW.project_id, OLD.project_id)
|
|
197
|
+
END;
|
|
198
|
+
|
|
199
|
+
SELECT project.owner_profile_id
|
|
200
|
+
INTO v_owner_profile_id
|
|
201
|
+
FROM public.projects project
|
|
202
|
+
WHERE project.id = v_project_id;
|
|
203
|
+
IF NOT FOUND THEN
|
|
204
|
+
RETURN NULL;
|
|
205
|
+
END IF;
|
|
206
|
+
|
|
207
|
+
SELECT
|
|
208
|
+
count(*) FILTER (WHERE member.role = 'owner'),
|
|
209
|
+
count(*) FILTER (
|
|
210
|
+
WHERE member.role = 'owner'
|
|
211
|
+
AND member.profile_id = v_owner_profile_id
|
|
212
|
+
)
|
|
213
|
+
INTO v_owner_count, v_matching_owner_count
|
|
214
|
+
FROM public.project_members member
|
|
215
|
+
WHERE member.project_id = v_project_id;
|
|
216
|
+
|
|
217
|
+
IF (v_owner_profile_id IS NULL AND v_owner_count <> 0)
|
|
218
|
+
OR (v_owner_profile_id IS NOT NULL AND (v_owner_count <> 1 OR v_matching_owner_count <> 1)) THEN
|
|
219
|
+
RAISE EXCEPTION 'Project owner and owner membership must match exactly.'
|
|
220
|
+
USING ERRCODE = '23514';
|
|
221
|
+
END IF;
|
|
222
|
+
RETURN NULL;
|
|
223
|
+
END;
|
|
224
|
+
$$;
|
|
225
|
+
|
|
226
|
+
DROP TRIGGER IF EXISTS trg_validate_project_owner_from_project ON public.projects;
|
|
227
|
+
CREATE CONSTRAINT TRIGGER trg_validate_project_owner_from_project
|
|
228
|
+
AFTER INSERT OR UPDATE OF owner_profile_id ON public.projects
|
|
229
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
230
|
+
FOR EACH ROW
|
|
231
|
+
EXECUTE FUNCTION public.validate_project_owner_consistency();
|
|
232
|
+
|
|
233
|
+
DROP TRIGGER IF EXISTS trg_validate_project_owner_from_member ON public.project_members;
|
|
234
|
+
CREATE CONSTRAINT TRIGGER trg_validate_project_owner_from_member
|
|
235
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.project_members
|
|
236
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
237
|
+
FOR EACH ROW
|
|
238
|
+
EXECUTE FUNCTION public.validate_project_owner_consistency();
|
|
239
|
+
|
|
240
|
+
-- Explicit selected-client grants must not silently reactivate if a profile is
|
|
241
|
+
-- later changed back to the client role. Leaving the client role revokes the
|
|
242
|
+
-- approval and a project owner must select the profile again.
|
|
243
|
+
CREATE OR REPLACE FUNCTION public.clear_stale_project_client_profile_grants()
|
|
244
|
+
RETURNS trigger
|
|
245
|
+
LANGUAGE plpgsql
|
|
246
|
+
SECURITY DEFINER
|
|
247
|
+
SET search_path = public
|
|
248
|
+
AS $$
|
|
249
|
+
BEGIN
|
|
250
|
+
IF OLD.role_code = 'client'
|
|
251
|
+
AND (TG_OP = 'DELETE' OR NEW.role_code <> 'client') THEN
|
|
252
|
+
DELETE FROM public.project_client_profiles grant_row
|
|
253
|
+
WHERE grant_row.profile_id = OLD.profile_id;
|
|
254
|
+
END IF;
|
|
255
|
+
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
256
|
+
END;
|
|
257
|
+
$$;
|
|
258
|
+
|
|
259
|
+
DROP TRIGGER IF EXISTS trg_clear_stale_project_client_profile_grants
|
|
260
|
+
ON public.user_role_assignments;
|
|
261
|
+
CREATE TRIGGER trg_clear_stale_project_client_profile_grants
|
|
262
|
+
AFTER UPDATE OF role_code OR DELETE ON public.user_role_assignments
|
|
263
|
+
FOR EACH ROW
|
|
264
|
+
EXECUTE FUNCTION public.clear_stale_project_client_profile_grants();
|
|
265
|
+
|
|
266
|
+
-- Project membership is internal. Client access is represented only by the
|
|
267
|
+
-- explicit organization/profile audience tables above.
|
|
268
|
+
DROP POLICY IF EXISTS "Project team view projects" ON public.projects;
|
|
269
|
+
DROP POLICY IF EXISTS "Project staff view projects" ON public.projects;
|
|
270
|
+
CREATE POLICY "Project staff view projects"
|
|
271
|
+
ON public.projects
|
|
272
|
+
FOR SELECT
|
|
273
|
+
TO authenticated
|
|
274
|
+
USING (
|
|
275
|
+
public.is_admin()
|
|
276
|
+
OR (
|
|
277
|
+
public.is_staff()
|
|
278
|
+
AND (public.is_project_team_member(id) OR public.is_project_org_admin(id))
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
DROP POLICY IF EXISTS "Project team create projects" ON public.projects;
|
|
283
|
+
CREATE POLICY "Project staff create projects"
|
|
284
|
+
ON public.projects
|
|
285
|
+
FOR INSERT
|
|
286
|
+
TO authenticated
|
|
287
|
+
WITH CHECK (
|
|
288
|
+
public.is_admin()
|
|
289
|
+
OR (
|
|
290
|
+
public.is_staff()
|
|
291
|
+
AND (
|
|
292
|
+
owner_profile_id = public.current_profile_id()
|
|
293
|
+
OR public.can_create_projects_for_org(organization_id)
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
DROP POLICY IF EXISTS "Project team update projects" ON public.projects;
|
|
299
|
+
CREATE POLICY "Project staff update projects"
|
|
300
|
+
ON public.projects
|
|
301
|
+
FOR UPDATE
|
|
302
|
+
TO authenticated
|
|
303
|
+
USING (
|
|
304
|
+
public.is_admin()
|
|
305
|
+
OR (public.is_staff() AND public.is_project_content_editor(id))
|
|
306
|
+
)
|
|
307
|
+
WITH CHECK (
|
|
308
|
+
public.is_admin()
|
|
309
|
+
OR (public.is_staff() AND public.is_project_content_editor(id))
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
DROP POLICY IF EXISTS "Project owners delete projects" ON public.projects;
|
|
313
|
+
CREATE POLICY "Project staff delete projects"
|
|
314
|
+
ON public.projects
|
|
315
|
+
FOR DELETE
|
|
316
|
+
TO authenticated
|
|
317
|
+
USING (
|
|
318
|
+
public.is_admin()
|
|
319
|
+
OR (public.is_staff() AND public.is_project_owner_member(id))
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
DROP POLICY IF EXISTS "Project team view project members" ON public.project_members;
|
|
323
|
+
DROP POLICY IF EXISTS "Project staff view project members" ON public.project_members;
|
|
324
|
+
CREATE POLICY "Project staff view project members"
|
|
325
|
+
ON public.project_members
|
|
326
|
+
FOR SELECT
|
|
327
|
+
TO authenticated
|
|
328
|
+
USING (
|
|
329
|
+
public.is_admin()
|
|
330
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
DROP POLICY IF EXISTS "Project owners insert project members" ON public.project_members;
|
|
334
|
+
CREATE POLICY "Project staff insert project members"
|
|
335
|
+
ON public.project_members
|
|
336
|
+
FOR INSERT
|
|
337
|
+
TO authenticated
|
|
338
|
+
WITH CHECK (
|
|
339
|
+
public.is_admin()
|
|
340
|
+
OR (public.is_staff() AND public.is_project_owner_member(project_id))
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
DROP POLICY IF EXISTS "Project owners update project members" ON public.project_members;
|
|
344
|
+
CREATE POLICY "Project staff update project members"
|
|
345
|
+
ON public.project_members
|
|
346
|
+
FOR UPDATE
|
|
347
|
+
TO authenticated
|
|
348
|
+
USING (
|
|
349
|
+
public.is_admin()
|
|
350
|
+
OR (public.is_staff() AND public.is_project_owner_member(project_id))
|
|
351
|
+
)
|
|
352
|
+
WITH CHECK (
|
|
353
|
+
public.is_admin()
|
|
354
|
+
OR (public.is_staff() AND public.is_project_owner_member(project_id))
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
DROP POLICY IF EXISTS "Project owners delete project members" ON public.project_members;
|
|
358
|
+
CREATE POLICY "Project staff delete project members"
|
|
359
|
+
ON public.project_members
|
|
360
|
+
FOR DELETE
|
|
361
|
+
TO authenticated
|
|
362
|
+
USING (
|
|
363
|
+
public.is_admin()
|
|
364
|
+
OR (public.is_staff() AND public.is_project_owner_member(project_id))
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
DROP POLICY IF EXISTS "Project team view project milestones" ON public.project_milestones;
|
|
368
|
+
DROP POLICY IF EXISTS "Authorized users view project milestones" ON public.project_milestones;
|
|
369
|
+
CREATE POLICY "Authorized users view project milestones"
|
|
370
|
+
ON public.project_milestones
|
|
371
|
+
FOR SELECT
|
|
372
|
+
TO authenticated
|
|
373
|
+
USING (
|
|
374
|
+
public.is_admin()
|
|
375
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
376
|
+
OR (
|
|
377
|
+
visibility = 'client'
|
|
378
|
+
AND public.can_current_client_view_project(project_id)
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
DROP POLICY IF EXISTS "Project content editors insert project milestones" ON public.project_milestones;
|
|
383
|
+
CREATE POLICY "Project staff insert project milestones"
|
|
384
|
+
ON public.project_milestones
|
|
385
|
+
FOR INSERT
|
|
386
|
+
TO authenticated
|
|
387
|
+
WITH CHECK (
|
|
388
|
+
public.is_admin()
|
|
389
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
DROP POLICY IF EXISTS "Project content editors update project milestones" ON public.project_milestones;
|
|
393
|
+
CREATE POLICY "Project staff update project milestones"
|
|
394
|
+
ON public.project_milestones
|
|
395
|
+
FOR UPDATE
|
|
396
|
+
TO authenticated
|
|
397
|
+
USING (
|
|
398
|
+
public.is_admin()
|
|
399
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
400
|
+
)
|
|
401
|
+
WITH CHECK (
|
|
402
|
+
public.is_admin()
|
|
403
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
DROP POLICY IF EXISTS "Project content editors delete project milestones" ON public.project_milestones;
|
|
407
|
+
CREATE POLICY "Project staff delete project milestones"
|
|
408
|
+
ON public.project_milestones
|
|
409
|
+
FOR DELETE
|
|
410
|
+
TO authenticated
|
|
411
|
+
USING (
|
|
412
|
+
public.is_admin()
|
|
413
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
DROP POLICY IF EXISTS "Project team view project tasks" ON public.project_tasks;
|
|
417
|
+
DROP POLICY IF EXISTS "Project staff view project tasks" ON public.project_tasks;
|
|
418
|
+
CREATE POLICY "Project staff view project tasks"
|
|
419
|
+
ON public.project_tasks
|
|
420
|
+
FOR SELECT
|
|
421
|
+
TO authenticated
|
|
422
|
+
USING (
|
|
423
|
+
public.is_admin()
|
|
424
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
DROP POLICY IF EXISTS "Project content editors insert project tasks" ON public.project_tasks;
|
|
428
|
+
CREATE POLICY "Project staff insert project tasks"
|
|
429
|
+
ON public.project_tasks
|
|
430
|
+
FOR INSERT
|
|
431
|
+
TO authenticated
|
|
432
|
+
WITH CHECK (
|
|
433
|
+
public.is_admin()
|
|
434
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
DROP POLICY IF EXISTS "Project content editors update project tasks" ON public.project_tasks;
|
|
438
|
+
CREATE POLICY "Project staff update project tasks"
|
|
439
|
+
ON public.project_tasks
|
|
440
|
+
FOR UPDATE
|
|
441
|
+
TO authenticated
|
|
442
|
+
USING (
|
|
443
|
+
public.is_admin()
|
|
444
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
445
|
+
)
|
|
446
|
+
WITH CHECK (
|
|
447
|
+
public.is_admin()
|
|
448
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
DROP POLICY IF EXISTS "Project content editors delete project tasks" ON public.project_tasks;
|
|
452
|
+
CREATE POLICY "Project staff delete project tasks"
|
|
453
|
+
ON public.project_tasks
|
|
454
|
+
FOR DELETE
|
|
455
|
+
TO authenticated
|
|
456
|
+
USING (
|
|
457
|
+
public.is_admin()
|
|
458
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
DROP POLICY IF EXISTS "Project team view project links" ON public.project_links;
|
|
462
|
+
DROP POLICY IF EXISTS "Authorized users view project links" ON public.project_links;
|
|
463
|
+
CREATE POLICY "Authorized users view project links"
|
|
464
|
+
ON public.project_links
|
|
465
|
+
FOR SELECT
|
|
466
|
+
TO authenticated
|
|
467
|
+
USING (
|
|
468
|
+
public.is_admin()
|
|
469
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
470
|
+
OR (
|
|
471
|
+
visibility = 'client'
|
|
472
|
+
AND public.can_current_client_view_project(project_id)
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
DROP POLICY IF EXISTS "Project team create project links" ON public.project_links;
|
|
477
|
+
CREATE POLICY "Project staff create project links"
|
|
478
|
+
ON public.project_links
|
|
479
|
+
FOR INSERT
|
|
480
|
+
TO authenticated
|
|
481
|
+
WITH CHECK (
|
|
482
|
+
public.is_admin()
|
|
483
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
DROP POLICY IF EXISTS "Project content editors update project links" ON public.project_links;
|
|
487
|
+
CREATE POLICY "Project staff update project links"
|
|
488
|
+
ON public.project_links
|
|
489
|
+
FOR UPDATE
|
|
490
|
+
TO authenticated
|
|
491
|
+
USING (
|
|
492
|
+
public.is_admin()
|
|
493
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
494
|
+
)
|
|
495
|
+
WITH CHECK (
|
|
496
|
+
public.is_admin()
|
|
497
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
DROP POLICY IF EXISTS "Project content editors delete project links" ON public.project_links;
|
|
501
|
+
CREATE POLICY "Project staff delete project links"
|
|
502
|
+
ON public.project_links
|
|
503
|
+
FOR DELETE
|
|
504
|
+
TO authenticated
|
|
505
|
+
USING (
|
|
506
|
+
public.is_admin()
|
|
507
|
+
OR (public.is_staff() AND public.is_project_content_editor(project_id))
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
DROP POLICY IF EXISTS "Project team view project status log" ON public.project_status_log;
|
|
511
|
+
DROP POLICY IF EXISTS "Project staff view project status log" ON public.project_status_log;
|
|
512
|
+
CREATE POLICY "Project staff view project status log"
|
|
513
|
+
ON public.project_status_log
|
|
514
|
+
FOR SELECT
|
|
515
|
+
TO authenticated
|
|
516
|
+
USING (
|
|
517
|
+
public.is_admin()
|
|
518
|
+
OR (public.is_staff() AND public.is_project_team_member(project_id))
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
-- Notification and realtime activity audiences remain internal even when a
|
|
523
|
+
-- project is client-visible. Client-facing updates need a separate safe feed.
|
|
524
|
+
CREATE OR REPLACE FUNCTION public.can_profile_view_app_activity_event(
|
|
525
|
+
p_profile_id uuid,
|
|
526
|
+
p_domain text,
|
|
527
|
+
p_entity_table text,
|
|
528
|
+
p_entity_id uuid,
|
|
529
|
+
p_payload jsonb DEFAULT '{}'::jsonb
|
|
530
|
+
)
|
|
531
|
+
RETURNS boolean
|
|
532
|
+
LANGUAGE plpgsql
|
|
533
|
+
STABLE
|
|
534
|
+
SECURITY DEFINER
|
|
535
|
+
SET search_path = public
|
|
536
|
+
AS $$
|
|
537
|
+
DECLARE
|
|
538
|
+
v_project_id uuid;
|
|
539
|
+
v_role text;
|
|
540
|
+
BEGIN
|
|
541
|
+
IF p_profile_id IS NULL THEN
|
|
542
|
+
RETURN false;
|
|
543
|
+
END IF;
|
|
544
|
+
|
|
545
|
+
SELECT assignment.role_code
|
|
546
|
+
INTO v_role
|
|
547
|
+
FROM public.user_role_assignments assignment
|
|
548
|
+
WHERE assignment.profile_id = p_profile_id
|
|
549
|
+
LIMIT 1;
|
|
550
|
+
|
|
551
|
+
IF p_domain = 'admin' THEN
|
|
552
|
+
RETURN v_role = 'admin';
|
|
553
|
+
END IF;
|
|
554
|
+
|
|
555
|
+
IF p_domain <> 'projects' THEN
|
|
556
|
+
RETURN v_role = ANY (ARRAY['staff', 'admin']);
|
|
557
|
+
END IF;
|
|
558
|
+
|
|
559
|
+
IF v_role = 'admin' THEN
|
|
560
|
+
RETURN true;
|
|
561
|
+
END IF;
|
|
562
|
+
IF v_role <> 'staff' THEN
|
|
563
|
+
RETURN false;
|
|
564
|
+
END IF;
|
|
565
|
+
|
|
566
|
+
BEGIN
|
|
567
|
+
v_project_id := NULLIF(COALESCE(p_payload, '{}'::jsonb)->>'project_id', '')::uuid;
|
|
568
|
+
EXCEPTION WHEN invalid_text_representation THEN
|
|
569
|
+
v_project_id := NULL;
|
|
570
|
+
END;
|
|
571
|
+
|
|
572
|
+
IF v_project_id IS NULL AND p_entity_table = 'projects' THEN
|
|
573
|
+
v_project_id := p_entity_id;
|
|
574
|
+
END IF;
|
|
575
|
+
IF v_project_id IS NULL THEN
|
|
576
|
+
RETURN false;
|
|
577
|
+
END IF;
|
|
578
|
+
|
|
579
|
+
RETURN EXISTS (
|
|
580
|
+
SELECT 1
|
|
581
|
+
FROM public.projects project
|
|
582
|
+
WHERE project.id = v_project_id
|
|
583
|
+
AND (
|
|
584
|
+
project.owner_profile_id = p_profile_id
|
|
585
|
+
OR EXISTS (
|
|
586
|
+
SELECT 1
|
|
587
|
+
FROM public.project_members member
|
|
588
|
+
WHERE member.project_id = project.id
|
|
589
|
+
AND member.profile_id = p_profile_id
|
|
590
|
+
)
|
|
591
|
+
OR EXISTS (
|
|
592
|
+
SELECT 1
|
|
593
|
+
FROM public.organization_members member
|
|
594
|
+
WHERE member.organization_id = project.organization_id
|
|
595
|
+
AND member.profile_id = p_profile_id
|
|
596
|
+
AND member.role = 'admin'
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
END;
|
|
601
|
+
$$;
|
|
602
|
+
|
|
603
|
+
REVOKE ALL ON FUNCTION public.can_profile_view_app_activity_event(uuid, text, text, uuid, jsonb)
|
|
604
|
+
FROM PUBLIC, anon, authenticated;
|
|
605
|
+
GRANT EXECUTE ON FUNCTION public.can_profile_view_app_activity_event(uuid, text, text, uuid, jsonb)
|
|
606
|
+
TO service_role;
|
|
607
|
+
|
|
608
|
+
CREATE OR REPLACE FUNCTION public.can_view_app_activity_event(
|
|
609
|
+
p_domain text,
|
|
610
|
+
p_entity_table text,
|
|
611
|
+
p_entity_id uuid,
|
|
612
|
+
p_payload jsonb DEFAULT '{}'::jsonb
|
|
613
|
+
)
|
|
614
|
+
RETURNS boolean
|
|
615
|
+
LANGUAGE sql
|
|
616
|
+
STABLE
|
|
617
|
+
SECURITY DEFINER
|
|
618
|
+
SET search_path = public
|
|
619
|
+
AS $$
|
|
620
|
+
SELECT public.can_profile_view_app_activity_event(
|
|
621
|
+
public.current_profile_id(),
|
|
622
|
+
p_domain,
|
|
623
|
+
p_entity_table,
|
|
624
|
+
p_entity_id,
|
|
625
|
+
p_payload
|
|
626
|
+
)
|
|
627
|
+
$$;
|
|
628
|
+
|
|
629
|
+
REVOKE ALL ON FUNCTION public.can_view_app_activity_event(text, text, uuid, jsonb)
|
|
630
|
+
FROM PUBLIC, anon;
|
|
631
|
+
GRANT EXECUTE ON FUNCTION public.can_view_app_activity_event(text, text, uuid, jsonb)
|
|
632
|
+
TO authenticated;
|
|
633
|
+
|
|
634
|
+
DROP POLICY IF EXISTS "Project members can view project activity events"
|
|
635
|
+
ON public.app_activity_events;
|
|
636
|
+
DROP POLICY IF EXISTS "Project staff can view project activity events"
|
|
637
|
+
ON public.app_activity_events;
|
|
638
|
+
DROP POLICY IF EXISTS "Staff can view app activity events"
|
|
639
|
+
ON public.app_activity_events;
|
|
640
|
+
CREATE POLICY "Staff can view app activity events"
|
|
641
|
+
ON public.app_activity_events
|
|
642
|
+
FOR SELECT
|
|
643
|
+
TO authenticated
|
|
644
|
+
USING (
|
|
645
|
+
public.is_staff()
|
|
646
|
+
AND public.can_view_app_activity_event(domain, entity_table, entity_id, payload)
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
DROP TRIGGER IF EXISTS capture_removed_project_member_realtime_audience
|
|
650
|
+
ON public.app_activity_events;
|
|
651
|
+
DROP FUNCTION IF EXISTS public.capture_removed_project_member_realtime_audience();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Narrow client-owned organization memberships for account surfaces.
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION public.list_current_client_organizations()
|
|
4
|
+
RETURNS TABLE (
|
|
5
|
+
organization_id uuid,
|
|
6
|
+
organization_name text,
|
|
7
|
+
membership_role text
|
|
8
|
+
)
|
|
9
|
+
LANGUAGE sql
|
|
10
|
+
STABLE
|
|
11
|
+
SECURITY DEFINER
|
|
12
|
+
SET search_path = public
|
|
13
|
+
AS $$
|
|
14
|
+
SELECT
|
|
15
|
+
organization.id,
|
|
16
|
+
organization.name,
|
|
17
|
+
membership.role
|
|
18
|
+
FROM public.organization_members membership
|
|
19
|
+
JOIN public.organizations organization
|
|
20
|
+
ON organization.id = membership.organization_id
|
|
21
|
+
WHERE public.current_global_role() = 'client'
|
|
22
|
+
AND membership.profile_id = public.current_profile_id()
|
|
23
|
+
ORDER BY organization.name, organization.id
|
|
24
|
+
$$;
|
|
25
|
+
|
|
26
|
+
REVOKE ALL ON FUNCTION public.list_current_client_organizations()
|
|
27
|
+
FROM PUBLIC, anon, service_role;
|
|
28
|
+
GRANT EXECUTE ON FUNCTION public.list_current_client_organizations()
|
|
29
|
+
TO authenticated;
|