fix(gallery): proper auth-gated voting with persistence, undo, and change
Replaces the broken counter-bump RPC with a per-user share_votes table (PK share_id+user_id, RLS-enforced ownership). One vote per user per card, change direction or undo any time. Auth required for write; read is public. share_cards.upvotes/downvotes/score stay in sync via recalc trigger. New endpoints: POST /api/vote (auth-gated), GET /api/vote-state/:id (auth-aware).
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
-- 20260502115405_share_votes.sql
|
||||
--
|
||||
-- Per-user vote tracking for the /said share gallery.
|
||||
--
|
||||
-- Replaces the broken counter-bump RPC (vote_card) with a proper relational
|
||||
-- design: one row per (share_card, user) pair, RLS-enforced ownership,
|
||||
-- aggregate columns on share_cards kept in sync via trigger. Voters can
|
||||
-- change direction or undo at any time. One vote per user per card.
|
||||
--
|
||||
-- This migration is idempotent: re-running it is a no-op.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Table
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.share_votes (
|
||||
share_id text not null references public.share_cards(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
direction text not null check (direction in ('up','down')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (share_id, user_id)
|
||||
);
|
||||
|
||||
create index if not exists share_votes_share_id_idx on public.share_votes (share_id);
|
||||
create index if not exists share_votes_user_id_idx on public.share_votes (user_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Row Level Security
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
alter table public.share_votes enable row level security;
|
||||
|
||||
drop policy if exists "users can read all votes" on public.share_votes;
|
||||
drop policy if exists "users can insert their own votes" on public.share_votes;
|
||||
drop policy if exists "users can update their own votes" on public.share_votes;
|
||||
drop policy if exists "users can delete their own votes" on public.share_votes;
|
||||
|
||||
create policy "users can read all votes" on public.share_votes
|
||||
for select using (true);
|
||||
|
||||
create policy "users can insert their own votes" on public.share_votes
|
||||
for insert with check (auth.uid() = user_id);
|
||||
|
||||
create policy "users can update their own votes" on public.share_votes
|
||||
for update using (auth.uid() = user_id);
|
||||
|
||||
create policy "users can delete their own votes" on public.share_votes
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Aggregate sync trigger
|
||||
--
|
||||
-- After every insert / update / delete on share_votes, recompute the three
|
||||
-- aggregate columns on the parent share_cards row. SECURITY DEFINER is
|
||||
-- required because the calling user only has RLS access to their own vote
|
||||
-- but the trigger needs to update share_cards.upvotes/downvotes/score.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
create or replace function public.recalc_share_card_score()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
s_id text;
|
||||
begin
|
||||
s_id := coalesce(NEW.share_id, OLD.share_id);
|
||||
update public.share_cards
|
||||
set upvotes = (select count(*) from public.share_votes
|
||||
where share_id = s_id and direction = 'up'),
|
||||
downvotes = (select count(*) from public.share_votes
|
||||
where share_id = s_id and direction = 'down'),
|
||||
score = (select count(*) filter (where direction = 'up')
|
||||
- count(*) filter (where direction = 'down')
|
||||
from public.share_votes where share_id = s_id)
|
||||
where id = s_id;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists share_votes_recalc on public.share_votes;
|
||||
create trigger share_votes_recalc
|
||||
after insert or update or delete on public.share_votes
|
||||
for each row execute function public.recalc_share_card_score();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- updated_at touch trigger (only on UPDATE)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
create or replace function public.touch_share_votes_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
NEW.updated_at := now();
|
||||
return NEW;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists share_votes_touch on public.share_votes;
|
||||
create trigger share_votes_touch
|
||||
before update on public.share_votes
|
||||
for each row execute function public.touch_share_votes_updated_at();
|
||||
Reference in New Issue
Block a user