diff --git a/src/app/api/room-invitations/[invitationId]/route.ts b/src/app/api/room-invitations/[invitationId]/route.ts
new file mode 100644
index 000000000..51b0e1053
--- /dev/null
+++ b/src/app/api/room-invitations/[invitationId]/route.ts
@@ -0,0 +1,44 @@
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/lib/auth";
+import {
+ getRoomInvitation,
+ respondToRoomInvitation,
+} from "@/lib/supabase-rooms";
+import { NextResponse } from "next/server";
+
+export async function POST(
+ req: Request,
+ { params }: { params: Promise<{ invitationId: string }> }
+) {
+ const { invitationId } = await params;
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.name)
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+
+ const invitation = await getRoomInvitation(invitationId);
+ if (!invitation)
+ return NextResponse.json(
+ { error: "Invitation not found" },
+ { status: 404 }
+ );
+ if (invitation.github_username !== session.user.name)
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+ if (invitation.status !== "pending")
+ return NextResponse.json(
+ { error: "Invitation already responded to" },
+ { status: 409 }
+ );
+
+ const { action } = await req.json();
+ if (action !== "accept" && action !== "decline")
+ return NextResponse.json(
+ { error: 'action must be "accept" or "decline"' },
+ { status: 400 }
+ );
+
+ const updated = await respondToRoomInvitation(
+ invitationId,
+ action === "accept"
+ );
+ return NextResponse.json({ success: true, status: updated?.status });
+}
diff --git a/src/app/api/room-invitations/route.ts b/src/app/api/room-invitations/route.ts
new file mode 100644
index 000000000..a6990f0ae
--- /dev/null
+++ b/src/app/api/room-invitations/route.ts
@@ -0,0 +1,13 @@
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/lib/auth";
+import { getPendingInvitationsForUser } from "@/lib/supabase-rooms";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.name)
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+
+ const invitations = await getPendingInvitationsForUser(session.user.name);
+ return NextResponse.json({ invitations });
+}
diff --git a/src/app/api/rooms/[roomId]/invite/route.ts b/src/app/api/rooms/[roomId]/invite/route.ts
index 4867887a9..efa5298c8 100644
--- a/src/app/api/rooms/[roomId]/invite/route.ts
+++ b/src/app/api/rooms/[roomId]/invite/route.ts
@@ -1,7 +1,12 @@
-import { getServerSession } from 'next-auth';
-import { authOptions } from '@/lib/auth';
-import { getRoomById, getRoomMembers, addRoomMember } from '@/lib/supabase-rooms';
-import { NextResponse } from 'next/server';
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/lib/auth";
+import {
+ getRoomById,
+ getRoomMembers,
+ createRoomInvitation,
+ notifyRoomInvitation,
+} from "@/lib/supabase-rooms";
+import { NextResponse } from "next/server";
export async function POST(
req: Request,
@@ -10,32 +15,60 @@ export async function POST(
const { roomId } = await params;
const session = await getServerSession(authOptions);
if (!session?.user?.name)
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const room = await getRoomById(roomId, session.user.name);
- if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 });
+ if (!room) return NextResponse.json({ error: "Not found" }, { status: 404 });
if (!room.is_owner)
- return NextResponse.json({ error: 'Only the room owner can invite' }, { status: 403 });
+ return NextResponse.json(
+ { error: "Only the room owner can invite" },
+ { status: 403 }
+ );
const { github_username } = await req.json();
if (!github_username?.trim())
- return NextResponse.json({ error: 'github_username required' }, { status: 400 });
+ return NextResponse.json(
+ { error: "github_username required" },
+ { status: 400 }
+ );
// GitHub usernames: 1-39 chars, alphanumeric + hyphens, no leading/trailing hyphen
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(github_username))
- return NextResponse.json({ error: 'Invalid GitHub username' }, { status: 400 });
+ return NextResponse.json(
+ { error: "Invalid GitHub username" },
+ { status: 400 }
+ );
const ghRes = await fetch(`https://api.github.com/users/${github_username}`, {
headers: {
- Accept: 'application/vnd.github+json',
+ Accept: "application/vnd.github+json",
...(process.env.GITHUB_TOKEN
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
: {}),
},
});
if (ghRes.status === 404)
- return NextResponse.json({ error: `GitHub user "${github_username}" does not exist` }, { status: 404 });
+ return NextResponse.json(
+ { error: `GitHub user "${github_username}" does not exist` },
+ { status: 404 }
+ );
if (!ghRes.ok)
- return NextResponse.json({ error: 'Could not verify GitHub user' }, { status: 502 });
+ return NextResponse.json(
+ { error: "Could not verify GitHub user" },
+ { status: 502 }
+ );
const members = await getRoomMembers(roomId);
if (members.some((m) => m.github_username === github_username))
- return NextResponse.json({ error: 'User is already a member' }, { status: 409 });
- await addRoomMember(roomId, github_username);
- return NextResponse.json({ success: true });
-}
\ No newline at end of file
+ return NextResponse.json(
+ { error: "User is already a member" },
+ { status: 409 }
+ );
+ try {
+ await createRoomInvitation(roomId, github_username, session.user.name);
+ } catch (error: any) {
+ if (error?.code === "23505")
+ return NextResponse.json(
+ { error: "An invitation is already pending for this user" },
+ { status: 409 }
+ );
+ throw error;
+ }
+ await notifyRoomInvitation(github_username, room.name, session.user.name);
+ return NextResponse.json({ success: true, status: "pending" });
+}
diff --git a/src/app/rooms/RoomsListClient.tsx b/src/app/rooms/RoomsListClient.tsx
index ddb39dda4..fbc62cade 100644
--- a/src/app/rooms/RoomsListClient.tsx
+++ b/src/app/rooms/RoomsListClient.tsx
@@ -4,6 +4,7 @@ import { useState } from 'react';
import Link from 'next/link';
import type { CollaborationRoom } from '@/types/rooms';
import CreateRoomModal from '@/components/rooms/CreateRoomModal';
+import PendingInvitations from '@/components/rooms/PendingInvitations';
interface Props {
initialRooms: CollaborationRoom[];
@@ -32,6 +33,8 @@ export default function RoomsListClient({ initialRooms, currentUser }: Props) {
+
+
{/* Room cards */}
{rooms.length === 0 ? (
diff --git a/src/components/rooms/InviteModal.tsx b/src/components/rooms/InviteModal.tsx
index 53693be48..18f62c5c9 100644
--- a/src/components/rooms/InviteModal.tsx
+++ b/src/components/rooms/InviteModal.tsx
@@ -1,76 +1,88 @@
-'use client';
+"use client";
-import { useState } from 'react';
+import { useState } from "react";
interface Props {
roomId: string;
onClose: () => void;
onInvited: (username: string) => void;
}
-
export default function InviteModal({ roomId, onClose, onInvited }: Props) {
- const [username, setUsername] = useState('');
+ const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState
(null);
-
+ const [sent, setSent] = useState(false);
async function handleInvite(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
-
const res = await fetch(`/api/rooms/${roomId}/invite`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ github_username: username.trim() }),
});
-
const data = await res.json();
setLoading(false);
-
if (!res.ok) {
- setError(data.error ?? 'Invite failed');
+ setError(data.error ?? "Invite failed");
return;
}
-
onInvited(username.trim());
- onClose();
+ setSent(true);
}
return (
-
Invite by GitHub Username
-
-
);
-}
\ No newline at end of file
+}
diff --git a/src/components/rooms/MembersPanel.tsx b/src/components/rooms/MembersPanel.tsx
index 9e4d19db8..fa33e3954 100644
--- a/src/components/rooms/MembersPanel.tsx
+++ b/src/components/rooms/MembersPanel.tsx
@@ -1,8 +1,8 @@
-'use client';
+"use client";
-import { useState } from 'react';
-import type { RoomMember } from '@/types/rooms';
-import InviteModal from './InviteModal';
+import { useState } from "react";
+import type { RoomMember } from "@/types/rooms";
+import InviteModal from "./InviteModal";
interface Props {
roomId: string;
@@ -12,7 +12,13 @@ interface Props {
onMemberRemoved: (username: string) => void;
}
-export default function MembersPanel({ roomId, members, isOwner, onMemberAdded, onMemberRemoved }: Props) {
+export default function MembersPanel({
+ roomId,
+ members,
+ isOwner,
+ onMemberAdded,
+ onMemberRemoved,
+}: Props) {
const [showInvite, setShowInvite] = useState(false);
const [removingUsername, setRemovingUsername] = useState
(null);
@@ -22,16 +28,16 @@ export default function MembersPanel({ roomId, members, isOwner, onMemberAdded,
try {
const res = await fetch(
`/api/rooms/${roomId}/members/${encodeURIComponent(username)}`,
- { method: 'DELETE' }
+ { method: "DELETE" }
);
if (res.ok) {
onMemberRemoved(username);
} else {
const data = await res.json().catch(() => ({}));
- alert((data as { error?: string }).error ?? 'Failed to remove member');
+ alert((data as { error?: string }).error ?? "Failed to remove member");
}
} catch {
- alert('Network error. Please try again.');
+ alert("Network error. Please try again.");
} finally {
setRemovingUsername(null);
}
@@ -64,18 +70,20 @@ export default function MembersPanel({ roomId, members, isOwner, onMemberAdded,
/>
{m.github_username}
- {m.role === 'owner' && (
-
owner
+ {m.role === "owner" && (
+
+ owner
+
)}
- {isOwner && m.role !== 'owner' && (
+ {isOwner && m.role !== "owner" && (
)}
@@ -86,9 +94,9 @@ export default function MembersPanel({ roomId, members, isOwner, onMemberAdded,
setShowInvite(false)}
- onInvited={(username) => {
- onMemberAdded(username);
- setShowInvite(false);
+ onInvited={() => {
+ // Invitation is pending, not an actual membership — don't add to
+ // the members list until the invitee accepts.
}}
/>
)}
diff --git a/src/components/rooms/PendingInvitations.tsx b/src/components/rooms/PendingInvitations.tsx
new file mode 100644
index 000000000..0f8cc06ec
--- /dev/null
+++ b/src/components/rooms/PendingInvitations.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import type { RoomInvitation } from "@/types/rooms";
+
+export default function PendingInvitations() {
+ const router = useRouter();
+ const [invitations, setInvitations] = useState([]);
+ const [respondingId, setRespondingId] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ async function fetchInvitations() {
+ try {
+ const res = await fetch("/api/room-invitations");
+ if (!res.ok) return;
+ const data = await res.json();
+ if (!cancelled) setInvitations(data.invitations ?? []);
+ } catch {
+ // silent — pending invitations are non-critical to page load
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ }
+ fetchInvitations();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ async function respond(invitationId: string, action: "accept" | "decline") {
+ setRespondingId(invitationId);
+ try {
+ const res = await fetch(`/api/room-invitations/${invitationId}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ action }),
+ });
+ if (res.ok) {
+ setInvitations((prev) => prev.filter((inv) => inv.id !== invitationId));
+ if (action === "accept") router.refresh();
+ }
+ } finally {
+ setRespondingId(null);
+ }
+ }
+
+ if (loading || invitations.length === 0) return null;
+
+ return (
+
+
Pending Invitations
+ {invitations.map((inv) => (
+
+
+
{inv.collaboration_rooms.name}
+
+ {inv.collaboration_rooms.repo_owner}/{inv.collaboration_rooms.repo_name} · invited by{" "}
+ {inv.invited_by}
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/src/lib/supabase-rooms.ts b/src/lib/supabase-rooms.ts
index f4c4ff16f..2e3081320 100644
--- a/src/lib/supabase-rooms.ts
+++ b/src/lib/supabase-rooms.ts
@@ -1,4 +1,4 @@
-import 'server-only';
+import "server-only";
import { supabaseAdmin } from "@/lib/supabase-admin";
import type {
CollaborationRoom,
@@ -7,49 +7,192 @@ import type {
CreateRoomPayload,
} from "@/types/rooms";
-export async function getRoomsForUser(username: string): Promise {
+export async function getRoomsForUser(
+ username: string
+): Promise {
const { data, error } = await supabaseAdmin
.from("room_members")
- .select(`role, collaboration_rooms (id, name, description, repo_owner, repo_name, created_by, created_at, updated_at)`)
+ .select(
+ `role, collaboration_rooms (id, name, description, repo_owner, repo_name, created_by, created_at, updated_at)`
+ )
.eq("github_username", username);
if (error) throw error;
- return (data ?? []).map((row: any) => ({ ...row.collaboration_rooms, is_owner: row.role === "owner" }));
+ return (data ?? []).map((row: any) => ({
+ ...row.collaboration_rooms,
+ is_owner: row.role === "owner",
+ }));
}
-export async function createRoom(payload: CreateRoomPayload, creatorUsername: string): Promise {
- const { data: room, error } = await supabaseAdmin.from("collaboration_rooms").insert({ ...payload, created_by: creatorUsername }).select().single();
+export async function createRoom(
+ payload: CreateRoomPayload,
+ creatorUsername: string
+): Promise {
+ const { data: room, error } = await supabaseAdmin
+ .from("collaboration_rooms")
+ .insert({ ...payload, created_by: creatorUsername })
+ .select()
+ .single();
if (error) throw error;
- await supabaseAdmin.from("room_members").insert({ room_id: room.id, github_username: creatorUsername, role: "owner" });
+ await supabaseAdmin
+ .from("room_members")
+ .insert({
+ room_id: room.id,
+ github_username: creatorUsername,
+ role: "owner",
+ });
return room;
}
export async function getRoomById(roomId: string, username: string) {
- const { data: membership } = await supabaseAdmin.from("room_members").select("role").eq("room_id", roomId).eq("github_username", username).single();
+ const { data: membership } = await supabaseAdmin
+ .from("room_members")
+ .select("role")
+ .eq("room_id", roomId)
+ .eq("github_username", username)
+ .single();
if (!membership) return null;
- const { data: room } = await supabaseAdmin.from("collaboration_rooms").select("*").eq("id", roomId).single();
+ const { data: room } = await supabaseAdmin
+ .from("collaboration_rooms")
+ .select("*")
+ .eq("id", roomId)
+ .single();
return room ? { ...room, is_owner: membership.role === "owner" } : null;
}
export async function getRoomMembers(roomId: string): Promise {
- const { data, error } = await supabaseAdmin.from("room_members").select("*").eq("room_id", roomId).order("joined_at", { ascending: true });
+ const { data, error } = await supabaseAdmin
+ .from("room_members")
+ .select("*")
+ .eq("room_id", roomId)
+ .order("joined_at", { ascending: true });
if (error) throw error;
return data ?? [];
}
export async function addRoomMember(roomId: string, githubUsername: string) {
- const { error } = await supabaseAdmin.from("room_members").insert({ room_id: roomId, github_username: githubUsername, role: "member" });
+ const { error } = await supabaseAdmin
+ .from("room_members")
+ .insert({
+ room_id: roomId,
+ github_username: githubUsername,
+ role: "member",
+ });
if (error) throw error;
}
-export async function getRoomMessages(roomId: string, limit = 50, before?: string): Promise {
- let query = supabaseAdmin.from("room_messages").select("*").eq("room_id", roomId).order("created_at", { ascending: false }).limit(limit);
+export async function createRoomInvitation(
+ roomId: string,
+ githubUsername: string,
+ invitedBy: string
+) {
+ const { data, error } = await supabaseAdmin
+ .from("room_invitations")
+ .insert({
+ room_id: roomId,
+ github_username: githubUsername,
+ invited_by: invitedBy,
+ status: "pending",
+ })
+ .select()
+ .single();
+ if (error) throw error;
+ return data;
+}
+
+export async function getPendingInvitationsForUser(githubUsername: string) {
+ const { data, error } = await supabaseAdmin
+ .from("room_invitations")
+ .select(
+ `id, room_id, invited_by, created_at, collaboration_rooms (id, name, repo_owner, repo_name)`
+ )
+ .eq("github_username", githubUsername)
+ .eq("status", "pending")
+ .order("created_at", { ascending: false });
+ if (error) throw error;
+ return data ?? [];
+}
+
+export async function getRoomInvitation(invitationId: string) {
+ const { data, error } = await supabaseAdmin
+ .from("room_invitations")
+ .select("*")
+ .eq("id", invitationId)
+ .single();
+ if (error) return null;
+ return data;
+}
+
+export async function respondToRoomInvitation(
+ invitationId: string,
+ accept: boolean
+) {
+ const invitation = await getRoomInvitation(invitationId);
+ if (!invitation || invitation.status !== "pending") return null;
+
+ const { error: updateError } = await supabaseAdmin
+ .from("room_invitations")
+ .update({
+ status: accept ? "accepted" : "declined",
+ responded_at: new Date().toISOString(),
+ })
+ .eq("id", invitationId);
+ if (updateError) throw updateError;
+
+ if (accept) {
+ await addRoomMember(invitation.room_id, invitation.github_username);
+ }
+
+ return invitation;
+}
+
+async function getUserIdByGithubLogin(
+ githubLogin: string
+): Promise {
+ const { data, error } = await supabaseAdmin
+ .from("users")
+ .select("id")
+ .eq("github_login", githubLogin)
+ .single();
+ if (error) return null;
+ return data?.id ?? null;
+}
+
+export async function notifyRoomInvitation(
+ githubUsername: string,
+ roomName: string,
+ invitedBy: string
+) {
+ const userId = await getUserIdByGithubLogin(githubUsername);
+ if (!userId) return;
+ const { error } = await supabaseAdmin.from("notifications").insert({
+ user_id: userId,
+ type: "room_invitation",
+ message: `${invitedBy} invited you to join the room "${roomName}"`,
+ });
+ if (error) throw error;
+}
+
+export async function getRoomMessages(
+ roomId: string,
+ limit = 50,
+ before?: string
+): Promise {
+ let query = supabaseAdmin
+ .from("room_messages")
+ .select("*")
+ .eq("room_id", roomId)
+ .order("created_at", { ascending: false })
+ .limit(limit);
if (before) query = query.lt("created_at", before);
const { data, error } = await query;
if (error) throw error;
return (data ?? []).reverse();
}
-export async function getRoomMessagesSince(roomId: string, after: string): Promise {
+export async function getRoomMessagesSince(
+ roomId: string,
+ after: string
+): Promise {
const { data, error } = await supabaseAdmin
.from("room_messages")
.select("*")
@@ -60,13 +203,30 @@ export async function getRoomMessagesSince(roomId: string, after: string): Promi
return data ?? [];
}
-export async function sendRoomMessage(roomId: string, senderUsername: string, senderAvatar: string | null, content: string): Promise {
- const { data, error } = await supabaseAdmin.from("room_messages").insert({ room_id: roomId, sender_username: senderUsername, sender_avatar: senderAvatar, content }).select().single();
+export async function sendRoomMessage(
+ roomId: string,
+ senderUsername: string,
+ senderAvatar: string | null,
+ content: string
+): Promise {
+ const { data, error } = await supabaseAdmin
+ .from("room_messages")
+ .insert({
+ room_id: roomId,
+ sender_username: senderUsername,
+ sender_avatar: senderAvatar,
+ content,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
}
-export async function removeRoomMember(roomId: string, githubUsername: string): Promise {
+export async function removeRoomMember(
+ roomId: string,
+ githubUsername: string
+): Promise {
const { error } = await supabaseAdmin
.from("room_members")
.delete()
diff --git a/src/types/rooms.ts b/src/types/rooms.ts
index 516987233..d415b47a6 100644
--- a/src/types/rooms.ts
+++ b/src/types/rooms.ts
@@ -42,6 +42,18 @@ export interface InvitePayload {
room_id: string;
github_username: string;
}
+export interface RoomInvitation {
+ id: string;
+ room_id: string;
+ invited_by: string;
+ created_at: string;
+ collaboration_rooms: {
+ id: string;
+ name: string;
+ repo_owner: string;
+ repo_name: string;
+ };
+}
export interface SendMessagePayload {
room_id: string;
diff --git a/supabase/migrations/20260729041910_add_room_invitations.sql b/supabase/migrations/20260729041910_add_room_invitations.sql
new file mode 100644
index 000000000..e6832f6cf
--- /dev/null
+++ b/supabase/migrations/20260729041910_add_room_invitations.sql
@@ -0,0 +1,32 @@
+-- Adds a pending-invitation flow for collaboration rooms.
+-- Previously, inviting a user via github_username added them to
+-- room_members immediately. This table stores a pending invitation
+-- that the invited user must accept before becoming a room member.
+
+CREATE TABLE IF NOT EXISTS room_invitations (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ room_id UUID REFERENCES collaboration_rooms(id) ON DELETE CASCADE,
+ github_username TEXT NOT NULL,
+ invited_by TEXT NOT NULL,
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined')),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ responded_at TIMESTAMPTZ,
+ UNIQUE(room_id, github_username, status)
+);
+
+CREATE INDEX IF NOT EXISTS room_invitations_invitee_idx
+ ON room_invitations(github_username, status);
+
+ALTER TABLE room_invitations ENABLE ROW LEVEL SECURITY;
+
+DROP POLICY IF EXISTS "invitation_select" ON room_invitations;
+CREATE POLICY "invitation_select" ON room_invitations
+ FOR SELECT USING (
+ github_username = current_setting('request.jwt.claims', true)::json->>'login'
+ OR EXISTS (
+ SELECT 1 FROM room_members
+ WHERE room_id = room_invitations.room_id
+ AND github_username = current_setting('request.jwt.claims', true)::json->>'login'
+ AND role = 'owner'
+ )
+ );
\ No newline at end of file
diff --git a/supabase/schema.sql b/supabase/schema.sql
index 214709c65..6f474a467 100644
--- a/supabase/schema.sql
+++ b/supabase/schema.sql
@@ -277,9 +277,22 @@ CREATE TABLE IF NOT EXISTS room_messages (
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
+CREATE TABLE IF NOT EXISTS room_invitations (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ room_id UUID REFERENCES collaboration_rooms(id) ON DELETE CASCADE,
+ github_username TEXT NOT NULL,
+ invited_by TEXT NOT NULL,
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined')),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ responded_at TIMESTAMPTZ,
+ UNIQUE(room_id, github_username, status)
+);
+CREATE INDEX IF NOT EXISTS room_invitations_invitee_idx
+ ON room_invitations(github_username, status);
ALTER TABLE collaboration_rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE room_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE room_messages ENABLE ROW LEVEL SECURITY;
+ALTER TABLE room_invitations ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "room_select" ON collaboration_rooms;
CREATE POLICY "room_select" ON collaboration_rooms
FOR SELECT USING (
@@ -306,6 +319,17 @@ CREATE POLICY "message_insert" ON room_messages
AND github_username = current_setting('request.jwt.claims', true)::json->>'login'
)
);
+DROP POLICY IF EXISTS "invitation_select" ON room_invitations;
+CREATE POLICY "invitation_select" ON room_invitations
+ FOR SELECT USING (
+ github_username = current_setting('request.jwt.claims', true)::json->>'login'
+ OR EXISTS (
+ SELECT 1 FROM room_members
+ WHERE room_id = room_invitations.room_id
+ AND github_username = current_setting('request.jwt.claims', true)::json->>'login'
+ AND role = 'owner'
+ )
+ );
CREATE TABLE IF NOT EXISTS leaderboard_cache (
key text primary key,
payload jsonb,