feat: protect video playback access

This commit is contained in:
Codex
2026-06-28 23:00:05 +08:00
parent db65295ddc
commit 3647c2bc2d
12 changed files with 581 additions and 24 deletions

View File

@@ -0,0 +1,92 @@
alter table public.video_explanations
add column if not exists asset_id uuid references public.content_assets(id) on delete set null,
add column if not exists access_mode text not null default 'svip',
add column if not exists free_preview_seconds integer not null default 0,
add column if not exists play_count integer not null default 0;
do $$
begin
if not exists (select 1 from pg_constraint where conname = 'video_explanations_access_mode_check') then
alter table public.video_explanations
add constraint video_explanations_access_mode_check
check (access_mode in ('free', 'svip', 'video_quota'));
end if;
if not exists (select 1 from pg_constraint where conname = 'video_explanations_free_preview_check') then
alter table public.video_explanations
add constraint video_explanations_free_preview_check
check (free_preview_seconds >= 0);
end if;
if not exists (select 1 from pg_constraint where conname = 'video_explanations_play_count_check') then
alter table public.video_explanations
add constraint video_explanations_play_count_check
check (play_count >= 0);
end if;
end $$;
create table if not exists public.video_play_quota_accounts (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
user_id uuid not null references public.platform_users(id) on delete cascade,
quota_type text not null default 'video_play',
total_quota integer not null default 0,
used_quota integer not null default 0,
expires_at timestamptz,
source_type text,
source_id uuid,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (tenant_id, user_id, quota_type, source_type, source_id)
);
create table if not exists public.video_play_events (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
user_id uuid not null references public.platform_users(id) on delete cascade,
video_id uuid not null references public.video_explanations(id) on delete cascade,
question_id uuid references public.questions(id) on delete set null,
entitlement_id uuid references public.entitlements(id) on delete set null,
quota_account_id uuid references public.video_play_quota_accounts(id) on delete set null,
play_token_hash text not null unique,
status text not null default 'issued' check (status in ('issued', 'started', 'completed', 'expired', 'revoked')),
access_mode text not null default 'svip' check (access_mode in ('free', 'svip', 'video_quota')),
consumed_quota integer not null default 0 check (consumed_quota >= 0),
signed_url_expires_at timestamptz,
ip_address text,
user_agent text,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_video_explanations_asset
on public.video_explanations(tenant_id, asset_id)
where asset_id is not null;
create index if not exists idx_video_quota_user
on public.video_play_quota_accounts(tenant_id, user_id, quota_type, expires_at);
create index if not exists idx_video_play_events_user
on public.video_play_events(tenant_id, user_id, created_at desc);
create index if not exists idx_video_play_events_video
on public.video_play_events(tenant_id, video_id, created_at desc);
do $$
declare
table_name text;
begin
foreach table_name in array array['video_play_quota_accounts', 'video_play_events']
loop
execute format('alter table public.%I enable row level security', table_name);
execute format('drop policy if exists tenant_isolation on public.%I', table_name);
execute format(
'create policy tenant_isolation on public.%I for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin())',
table_name
);
execute format('drop trigger if exists set_updated_at on public.%I', table_name);
execute format('create trigger set_updated_at before update on public.%I for each row execute function app.touch_updated_at()', table_name);
end loop;
end $$;