fix: add rpc-backed member removal migration

This commit is contained in:
Lukas Müllner 2026-03-05 11:56:01 +01:00
parent da9981d297
commit febba238ec
2 changed files with 90 additions and 7 deletions

View file

@ -1,7 +1,64 @@
-- Run this in Supabase SQL Editor to allow a read-only viewer role.
-- Run in Supabase SQL Editor.
-- 1) Allow viewer role
alter table public.collection_members
drop constraint if exists collection_members_role_check;
alter table public.collection_members
add constraint collection_members_role_check
check (role in ('owner', 'member', 'viewer'));
-- 2) Ensure owners can delete members from their collections
-- (and users can still remove themselves for leave flow).
drop policy if exists "Owner can remove members" on public.collection_members;
create policy "Owner can remove members"
on public.collection_members
for delete
to authenticated
using (
collection_id in (
select c.id
from public.collections c
where c.owner_id = auth.uid()
)
or user_id = auth.uid()
);
-- 3) Stable, policy-safe removal RPC for app use
create or replace function public.remove_collection_member(
p_collection_id uuid,
p_member_user_id uuid
)
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
v_owner_id uuid;
begin
select c.owner_id
into v_owner_id
from public.collections c
where c.id = p_collection_id;
if v_owner_id is null then
raise exception 'Collection not found.';
end if;
if v_owner_id <> auth.uid() then
raise exception 'Only the collection owner can remove members.';
end if;
if p_member_user_id = v_owner_id then
raise exception 'Collection owner cannot be removed.';
end if;
delete from public.collection_members cm
where cm.collection_id = p_collection_id
and cm.user_id = p_member_user_id
and cm.role <> 'owner';
end;
$$;
grant execute on function public.remove_collection_member(uuid, uuid) to authenticated;

View file

@ -386,17 +386,43 @@ class CollectionService {
throw Exception('Collection owner cannot be removed.');
}
await supabase
.from('collection_members')
.delete()
.eq('collection_id', collectionId)
.eq('user_id', memberUserId);
var deleted = false;
try {
await supabase.rpc('remove_collection_member', params: {
'p_collection_id': collectionId,
'p_member_user_id': memberUserId,
});
deleted = true;
} catch (e) {
final message = e.toString();
final rpcUnavailable = message.contains('remove_collection_member') &&
(message.contains('not found') ||
message.contains('does not exist') ||
message.contains('PGRST202'));
if (!rpcUnavailable) {
rethrow;
}
await supabase
.from('collection_members')
.delete()
.eq('collection_id', collectionId)
.eq('user_id', memberUserId);
deleted = true;
}
if (!deleted) {
throw Exception('Member removal failed.');
}
final membersAfter = await getMembers(collectionId);
final stillExists =
membersAfter.any((member) => member.userId == memberUserId);
if (stillExists) {
throw Exception('Member removal did not persist. Please check RLS delete policy.');
throw Exception(
'Member removal did not persist. Run VIEWER_ROLE_MIGRATION.sql to update policies/functions.',
);
}
}