Is your feature request related to a problem?
Once a user is invited to a collaboration room, there is no way for them to leave. The only destructive action available is `DELETE /api/rooms/[roomId]`, which removes the entire room and is restricted to the owner. Non-owner members are permanently in every room they are added to, with no way to exit.
This also means owners have no way to remove a specific member without deleting the entire room.
Current API surface
- `POST /api/rooms/[roomId]/invite` — adds a member (owner only)
- `DELETE /api/rooms/[roomId]` — deletes the entire room (owner only)
- There is no endpoint to remove a single member.
Proposed solution
1. New API endpoint
Add `DELETE /api/rooms/[roomId]/members/[username]` with the following authorization logic:
- A user may always remove themselves (leave the room).
- The room owner may remove any non-owner member.
- Owners cannot remove themselves (they must delete the room or transfer ownership instead).
```ts
// src/app/api/rooms/[roomId]/members/[username]/route.ts
export async function DELETE(req, { params }) {
const session = await getServerSession(authOptions);
if (!session?.githubLogin)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const room = await getRoomById(params.roomId, session.githubLogin);
if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 });
const isSelf = normalizeRoomGithubUsername(params.username) ===
normalizeRoomGithubUsername(session.githubLogin);
const isOwner = room.is_owner;
if (!isSelf && !isOwner)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
if (isSelf && isOwner)
return NextResponse.json(
{ error: 'Room owner cannot leave. Delete the room or transfer ownership.' },
{ status: 400 }
);
const { error } = await supabaseAdmin
.from('room_members')
.delete()
.eq('room_id', params.roomId)
.eq('github_username', normalizeRoomGithubUsername(params.username));
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ success: true });
}
```
2. UI changes
- `MembersPanel.tsx` — Add a "Remove" button next to each non-owner member (visible to the owner only).
- `RoomClient.tsx` — Add a "Leave Room" button in the room header (visible to non-owner members only). On success, redirect to `/rooms`.
Files to modify
- `src/app/api/rooms/[roomId]/members/[username]/route.ts` — new file
- `src/lib/supabase-rooms.ts` — add `removeRoomMember(roomId, username)` helper
- `src/components/rooms/MembersPanel.tsx` — "Remove member" button for owners
- `src/app/rooms/[roomId]/RoomClient.tsx` — "Leave Room" button for non-owners
Acceptance criteria
Is your feature request related to a problem?
Once a user is invited to a collaboration room, there is no way for them to leave. The only destructive action available is `DELETE /api/rooms/[roomId]`, which removes the entire room and is restricted to the owner. Non-owner members are permanently in every room they are added to, with no way to exit.
This also means owners have no way to remove a specific member without deleting the entire room.
Current API surface
Proposed solution
1. New API endpoint
Add `DELETE /api/rooms/[roomId]/members/[username]` with the following authorization logic:
```ts
// src/app/api/rooms/[roomId]/members/[username]/route.ts
export async function DELETE(req, { params }) {
const session = await getServerSession(authOptions);
if (!session?.githubLogin)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const room = await getRoomById(params.roomId, session.githubLogin);
if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 });
const isSelf = normalizeRoomGithubUsername(params.username) ===
normalizeRoomGithubUsername(session.githubLogin);
const isOwner = room.is_owner;
if (!isSelf && !isOwner)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
if (isSelf && isOwner)
return NextResponse.json(
{ error: 'Room owner cannot leave. Delete the room or transfer ownership.' },
{ status: 400 }
);
const { error } = await supabaseAdmin
.from('room_members')
.delete()
.eq('room_id', params.roomId)
.eq('github_username', normalizeRoomGithubUsername(params.username));
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ success: true });
}
```
2. UI changes
Files to modify
Acceptance criteria