diff --git a/src/app/api/ai/roast/route.ts b/src/app/api/ai/roast/route.ts index 2eb468a8c..577b7b41c 100644 --- a/src/app/api/ai/roast/route.ts +++ b/src/app/api/ai/roast/route.ts @@ -1,11 +1,51 @@ import { NextResponse } from 'next/server'; import { GoogleGenerativeAI } from '@google/generative-ai'; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { resolveAppUser } from "@/lib/resolve-user"; + +import { + upstashRateLimitFixedWindow, + getUpstashConfig, +} from "@/lib/upstash-rest"; + +import { createMemoryFixedWindowRateLimiter } from "@/lib/rate-limit"; // Initialize the Google Generative AI SDK const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); +const AI_ROAST_LIMIT = 5; +const AI_ROAST_WINDOW_SECONDS = 60 * 60; + +const memoryLimiter = createMemoryFixedWindowRateLimiter({ + windowMs: AI_ROAST_WINDOW_SECONDS * 1000, + pruneIntervalMs: AI_ROAST_WINDOW_SECONDS * 1000, + maxEntries: 10_000, +}); export async function POST(req: Request) { - try { + const session = await getServerSession(authOptions); +const user = await resolveAppUser( + session.githubId, + session.githubLogin +); + +if (!user) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); +} + +const userId = user.id; + +if (!session?.githubId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); +} + + try { const body = await req.json(); const { mode, stats } = body; diff --git a/src/app/api/ai/weekly-summary/route.ts b/src/app/api/ai/weekly-summary/route.ts index b467da6c8..4d43d4a93 100644 --- a/src/app/api/ai/weekly-summary/route.ts +++ b/src/app/api/ai/weekly-summary/route.ts @@ -10,7 +10,7 @@ * Security * -------- * - Session required — unauthenticated requests are rejected with 401. - * - ANTHROPIC_API_KEY is read only on the server; it never appears in any + * - ANTHROPIC_API_KEY isa read only on the server; it never appears in any * response or client-accessible bundle. * - Metrics are validated server-side before the prompt is built. * - String fields (topRepo) are truncated to prevent over-long inputs. diff --git a/src/app/api/metrics/repo-explorer/route.ts b/src/app/api/metrics/repo-explorer/route.ts index 7d5810158..eeeff1796 100644 --- a/src/app/api/metrics/repo-explorer/route.ts +++ b/src/app/api/metrics/repo-explorer/route.ts @@ -2,8 +2,10 @@ import { getSessionWithToken } from "@/lib/get-session-token"; import { fetchUserRepos } from "@/lib/github"; import { NextRequest } from "next/server"; import { isMetricsCacheBypassed, metricsCacheKey, withMetricsCache } from "@/lib/metrics-cache"; +import { fetchUserRepos } from "@/lib/github"; import { ExplorerRepoCardData } from "@/lib/repo-analytics-types"; + export const dynamic = "force-dynamic"; const GITHUB_API = "https://api.github.com"; @@ -19,11 +21,20 @@ export async function GET(req: NextRequest) { const bypass = isMetricsCacheBypassed(req); const key = metricsCacheKey(session.githubId ?? session.githubLogin!, "repo-explorer-v2" as any, { days: 7 }); - try { - const data = await withMetricsCache({ bypass, key, ttlSeconds: 30 * 60 }, async () => { - // Paginate through all pages (up to 1000 repos) so users with more - // than 100 repositories see their complete list — fixes #2843. - const repos = await fetchUserRepos(accessToken, { perPage: 100, maxPages: 10 }); +try { + const data = await withMetricsCache( + { bypass, key, ttlSeconds: 30 * 60 }, + async () => { + // Paginate through all pages (up to 1000 repos) so users with more + // than 100 repositories see their complete list — fixes #2843. + const repos = await fetchUserRepos(accessToken, { + perPage: 100, + maxPages: 10, + }); + + // ...rest of the existing code + } + ); const since = new Date(); since.setDate(since.getDate() - 30); const sinceStr = since.toISOString().slice(0, 10); @@ -89,7 +100,8 @@ export async function GET(req: NextRequest) { result.sort((a, b) => b.commitCount - a.commitCount || new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); return { repos: result }; - }); + } + ); return Response.json(data); } catch (error) { console.error(error); diff --git a/src/app/api/rooms/[roomId]/invite/route.ts b/src/app/api/rooms/[roomId]/invite/route.ts index 4867887a9..03092b0ec 100644 --- a/src/app/api/rooms/[roomId]/invite/route.ts +++ b/src/app/api/rooms/[roomId]/invite/route.ts @@ -1,6 +1,11 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; -import { getRoomById, getRoomMembers, addRoomMember } from '@/lib/supabase-rooms'; +import { + getRoomById, + getRoomMembers, + createRoomInvitation, + getPendingInvitation +} from '@/lib/supabase-rooms'; import { NextResponse } from 'next/server'; export async function POST( @@ -36,6 +41,23 @@ export async function POST( 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 + const existingInvite = await getPendingInvitation( + roomId, + github_username +); + +if (existingInvite) { + return NextResponse.json( + { error: "User already has a pending invitation." }, + { status: 409 } + ); +} +await createRoomInvitation( + roomId, + github_username, + session.user.name +); +return NextResponse.json({ + success: true, + message: "Invitation sent." +});} \ No newline at end of file diff --git a/src/lib/supabase-rooms.ts b/src/lib/supabase-rooms.ts index f4c4ff16f..657213608 100644 --- a/src/lib/supabase-rooms.ts +++ b/src/lib/supabase-rooms.ts @@ -35,9 +35,36 @@ export async function getRoomMembers(roomId: string): Promise { if (error) throw error; return data ?? []; } +export async function getPendingInvitation( + roomId: string, + githubUsername: string +) { + const { data, error } = await supabaseAdmin + .from("room_invitations") + .select("id") + .eq("room_id", roomId) + .eq("github_username", githubUsername) + .eq("status", "pending") + .maybeSingle(); + + if (error) throw error; + return data; +} + +export async function createRoomInvitation( + roomId: string, + githubUsername: string, + invitedBy: string +) { + const { error } = await supabaseAdmin + .from("room_invitations") + .insert({ + room_id: roomId, + github_username: githubUsername, + invited_by: invitedBy, + status: "pending", + }); -export async function addRoomMember(roomId: string, githubUsername: string) { - const { error } = await supabaseAdmin.from("room_members").insert({ room_id: roomId, github_username: githubUsername, role: "member" }); if (error) throw error; } diff --git a/supabase/migrations/20260726_create_room_invitations.sql b/supabase/migrations/20260726_create_room_invitations.sql new file mode 100644 index 000000000..6310718d4 --- /dev/null +++ b/supabase/migrations/20260726_create_room_invitations.sql @@ -0,0 +1,13 @@ +create table 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 not null default 'pending', + created_at timestamptz default now(), + responded_at timestamptz +); + +create unique index room_invitation_unique +on room_invitations(room_id, github_username) +where status = 'pending'; \ No newline at end of file