diff --git a/src/server.ts b/src/server.ts index d5f5d47..6963f54 100644 --- a/src/server.ts +++ b/src/server.ts @@ -72,7 +72,10 @@ async function authorizeSamChat( } catch { return new Response("Unauthorized", { status: 401 }); } - const session = await SamSessionRepository.getActiveSession(sessionId); + const session = await SamSessionRepository.getActiveSession( + sessionId, + context.userId, + ); const project = session ? await ProjectRepository.getProjectForOrganization( session.projectId, diff --git a/src/server/features/sam/SamSessionRepository.ts b/src/server/features/sam/SamSessionRepository.ts index 9430c92..15e5e74 100644 --- a/src/server/features/sam/SamSessionRepository.ts +++ b/src/server/features/sam/SamSessionRepository.ts @@ -21,7 +21,8 @@ async function createSession(input: CreateSamSessionInput) { } // Callers must have already authorized the project (requireProjectContext). -async function listSessionsForProject(projectId: string) { +// Scoped to userId so a project member only sees their own sessions. +async function listSessionsForProject(projectId: string, userId: string) { return db .select({ id: samSessions.id, @@ -31,7 +32,11 @@ async function listSessionsForProject(projectId: string) { }) .from(samSessions) .where( - and(eq(samSessions.projectId, projectId), isNull(samSessions.archivedAt)), + and( + eq(samSessions.projectId, projectId), + eq(samSessions.userId, userId), + isNull(samSessions.archivedAt), + ), ) .orderBy(desc(samSessions.updatedAt), desc(samSessions.id)); } @@ -49,16 +54,23 @@ async function getSessionById(id: string) { return row ?? null; } -// Excludes archived sessions so callers treat them like deleted ones -// (connection refused / not archivable) even though the row and DO transcript -// are kept. Does NOT authorize: callers must check the caller's access to +// Look up a caller's own active session by id, scoped to userId so one org +// member can't act on another's session. Excludes archived sessions so callers +// treat them like deleted ones (connection refused / not archivable) even +// though the row and DO transcript are kept. Callers must still check access to // row.projectId via the canonical project-access path // (ProjectRepository.getProjectForOrganization) before acting on the session. -async function getActiveSession(id: string) { +async function getActiveSession(id: string, userId: string) { const [row] = await db .select() .from(samSessions) - .where(and(eq(samSessions.id, id), isNull(samSessions.archivedAt))) + .where( + and( + eq(samSessions.id, id), + eq(samSessions.userId, userId), + isNull(samSessions.archivedAt), + ), + ) .limit(1); return row ?? null; } diff --git a/src/serverFunctions/sam.ts b/src/serverFunctions/sam.ts index f263df0..ebe22d0 100644 --- a/src/serverFunctions/sam.ts +++ b/src/serverFunctions/sam.ts @@ -17,7 +17,10 @@ export const listSamSessions = createServerFn({ method: "GET" }) .middleware(requireProjectContext) .inputValidator((data: unknown) => projectScopedSchema.parse(data)) .handler(async ({ context }) => { - return SamSessionRepository.listSessionsForProject(context.projectId); + return SamSessionRepository.listSessionsForProject( + context.projectId, + context.userId, + ); }); // Creates a new SAM chat session and returns its id; the client then opens a DO @@ -47,7 +50,10 @@ export const archiveSamSession = createServerFn({ method: "POST" }) .handler(async ({ data, context }) => { // Authorize against the session's project (the canonical project-access // path), not the caller's org directly. - const session = await SamSessionRepository.getActiveSession(data.sessionId); + const session = await SamSessionRepository.getActiveSession( + data.sessionId, + context.userId, + ); const project = session ? await ProjectRepository.getProjectForOrganization( session.projectId,