File size: 8,046 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
164
165
166
167
168
169
170
171
172
173
-- Authoritative MediaRouter tenancy and canonical media ownership.
-- Apply after 0001 with the normal production migration process.
-- API keys remain credentials; api_key_principals binds them to a persisted
-- active membership. Do not grant these tables directly to browser clients.

begin;
create extension if not exists pgcrypto;

create table if not exists users (
  id text primary key default gen_random_uuid()::text,
  subject text not null unique,
  display_name text,
  status text not null default 'active' check (status in ('active', 'disabled')),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists workspaces (
  id text primary key default gen_random_uuid()::text,
  slug text not null unique,
  name text not null,
  status text not null default 'active' check (status in ('active', 'suspended', 'disabled')),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists workspace_memberships (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete cascade,
  user_id text not null references users(id) on delete cascade,
  role text not null default 'member' check (role in ('owner', 'admin', 'member', 'viewer', 'service')),
  status text not null default 'active' check (status in ('active', 'disabled')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uq_workspace_membership unique (workspace_id, user_id)
);
create index if not exists ix_workspace_memberships_user on workspace_memberships(user_id);
create index if not exists ix_workspace_memberships_workspace on workspace_memberships(workspace_id);

create table if not exists api_key_principals (
  id text primary key default gen_random_uuid()::text,
  api_key_id text not null unique references api_keys(id) on delete cascade,
  workspace_id text not null references workspaces(id) on delete cascade,
  user_id text not null references users(id) on delete cascade,
  membership_id text not null references workspace_memberships(id) on delete restrict,
  status text not null default 'active' check (status in ('active', 'disabled')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);
create index if not exists ix_api_key_principals_workspace on api_key_principals(workspace_id);
create index if not exists ix_api_key_principals_user on api_key_principals(user_id);

create table if not exists media_assets (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  request_id text not null,
  filename text not null,
  mime_type text not null,
  file_size bigint not null check (file_size >= 0),
  sha256 char(64) not null check (sha256 ~ '^[0-9a-f]{64}$'),
  metadata jsonb not null default '{}'::jsonb,
  created_by_user_id text references users(id) on delete set null,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uq_media_asset_output unique (request_id, filename)
);
create index if not exists ix_media_assets_workspace_created on media_assets(workspace_id, created_at desc);
create index if not exists ix_media_assets_workspace_request on media_assets(workspace_id, request_id);

create table if not exists media_asset_variants (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  asset_id text not null references media_assets(id) on delete cascade,
  request_id text not null,
  filename text not null,
  mime_type text not null,
  file_size bigint not null check (file_size >= 0),
  sha256 char(64) not null check (sha256 ~ '^[0-9a-f]{64}$'),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  constraint uq_media_asset_variant_output unique (asset_id, request_id, filename)
);
create index if not exists ix_media_asset_variants_workspace_asset on media_asset_variants(workspace_id, asset_id);

create or replace function mediarouter_tenant_touch_updated_at()
returns trigger language plpgsql as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

do $$
declare table_name text;
begin
  foreach table_name in array array['users','workspaces','workspace_memberships','api_key_principals','media_assets'] loop
    execute format('drop trigger if exists mediarouter_tenant_touch_updated_at on %I', table_name);
    execute format('create trigger mediarouter_tenant_touch_updated_at before update on %I for each row execute function mediarouter_tenant_touch_updated_at()', table_name);
  end loop;
end $$;

-- A tenant API connection must set both values transaction-locally. Backend
-- authentication/administration and service workers use separate credentials,
-- never an exposed browser or SDK connection.
alter table users enable row level security;
alter table users force row level security;
drop policy if exists users_self on users;
create policy users_self on users
  using (id = current_setting('app.user_id', true))
  with check (id = current_setting('app.user_id', true));

alter table workspaces enable row level security;
alter table workspaces force row level security;
drop policy if exists workspace_membership_read on workspaces;
create policy workspace_membership_read on workspaces
  using (id = current_setting('app.workspace_id', true) and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = workspaces.id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  ))
  with check (false);

alter table workspace_memberships enable row level security;
alter table workspace_memberships force row level security;
drop policy if exists workspace_membership_self on workspace_memberships;
create policy workspace_membership_self on workspace_memberships
  using (workspace_id = current_setting('app.workspace_id', true)
    and user_id = current_setting('app.user_id', true))
  with check (false);

alter table api_key_principals enable row level security;
alter table api_key_principals force row level security;
drop policy if exists api_key_principal_self on api_key_principals;
create policy api_key_principal_self on api_key_principals
  using (workspace_id = current_setting('app.workspace_id', true)
    and user_id = current_setting('app.user_id', true))
  with check (false);

do $$
declare table_name text;
begin
  foreach table_name in array array['media_assets','media_asset_variants'] loop
    execute format('alter table %I enable row level security', table_name);
    execute format('alter table %I force row level security', table_name);
    execute format('drop policy if exists canonical_asset_workspace_isolation on %I', table_name);
    execute format(
      'create policy canonical_asset_workspace_isolation on %I using (workspace_id = current_setting(''app.workspace_id'', true)) with check (workspace_id = current_setting(''app.workspace_id'', true))',
      table_name
    );
  end loop;
end $$;

create or replace function mediarouter_assert_canonical_variant_workspace()
returns trigger language plpgsql as $$
declare asset_workspace text;
begin
  select workspace_id into asset_workspace from media_assets where id = new.asset_id;
  if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
    raise exception 'media variant must belong to its source asset workspace' using errcode = '23503';
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_canonical_variant_workspace on media_asset_variants;
create trigger mediarouter_canonical_variant_workspace
before insert or update of workspace_id, asset_id on media_asset_variants
for each row execute function mediarouter_assert_canonical_variant_workspace();

commit;