feat: profile avatar upload + share_conv_keys rpc + favicon + smtp tweaks
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
-- 1) Test-Daten-Wipe: alle conversation/message Daten weg, User+Devices+
|
||||
-- Friendships+Invites bleiben erhalten so dass bestehende Logins weiter
|
||||
-- funktionieren. Cascade räumt mitlaufende rows (envelopes, attachments,
|
||||
-- reactions, reads, conversation_keys, conversation_members).
|
||||
|
||||
truncate
|
||||
public.message_reactions,
|
||||
public.message_reads,
|
||||
public.message_attachments,
|
||||
public.messages,
|
||||
public.conversation_keys,
|
||||
public.conversation_members,
|
||||
public.conversations
|
||||
restart identity cascade;
|
||||
|
||||
-- 2) RPC: share conv-key bundles to multiple recipients atomically.
|
||||
-- Runs as SECURITY DEFINER so RLS doesn't reject individual rows. The
|
||||
-- function itself enforces the same invariants as the policy. This avoids
|
||||
-- the per-row 403 console spam we got with direct inserts.
|
||||
|
||||
create or replace function public.share_conv_keys(
|
||||
p_conv_id uuid,
|
||||
p_sender_device_id uuid,
|
||||
p_key_version int,
|
||||
p_bundles jsonb
|
||||
) returns int
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
caller uuid := auth.uid();
|
||||
bundle jsonb;
|
||||
recipient_user_id uuid;
|
||||
inserted int := 0;
|
||||
recipient_did uuid;
|
||||
enc_key_hex text;
|
||||
nonce_hex text;
|
||||
begin
|
||||
if caller is null then
|
||||
raise exception 'not authenticated';
|
||||
end if;
|
||||
|
||||
-- Caller must be an accepted member of the conversation.
|
||||
if not exists (
|
||||
select 1
|
||||
from public.conversation_members
|
||||
where conversation_id = p_conv_id
|
||||
and user_id = caller
|
||||
and accepted = true
|
||||
) then
|
||||
raise exception 'caller is not an accepted member of %', p_conv_id;
|
||||
end if;
|
||||
|
||||
-- Sender device must belong to the caller.
|
||||
if not exists (
|
||||
select 1
|
||||
from public.devices
|
||||
where id = p_sender_device_id
|
||||
and user_id = caller
|
||||
) then
|
||||
raise exception 'sender_device % not owned by caller', p_sender_device_id;
|
||||
end if;
|
||||
|
||||
-- Iterate bundles and only insert ones whose recipient is a current
|
||||
-- accepted member. Silently skip otherwise (no error → no 403 noise).
|
||||
for bundle in
|
||||
select * from jsonb_array_elements(p_bundles)
|
||||
loop
|
||||
recipient_did := (bundle->>'recipient_device_id')::uuid;
|
||||
enc_key_hex := bundle->>'encrypted_key';
|
||||
nonce_hex := bundle->>'nonce';
|
||||
|
||||
select user_id
|
||||
into recipient_user_id
|
||||
from public.devices
|
||||
where id = recipient_did;
|
||||
if recipient_user_id is null then continue; end if;
|
||||
|
||||
if not exists (
|
||||
select 1
|
||||
from public.conversation_members
|
||||
where conversation_id = p_conv_id
|
||||
and user_id = recipient_user_id
|
||||
and accepted = true
|
||||
) then continue; end if;
|
||||
|
||||
insert into public.conversation_keys
|
||||
(conversation_id, recipient_device_id, key_version,
|
||||
sender_device_id, encrypted_key, nonce)
|
||||
values
|
||||
(p_conv_id, recipient_did, p_key_version,
|
||||
p_sender_device_id,
|
||||
decode(enc_key_hex, 'hex'),
|
||||
decode(nonce_hex, 'hex'))
|
||||
on conflict (conversation_id, recipient_device_id, key_version)
|
||||
do nothing;
|
||||
|
||||
if found then
|
||||
inserted := inserted + 1;
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return inserted;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke execute on function public.share_conv_keys(uuid, uuid, int, jsonb) from public, anon;
|
||||
grant execute on function public.share_conv_keys(uuid, uuid, int, jsonb) to authenticated;
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Profile-Avatar storage bucket.
|
||||
--
|
||||
-- Layout: <user_id>/<filename>.webp
|
||||
-- Public read so peers can see each other's avatar without auth roundtrip
|
||||
-- (avatars are non-sensitive). Write/update/delete restricted to the owning
|
||||
-- user via path prefix matching their auth.uid().
|
||||
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('profile-avatars', 'profile-avatars', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- Read: anyone authenticated can view any avatar (public bucket also lets
|
||||
-- unauth fetch by URL but our RLS keeps the table-level policy explicit).
|
||||
drop policy if exists profile_avatars_select on storage.objects;
|
||||
create policy profile_avatars_select
|
||||
on storage.objects
|
||||
for select
|
||||
to authenticated, anon
|
||||
using (bucket_id = 'profile-avatars');
|
||||
|
||||
-- Write only into your own folder (first path segment must equal auth.uid()).
|
||||
drop policy if exists profile_avatars_insert_own on storage.objects;
|
||||
create policy profile_avatars_insert_own
|
||||
on storage.objects
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'profile-avatars'
|
||||
and (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
drop policy if exists profile_avatars_update_own on storage.objects;
|
||||
create policy profile_avatars_update_own
|
||||
on storage.objects
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'profile-avatars'
|
||||
and (storage.foldername(name))[1] = auth.uid()::text
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'profile-avatars'
|
||||
and (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
drop policy if exists profile_avatars_delete_own on storage.objects;
|
||||
create policy profile_avatars_delete_own
|
||||
on storage.objects
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'profile-avatars'
|
||||
and (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
Reference in New Issue
Block a user