From 526f9d7bcc7ee517a3c399978b7933a7047b25db Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 18:09:36 +0200 Subject: [PATCH] feat(db): message_attachments.view_once + mark_attachment_viewed RPC --- .../20260516000004_view_once_attachments.sql | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 supabase/migrations/20260516000004_view_once_attachments.sql diff --git a/supabase/migrations/20260516000004_view_once_attachments.sql b/supabase/migrations/20260516000004_view_once_attachments.sql new file mode 100644 index 0000000..90f1337 --- /dev/null +++ b/supabase/migrations/20260516000004_view_once_attachments.sql @@ -0,0 +1,58 @@ +-- View-once media: per-attachment opt-in. When a recipient opens the +-- attachment for the first time we delete the storage object and replace +-- it with a tombstone marker (kept_until / viewed_at). The encrypted +-- attachment row stays so the bubble can render "Angesehen". + +alter table public.message_attachments + add column if not exists view_once boolean not null default false, + add column if not exists viewed_at timestamptz null, + add column if not exists viewed_by uuid null references auth.users(id) on delete set null; + +-- Mark-viewed RPC: server-authoritative so the recipient can't replay the +-- decrypted blob across devices. Atomic: only the FIRST viewer wins. The +-- function returns the previous `viewed_at` so the client can distinguish +-- first-open (got it) from already-viewed (too late, sorry). +create or replace function public.mark_attachment_viewed(p_attachment_id uuid) +returns jsonb +language plpgsql +security definer +set search_path = public +as $$ +declare + caller uuid := auth.uid(); + att public.message_attachments%rowtype; + msg public.messages%rowtype; + prev timestamptz; +begin + if caller is null then raise exception 'not authenticated'; end if; + + select * into att from public.message_attachments where id = p_attachment_id for update; + if not found then raise exception 'attachment not found'; end if; + if not att.view_once then return jsonb_build_object('view_once', false); end if; + + select * into msg from public.messages where id = att.message_id; + if not found then raise exception 'orphan attachment'; end if; + if not public.is_conversation_member(msg.conversation_id) then + raise exception 'not a member'; + end if; + -- Sender opening their own view-once doesn't "burn" it — they sent it. + if msg.sender_id = caller then + return jsonb_build_object('view_once', true, 'viewed_at', att.viewed_at, 'self', true); + end if; + + if att.viewed_at is not null then + return jsonb_build_object('view_once', true, 'viewed_at', att.viewed_at, 'already', true); + end if; + + update public.message_attachments + set viewed_at = now(), + viewed_by = caller + where id = p_attachment_id + returning viewed_at into prev; + + return jsonb_build_object('view_once', true, 'viewed_at', prev); +end; +$$; + +revoke execute on function public.mark_attachment_viewed(uuid) from public, anon; +grant execute on function public.mark_attachment_viewed(uuid) to authenticated;