-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Bearer token auth to /api/chats endpoint #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8caa0c9
fc4039f
0b726f3
d1e5675
0fbf0a4
add518d
08f3ab7
69fca2d
59c7af1
ba22bba
a240172
6e48b24
7371e58
fa1b91d
b818e58
ede2f06
3608c28
d72faf5
65ac49d
c6f1dfb
ed3759c
b118128
283132e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getAgentCreatorHandler } from "@/lib/agentCreator/getAgentCreatorHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/agent-creator | ||
| * | ||
| * Fetch agent creator information for display in the UI. | ||
| * | ||
| * This is a public endpoint that does not require authentication. | ||
| * | ||
| * Query parameters: | ||
| * - creatorId: Required - The account ID of the agent creator | ||
| * | ||
| * @param request - The request object | ||
| * @returns A NextResponse with creator info (name, image, is_admin) or an error | ||
| */ | ||
| export async function GET(request: NextRequest): Promise<NextResponse> { | ||
| return getAgentCreatorHandler(request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { toggleAgentTemplateFavoriteHandler } from "@/lib/agentTemplates/toggleAgentTemplateFavoriteHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * POST /api/agent-templates/favorites | ||
| * | ||
| * Toggle a template's favorite status for the authenticated user. | ||
| * | ||
| * Authentication: Authorization Bearer token required. | ||
| * | ||
| * Request body: | ||
| * - templateId: string - The template ID to favorite/unfavorite | ||
| * - isFavourite: boolean - true to add, false to remove | ||
| * | ||
| * @param request - The request object | ||
| * @returns A NextResponse with success or an error | ||
| */ | ||
| export async function POST(request: NextRequest): Promise<NextResponse> { | ||
| return toggleAgentTemplateFavoriteHandler(request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getAgentTemplatesHandler } from "@/lib/agentTemplates/getAgentTemplatesHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/agent-templates | ||
| * | ||
| * Fetch agent templates accessible to the authenticated user. | ||
| * | ||
| * Authentication: Authorization Bearer token required. | ||
| * | ||
| * Query parameters: | ||
| * - userId: Optional user ID (defaults to authenticated user) | ||
| * | ||
| * @param request - The request object | ||
| * @returns A NextResponse with the templates array or an error | ||
| */ | ||
| export async function GET(request: NextRequest): Promise<NextResponse> { | ||
| return getAgentTemplatesHandler(request); | ||
sweetmantech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getAiModelsHandler } from "@/lib/aiModels/getAiModelsHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/ai/models | ||
| * | ||
| * Fetch the list of available AI models from the Vercel AI Gateway. | ||
| * | ||
| * This is a public endpoint that does not require authentication. | ||
| * It returns models suitable for chat, filtering out embed models. | ||
| * | ||
| * @returns A NextResponse with { models: GatewayLanguageModelEntry[] } or an error | ||
| */ | ||
| export async function GET(): Promise<NextResponse> { | ||
| return getAiModelsHandler(); | ||
| } | ||
|
|
||
| // Disable caching to always serve the latest model list. | ||
| export const dynamic = "force-dynamic"; | ||
| export const fetchCache = "force-no-store"; | ||
| export const revalidate = 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getArtistAgentsHandler } from "@/lib/artistAgents/getArtistAgentsHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find and examine the getCorsHeaders function
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "getCorsHeaders" | head -20Repository: Recoupable-com/Recoup-API Length of output: 747 🏁 Script executed: #!/bin/bash
# Look for the getCorsHeaders implementation
rg -A 20 "export.*getCorsHeaders|function getCorsHeaders" --type ts --type tsxRepository: Recoupable-com/Recoup-API Length of output: 96 🏁 Script executed: #!/bin/bash
# Check for the specific file mentioned in the review
cat lib/networking/getCorsHeaders.ts 2>/dev/null || echo "File not found at expected location"Repository: Recoupable-com/Recoup-API Length of output: 414 🏁 Script executed: #!/bin/bash
# Search for Access-Control-Allow-Headers in the codebase
rg "Access-Control-Allow-Headers" --type ts --type tsx -B 2 -A 2Repository: Recoupable-com/Recoup-API Length of output: 96 🏁 Script executed: #!/bin/bash
# Search for x-api-key usage in the codebase
rg "x-api-key|X-Api-Key|X-API-KEY" --type ts -iRepository: Recoupable-com/Recoup-API Length of output: 7484 🏁 Script executed: #!/bin/bash
# Check the artist-agents route specifically
cat app/api/artist-agents/route.tsRepository: Recoupable-com/Recoup-API Length of output: 1087 🏁 Script executed: #!/bin/bash
# Search for any authentication patterns in api routes
rg "Authorization|x-api-key" app/api/ --type ts -B 2 -A 2 | head -50Repository: Recoupable-com/Recoup-API Length of output: 2749 Add The endpoint's JSDoc states it accepts 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * GET /api/artist-agents | ||
| * | ||
| * Fetch artist agents by social IDs. | ||
| * | ||
| * Authentication: x-api-key header OR Authorization Bearer token required. | ||
| * Exactly one authentication mechanism must be provided. | ||
| * | ||
| * Query parameters: | ||
| * - socialId: One or more social IDs (can be repeated, e.g., ?socialId=123&socialId=456) | ||
| * | ||
| * @param request - The request object | ||
| * @returns A NextResponse with the agents array or an error | ||
| */ | ||
| export async function GET(request: NextRequest): Promise<NextResponse> { | ||
| return getArtistAgentsHandler(request); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { ADMIN_EMAILS } from "@/lib/const"; | ||
|
|
||
| describe("lib/const", () => { | ||
| describe("ADMIN_EMAILS", () => { | ||
| it("should export ADMIN_EMAILS as an array", () => { | ||
| expect(Array.isArray(ADMIN_EMAILS)).toBe(true); | ||
| }); | ||
|
|
||
| it("should contain at least one admin email", () => { | ||
| expect(ADMIN_EMAILS.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("should contain valid email strings", () => { | ||
| for (const email of ADMIN_EMAILS) { | ||
| expect(typeof email).toBe("string"); | ||
| expect(email).toMatch(/@/); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { NextRequest } from "next/server"; | ||
| import { getAgentCreatorHandler } from "../getAgentCreatorHandler"; | ||
|
|
||
| import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; | ||
| import { ADMIN_EMAILS } from "@/lib/const"; | ||
|
|
||
| // Mock dependencies | ||
| vi.mock("@/lib/supabase/accounts/getAccountWithDetails", () => ({ | ||
| getAccountWithDetails: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/const", async (importOriginal) => { | ||
| const actual = await importOriginal<typeof import("@/lib/const")>(); | ||
| return { | ||
| ...actual, | ||
| ADMIN_EMAILS: ["admin@example.com"], | ||
| }; | ||
| }); | ||
|
|
||
| vi.mock("@/lib/networking/getCorsHeaders", () => ({ | ||
| getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), | ||
| })); | ||
|
|
||
| /** | ||
| * Creates a mock request with creatorId query param | ||
| * | ||
| * @param creatorId | ||
| */ | ||
| function createMockRequest(creatorId?: string): NextRequest { | ||
| const url = new URL("http://localhost/api/agent-creator"); | ||
| if (creatorId) { | ||
| url.searchParams.set("creatorId", creatorId); | ||
| } | ||
| return { | ||
| url: url.toString(), | ||
| nextUrl: { | ||
| searchParams: url.searchParams, | ||
| }, | ||
| } as unknown as NextRequest; | ||
| } | ||
|
|
||
| describe("getAgentCreatorHandler", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe("validation", () => { | ||
| it("returns 400 when creatorId is missing", async () => { | ||
| const request = createMockRequest(); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(400); | ||
| expect(json.message).toBe("Missing creatorId"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("successful responses", () => { | ||
| it("returns creator info with name and image", async () => { | ||
| vi.mocked(getAccountWithDetails).mockResolvedValue({ | ||
| id: "creator-123", | ||
| name: "Test Creator", | ||
| created_at: "2024-01-01T00:00:00Z", | ||
| organization_id: null, | ||
| image: "https://example.com/avatar.jpg", | ||
| email: "testcreator@example.com", | ||
| wallet: null, | ||
| account_id: "creator-123", | ||
| }); | ||
|
|
||
| const request = createMockRequest("creator-123"); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(json.creator).toEqual({ | ||
| name: "Test Creator", | ||
| image: "https://example.com/avatar.jpg", | ||
| is_admin: false, | ||
| }); | ||
| expect(getAccountWithDetails).toHaveBeenCalledWith("creator-123"); | ||
| }); | ||
|
|
||
| it("returns is_admin true for admin emails", async () => { | ||
| vi.mocked(getAccountWithDetails).mockResolvedValue({ | ||
| id: "admin-123", | ||
| name: "Admin User", | ||
| created_at: "2024-01-01T00:00:00Z", | ||
| organization_id: null, | ||
| image: "https://example.com/admin.jpg", | ||
| email: "admin@example.com", // This matches ADMIN_EMAILS mock | ||
| wallet: null, | ||
| account_id: "admin-123", | ||
| }); | ||
|
|
||
| const request = createMockRequest("admin-123"); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(json.creator.is_admin).toBe(true); | ||
| }); | ||
|
|
||
| it("returns null values when account has no name or image", async () => { | ||
| vi.mocked(getAccountWithDetails).mockResolvedValue({ | ||
| id: "creator-456", | ||
| name: null, | ||
| created_at: "2024-01-01T00:00:00Z", | ||
| organization_id: null, | ||
| image: undefined, | ||
| email: "noinfo@example.com", | ||
| wallet: null, | ||
| account_id: "creator-456", | ||
| }); | ||
|
|
||
| const request = createMockRequest("creator-456"); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(json.creator).toEqual({ | ||
| name: null, | ||
| image: null, | ||
| is_admin: false, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("error handling", () => { | ||
| it("returns 404 when creator not found", async () => { | ||
| vi.mocked(getAccountWithDetails).mockResolvedValue(null); | ||
|
|
||
| const request = createMockRequest("nonexistent-123"); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(404); | ||
| expect(json.message).toBe("Creator not found"); | ||
| }); | ||
|
|
||
| it("returns 400 when database query throws", async () => { | ||
| vi.mocked(getAccountWithDetails).mockRejectedValue(new Error("Database error")); | ||
|
|
||
| const request = createMockRequest("creator-123"); | ||
| const response = await getAgentCreatorHandler(request); | ||
| const json = await response.json(); | ||
|
|
||
| expect(response.status).toBe(400); | ||
| expect(json.message).toBe("Database error"); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.