Spaces:
Sleeping
Sleeping
File size: 6,796 Bytes
3493993 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | -- Authoritative workspace-owned Project foundation.
-- Apply after app/security/migrations/0002_authoritative_tenancy_postgres.sql.
-- IDs remain UUID values stored as text to match MediaRouter's existing
-- tenancy and canonical-asset foreign-key types.
begin;
create extension if not exists pgcrypto;
create table if not exists projects (
id text primary key default gen_random_uuid()::text,
workspace_id text not null references workspaces(id) on delete restrict,
created_by text not null references users(id) on delete restrict,
name text not null check (char_length(name) between 1 and 200),
description text check (description is null or char_length(description) <= 4000),
status text not null default 'active' check (status in ('active', 'archived')),
thumbnail_asset_id text references media_assets(id) on delete set null,
metadata jsonb not null default '{}'::jsonb
check (jsonb_typeof(metadata) = 'object'),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
archived_at timestamptz,
constraint ck_projects_archive_timestamp check (
(status = 'active' and archived_at is null)
or (status = 'archived' and archived_at is not null)
)
);
create index if not exists ix_projects_workspace on projects(workspace_id);
create index if not exists ix_projects_workspace_status on projects(workspace_id, status);
create index if not exists ix_projects_workspace_updated on projects(workspace_id, updated_at desc);
create index if not exists ix_projects_created_by on projects(created_by);
-- The shared AuditService owns this generic event table. It is deliberately
-- not project-specific so future domains can reuse the same safe boundary.
create table if not exists audit_events (
id text primary key default gen_random_uuid()::text,
workspace_id text not null references workspaces(id) on delete restrict,
actor_user_id text references users(id) on delete set null,
api_key_id text references api_keys(id) on delete set null,
event_type text not null check (char_length(event_type) between 1 and 100),
entity_type text not null check (char_length(entity_type) between 1 and 64),
entity_id text not null,
request_id text,
metadata jsonb not null default '{}'::jsonb check (jsonb_typeof(metadata) = 'object'),
created_at timestamptz not null default now()
);
create index if not exists ix_audit_events_workspace_created
on audit_events(workspace_id, created_at desc);
create index if not exists ix_audit_events_type on audit_events(event_type);
create index if not exists ix_audit_events_entity on audit_events(entity_type, entity_id);
drop trigger if exists mediarouter_tenant_touch_updated_at on projects;
create trigger mediarouter_tenant_touch_updated_at
before update on projects
for each row execute function mediarouter_tenant_touch_updated_at();
create or replace function mediarouter_assert_project_ownership()
returns trigger language plpgsql as $$
declare asset_workspace text;
begin
if tg_op = 'UPDATE' then
if new.workspace_id is distinct from old.workspace_id
or new.created_by is distinct from old.created_by then
raise exception 'project ownership fields are immutable' using errcode = '23514';
end if;
end if;
if new.thumbnail_asset_id is not null then
select workspace_id into asset_workspace from media_assets
where id = new.thumbnail_asset_id;
if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
raise exception 'project thumbnail must belong to its workspace' using errcode = '23503';
end if;
end if;
return new;
end;
$$;
drop trigger if exists mediarouter_project_ownership on projects;
create trigger mediarouter_project_ownership
before insert or update of workspace_id, created_by, thumbnail_asset_id on projects
for each row execute function mediarouter_assert_project_ownership();
alter table projects enable row level security;
alter table projects force row level security;
drop policy if exists projects_select on projects;
create policy projects_select on projects for select using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = projects.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
drop policy if exists projects_insert on projects;
create policy projects_insert on projects for insert with check (
workspace_id = current_setting('app.workspace_id', true)
and created_by = current_setting('app.user_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = projects.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
drop policy if exists projects_update on projects;
create policy projects_update on projects for update using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = projects.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
) with check (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = projects.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
drop policy if exists projects_delete on projects;
create policy projects_delete on projects for delete using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = projects.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
alter table audit_events enable row level security;
alter table audit_events force row level security;
drop policy if exists audit_events_select on audit_events;
create policy audit_events_select on audit_events for select using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = audit_events.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
drop policy if exists audit_events_insert on audit_events;
create policy audit_events_insert on audit_events for insert with check (
workspace_id = current_setting('app.workspace_id', true)
and actor_user_id = current_setting('app.user_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = audit_events.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
commit;
|