From b6d507fcf5861c582f993cea1b6a833c1dba890e Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Wed, 14 Jan 2026 01:21:58 +1000 Subject: [PATCH] feat: add API key authentication to user and resource endpoints Update multiple endpoints to use authenticateRequest() middleware instead of session-only authentication. This allows integration tests to authenticate using API keys. Endpoints updated: - /api/user/profile - /api/user/likes/tools, collections, agents - /api/agents (list, create) - /api/agents/[id] (get, update, delete) - /api/collections (list, create) - /api/collections/[id] (get, update, delete) --- apps/web/src/app/api/agents/[id]/route.ts | 25 +++++++------ apps/web/src/app/api/agents/route.ts | 24 ++++++------- .../web/src/app/api/collections/[id]/route.ts | 35 ++++++++----------- apps/web/src/app/api/collections/route.ts | 30 +++++++--------- .../src/app/api/user/likes/agents/route.ts | 11 +++--- .../app/api/user/likes/collections/route.ts | 11 +++--- .../web/src/app/api/user/likes/tools/route.ts | 11 +++--- apps/web/src/app/api/user/profile/route.ts | 17 +++++---- 8 files changed, 71 insertions(+), 93 deletions(-) diff --git a/apps/web/src/app/api/agents/[id]/route.ts b/apps/web/src/app/api/agents/[id]/route.ts index 605804c..1870d42 100644 --- a/apps/web/src/app/api/agents/[id]/route.ts +++ b/apps/web/src/app/api/agents/[id]/route.ts @@ -1,9 +1,9 @@ import { Prisma, prisma } from '@tpmjs/db'; import { UpdateAgentSchema } from '@tpmjs/types/agent'; -import { headers } from 'next/headers'; import type { NextRequest } from 'next/server'; import { logActivity } from '~/lib/activity'; +import { authenticateRequest } from '~/lib/api-keys/middleware'; import { apiConflict, apiForbidden, @@ -13,7 +13,6 @@ import { apiUnauthorized, apiValidationError, } from '~/lib/api-response'; -import { auth } from '~/lib/auth'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -30,7 +29,7 @@ export async function GET(_request: NextRequest, context: RouteContext) { const requestId = crypto.randomUUID(); try { - const session = await auth.api.getSession({ headers: await headers() }); + const authResult = await authenticateRequest(); const { id } = await context.params; const agent = await prisma.agent.findUnique({ @@ -88,7 +87,7 @@ export async function GET(_request: NextRequest, context: RouteContext) { } // Check access - owner or public - const isOwner = session?.user?.id === agent.userId; + const isOwner = authResult.userId === agent.userId; if (!isOwner && !agent.isPublic) { return apiForbidden('Access denied', requestId); } @@ -135,8 +134,8 @@ export async function PATCH(request: NextRequest, context: RouteContext) { const requestId = crypto.randomUUID(); try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return apiUnauthorized('Authentication required', requestId); } @@ -159,7 +158,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) { if (!existing) { return apiNotFound('Agent', requestId); } - if (existing.userId !== session.user.id) { + if (existing.userId !== authResult.userId) { return apiForbidden('Access denied', requestId); } @@ -176,7 +175,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) { // Check name uniqueness if being changed if (parsed.data.name) { const existingByName = await prisma.agent.findFirst({ - where: { userId: session.user.id, name: parsed.data.name, id: { not: id } }, + where: { userId: authResult.userId, name: parsed.data.name, id: { not: id } }, }); if (existingByName) { return apiConflict('An agent with this name already exists', requestId); @@ -210,7 +209,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) { // Log activity (fire-and-forget) logActivity({ - userId: session.user.id, + userId: authResult.userId, type: 'AGENT_UPDATED', targetName: agent.name, targetType: 'agent', @@ -240,8 +239,8 @@ export async function DELETE(_request: NextRequest, context: RouteContext) { const requestId = crypto.randomUUID(); try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return apiUnauthorized('Authentication required', requestId); } @@ -255,7 +254,7 @@ export async function DELETE(_request: NextRequest, context: RouteContext) { if (!existing) { return apiNotFound('Agent', requestId); } - if (existing.userId !== session.user.id) { + if (existing.userId !== authResult.userId) { return apiForbidden('Access denied', requestId); } @@ -263,7 +262,7 @@ export async function DELETE(_request: NextRequest, context: RouteContext) { // Log activity (fire-and-forget) - note: agentId is not included since agent is deleted logActivity({ - userId: session.user.id, + userId: authResult.userId, type: 'AGENT_DELETED', targetName: existing.name, targetType: 'agent', diff --git a/apps/web/src/app/api/agents/route.ts b/apps/web/src/app/api/agents/route.ts index 52fbafa..6076801 100644 --- a/apps/web/src/app/api/agents/route.ts +++ b/apps/web/src/app/api/agents/route.ts @@ -1,10 +1,9 @@ import { prisma } from '@tpmjs/db'; import { AGENT_LIMITS, CreateAgentSchema } from '@tpmjs/types/agent'; -import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; import { logActivity } from '~/lib/activity'; -import { auth } from '~/lib/auth'; +import { authenticateRequest } from '~/lib/api-keys/middleware'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -28,8 +27,8 @@ function generateUid(name: string): string { */ export async function GET(request: NextRequest): Promise { try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } @@ -38,7 +37,7 @@ export async function GET(request: NextRequest): Promise { const offset = Number.parseInt(searchParams.get('offset') || '0'); const agents = await prisma.agent.findMany({ - where: { userId: session.user.id }, + where: { userId: authResult.userId }, select: { id: true, uid: true, @@ -94,10 +93,11 @@ export async function GET(request: NextRequest): Promise { */ export async function POST(request: NextRequest): Promise { try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } + const userId = authResult.userId; const body = await request.json(); const parsed = CreateAgentSchema.safeParse(body); @@ -110,7 +110,7 @@ export async function POST(request: NextRequest): Promise { // Check agent limit const agentCount = await prisma.agent.count({ - where: { userId: session.user.id }, + where: { userId }, }); if (agentCount >= AGENT_LIMITS.MAX_AGENTS_PER_USER) { return NextResponse.json( @@ -146,7 +146,7 @@ export async function POST(request: NextRequest): Promise { // Check for name uniqueness within user's agents const existingByName = await prisma.agent.findFirst({ - where: { userId: session.user.id, name }, + where: { userId, name }, }); if (existingByName) { return NextResponse.json( @@ -159,7 +159,7 @@ export async function POST(request: NextRequest): Promise { const agent = await prisma.$transaction(async (tx) => { const newAgent = await tx.agent.create({ data: { - userId: session.user.id, + userId, uid: finalUid, name, description, @@ -214,7 +214,7 @@ export async function POST(request: NextRequest): Promise { // Auto-like the agent await tx.agentLike.create({ data: { - userId: session.user.id, + userId, agentId: newAgent.id, }, }); @@ -224,7 +224,7 @@ export async function POST(request: NextRequest): Promise { // Log activity (fire-and-forget) logActivity({ - userId: session.user.id, + userId, type: 'AGENT_CREATED', targetName: agent.name, targetType: 'agent', diff --git a/apps/web/src/app/api/collections/[id]/route.ts b/apps/web/src/app/api/collections/[id]/route.ts index 52b4c65..075ef7b 100644 --- a/apps/web/src/app/api/collections/[id]/route.ts +++ b/apps/web/src/app/api/collections/[id]/route.ts @@ -1,9 +1,8 @@ import { Prisma, prisma } from '@tpmjs/db'; import { UpdateCollectionSchema } from '@tpmjs/types/collection'; -import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; import { logActivity } from '~/lib/activity'; -import { auth } from '~/lib/auth'; +import { authenticateRequest } from '~/lib/api-keys/middleware'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -55,11 +54,9 @@ export async function GET( try { // Check authentication - const session = await auth.api.getSession({ - headers: await headers(), - }); + const authResult = await authenticateRequest(); - if (!session) { + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json( { success: false, @@ -113,7 +110,7 @@ export async function GET( } // Check ownership (unless collection is public) - if (collection.userId !== session.user.id && !collection.isPublic) { + if (collection.userId !== authResult.userId && !collection.isPublic) { return NextResponse.json( { success: false, @@ -139,7 +136,7 @@ export async function GET( toolCount: collection._count.tools, createdAt: collection.createdAt, updatedAt: collection.updatedAt, - isOwner: collection.userId === session.user.id, + isOwner: collection.userId === authResult.userId, user: { username: collection.user.username, }, @@ -192,11 +189,9 @@ export async function PATCH( try { // Check authentication - const session = await auth.api.getSession({ - headers: await headers(), - }); + const authResult = await authenticateRequest(); - if (!session) { + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json( { success: false, @@ -223,7 +218,7 @@ export async function PATCH( ); } - if (existingCollection.userId !== session.user.id) { + if (existingCollection.userId !== authResult.userId) { return NextResponse.json( { success: false, @@ -259,7 +254,7 @@ export async function PATCH( if (name && name !== existingCollection.name) { const duplicateName = await prisma.collection.findFirst({ where: { - userId: session.user.id, + userId: authResult.userId, name: { equals: name, mode: 'insensitive' }, id: { not: id }, }, @@ -302,7 +297,7 @@ export async function PATCH( // Log activity (fire-and-forget) logActivity({ - userId: session.user.id, + userId: authResult.userId, type: 'COLLECTION_UPDATED', targetName: collection.name, targetType: 'collection', @@ -350,11 +345,9 @@ export async function DELETE( try { // Check authentication - const session = await auth.api.getSession({ - headers: await headers(), - }); + const authResult = await authenticateRequest(); - if (!session) { + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json( { success: false, @@ -381,7 +374,7 @@ export async function DELETE( ); } - if (collection.userId !== session.user.id) { + if (collection.userId !== authResult.userId) { return NextResponse.json( { success: false, @@ -402,7 +395,7 @@ export async function DELETE( // Log activity (fire-and-forget) - note: collectionId not included since it's deleted logActivity({ - userId: session.user.id, + userId: authResult.userId, type: 'COLLECTION_DELETED', targetName: collectionName, targetType: 'collection', diff --git a/apps/web/src/app/api/collections/route.ts b/apps/web/src/app/api/collections/route.ts index d646873..36eadd0 100644 --- a/apps/web/src/app/api/collections/route.ts +++ b/apps/web/src/app/api/collections/route.ts @@ -1,9 +1,8 @@ import { prisma } from '@tpmjs/db'; import { COLLECTION_LIMITS, CreateCollectionSchema } from '@tpmjs/types/collection'; -import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; import { logActivity } from '~/lib/activity'; -import { auth } from '~/lib/auth'; +import { authenticateRequest } from '~/lib/api-keys/middleware'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -89,11 +88,9 @@ export async function GET(request: NextRequest): Promise= COLLECTION_LIMITS.MAX_COLLECTIONS_PER_USER) { @@ -227,7 +223,7 @@ export async function POST(request: NextRequest): Promise { const newCollection = await tx.collection.create({ data: { - userId: session.user.id, + userId, name, slug, description: description || null, @@ -265,7 +261,7 @@ export async function POST(request: NextRequest): Promise { try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } const user = await prisma.user.findUnique({ - where: { id: session.user.id }, + where: { id: authResult.userId }, select: { id: true, name: true, @@ -51,8 +50,8 @@ export async function GET(): Promise { */ export async function PATCH(request: NextRequest): Promise { try { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session?.user?.id) { + const authResult = await authenticateRequest(); + if (!authResult.authenticated || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } @@ -100,7 +99,7 @@ export async function PATCH(request: NextRequest): Promise { const existingUser = await prisma.user.findFirst({ where: { username, - NOT: { id: session.user.id }, + NOT: { id: authResult.userId }, }, select: { id: true }, }); @@ -117,7 +116,7 @@ export async function PATCH(request: NextRequest): Promise { } const updatedUser = await prisma.user.update({ - where: { id: session.user.id }, + where: { id: authResult.userId }, data: { ...(name !== undefined && { name }), ...(username !== undefined && { username }),