fix: add rpc-backed member removal migration
This commit is contained in:
parent
da9981d297
commit
febba238ec
2 changed files with 90 additions and 7 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue