From 57f7bda3e7582516a4cd20f833c7d7955c67d8d7 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Sun, 7 Jun 2026 22:15:18 +0530 Subject: [PATCH] fix(rooms): normalize github usernames --- src/app/api/rooms/[roomId]/invite/route.ts | 22 ++++++++++++-------- src/app/api/rooms/[roomId]/messages/route.ts | 14 ++++++------- src/app/api/rooms/[roomId]/route.ts | 10 ++++----- src/app/api/rooms/route.ts | 10 ++++----- src/lib/rooms.ts | 11 ++++++++++ test/rooms.test.ts | 22 ++++++++++++++++++++ 6 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 src/lib/rooms.ts create mode 100644 test/rooms.test.ts diff --git a/src/app/api/rooms/[roomId]/invite/route.ts b/src/app/api/rooms/[roomId]/invite/route.ts index ac838d64..c6e2bf73 100644 --- a/src/app/api/rooms/[roomId]/invite/route.ts +++ b/src/app/api/rooms/[roomId]/invite/route.ts @@ -1,6 +1,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { getRoomById, getRoomMembers, addRoomMember } from '@/lib/supabase-rooms'; +import { githubUsernamesEqual, normalizeRoomGithubUsername } from '@/lib/rooms'; import { NextResponse } from 'next/server'; export async function POST( @@ -8,16 +9,17 @@ export async function POST( { params }: { params: { roomId: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const room = await getRoomById(params.roomId, session.user.name); + const room = await getRoomById(params.roomId, session.githubLogin); 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 }); const { github_username } = await req.json(); - if (!github_username?.trim()) - return NextResponse.json({ error: 'github_username required' }, { status: 400 }); - const ghRes = await fetch(`https://api.github.com/users/${github_username}`, { + const normalizedUsername = normalizeRoomGithubUsername(github_username); + if (!normalizedUsername) + return NextResponse.json({ error: 'Valid github_username required' }, { status: 400 }); + const ghRes = await fetch(`https://api.github.com/users/${encodeURIComponent(normalizedUsername)}`, { headers: { Accept: 'application/vnd.github+json', ...(process.env.GITHUB_TOKEN @@ -26,12 +28,14 @@ export async function POST( }, }); if (ghRes.status === 404) - return NextResponse.json({ error: `GitHub user "${github_username}" does not exist` }, { status: 404 }); + return NextResponse.json({ error: `GitHub user "${normalizedUsername}" does not exist` }, { status: 404 }); if (!ghRes.ok) return NextResponse.json({ error: 'Could not verify GitHub user' }, { status: 502 }); + const githubUser = await ghRes.json() as { login?: string }; + const canonicalUsername = normalizeRoomGithubUsername(githubUser.login) ?? normalizedUsername; const members = await getRoomMembers(params.roomId); - if (members.some((m) => m.github_username === github_username)) + if (members.some((m) => githubUsernamesEqual(m.github_username, canonicalUsername))) return NextResponse.json({ error: 'User is already a member' }, { status: 409 }); - await addRoomMember(params.roomId, github_username); + await addRoomMember(params.roomId, canonicalUsername); return NextResponse.json({ success: true }); -} \ No newline at end of file +} diff --git a/src/app/api/rooms/[roomId]/messages/route.ts b/src/app/api/rooms/[roomId]/messages/route.ts index aa9bf597..a2423276 100644 --- a/src/app/api/rooms/[roomId]/messages/route.ts +++ b/src/app/api/rooms/[roomId]/messages/route.ts @@ -9,9 +9,9 @@ export async function GET( { params }: { params: { roomId: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const room = await getRoomById(params.roomId, session.user.name); + const room = await getRoomById(params.roomId, session.githubLogin); if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); const url = new URL(req.url); const before = url.searchParams.get('before') ?? undefined; @@ -24,9 +24,9 @@ export async function POST( { params }: { params: { roomId: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const room = await getRoomById(params.roomId, session.user.name); + const room = await getRoomById(params.roomId, session.githubLogin); if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); const body = await req.json(); const validation = validateTextInput(body?.content, 'content', 4000); @@ -34,9 +34,9 @@ export async function POST( return NextResponse.json({ error: validation.error }, { status: 400 }); const message = await sendRoomMessage( params.roomId, - session.user.name, - session.user.image ?? null, + session.githubLogin, + session.user?.image ?? null, validation.value ); return NextResponse.json(message, { status: 201 }); -} \ No newline at end of file +} diff --git a/src/app/api/rooms/[roomId]/route.ts b/src/app/api/rooms/[roomId]/route.ts index 8000688e..7f1a720b 100644 --- a/src/app/api/rooms/[roomId]/route.ts +++ b/src/app/api/rooms/[roomId]/route.ts @@ -9,9 +9,9 @@ export async function GET( { params }: { params: { roomId: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const room = await getRoomById(params.roomId, session.user.name); + const room = await getRoomById(params.roomId, session.githubLogin); if (!room) return NextResponse.json({ error: 'Not found or not a member' }, { status: 404 }); const members = await getRoomMembers(params.roomId); return NextResponse.json({ ...room, members }); @@ -22,10 +22,10 @@ export async function DELETE( { params }: { params: { roomId: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const room = await getRoomById(params.roomId, session.user.name); + const room = await getRoomById(params.roomId, session.githubLogin); if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); if (!room.is_owner) return NextResponse.json({ error: 'Only the owner can delete this room' }, { status: 403 }); @@ -37,4 +37,4 @@ export async function DELETE( if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ success: true }); -} \ No newline at end of file +} diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts index ab0b328c..9799bcf7 100644 --- a/src/app/api/rooms/route.ts +++ b/src/app/api/rooms/route.ts @@ -6,10 +6,10 @@ import type { CreateRoomPayload } from '@/types/rooms'; export async function GET() { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); try { - const rooms = await getRoomsForUser(session.user.name); + const rooms = await getRoomsForUser(session.githubLogin); return NextResponse.json(rooms); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); @@ -18,15 +18,15 @@ export async function GET() { export async function POST(req: Request) { const session = await getServerSession(authOptions); - if (!session?.user?.name) + if (!session?.githubLogin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body: CreateRoomPayload = await req.json(); if (!body.name?.trim() || !body.repo_owner?.trim() || !body.repo_name?.trim()) return NextResponse.json({ error: 'name, repo_owner, and repo_name are required' }, { status: 400 }); try { - const room = await createRoom(body, session.user.name); + const room = await createRoom(body, session.githubLogin); return NextResponse.json(room, { status: 201 }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/lib/rooms.ts b/src/lib/rooms.ts new file mode 100644 index 00000000..161cb0b3 --- /dev/null +++ b/src/lib/rooms.ts @@ -0,0 +1,11 @@ +import { normalizeGitHubUsername } from "./validate-github-username"; + +export function normalizeRoomGithubUsername( + value: string | null | undefined +): string | null { + return normalizeGitHubUsername(value); +} + +export function githubUsernamesEqual(a: string, b: string): boolean { + return a.toLowerCase() === b.toLowerCase(); +} diff --git a/test/rooms.test.ts b/test/rooms.test.ts new file mode 100644 index 00000000..256806c6 --- /dev/null +++ b/test/rooms.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { + githubUsernamesEqual, + normalizeRoomGithubUsername, +} from "@/lib/rooms"; + +describe("room username helpers", () => { + it("normalizes valid GitHub usernames", () => { + expect(normalizeRoomGithubUsername(" Octocat ")).toBe("Octocat"); + }); + + it("rejects invalid GitHub usernames", () => { + expect(normalizeRoomGithubUsername("../octocat")).toBeNull(); + expect(normalizeRoomGithubUsername("-octocat")).toBeNull(); + expect(normalizeRoomGithubUsername("octocat-")).toBeNull(); + }); + + it("compares GitHub usernames case-insensitively", () => { + expect(githubUsernamesEqual("Octocat", "octocat")).toBe(true); + expect(githubUsernamesEqual("hubot", "octocat")).toBe(false); + }); +});