feat(db): message_attachments.view_once + mark_attachment_viewed RPC

This commit is contained in:
byGalax
2026-05-16 18:09:36 +02:00
parent cd79b77e6f
commit 526f9d7bcc
@@ -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;