Spaces:
Sleeping
Sleeping
| -- 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; | |