From 3b275c1048bc0f77b20d282dd65b762550e73e8a Mon Sep 17 00:00:00 2001 From: Gurden Batra Date: Fri, 12 Jun 2026 12:13:32 +0200 Subject: [PATCH] refactor(api): migrate 38 routes to withAuth + return 401 JSON for /api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the withAuth rollout started in the foundation PR. All standard user-authed routes now use withAuth instead of hand-rolling createClient()+getUser()→401, eliminating three stylistic variants of the same boilerplate. Auth-only, mechanical: success/error response payloads are preserved verbatim (no ok()/fail() conversion), as are maxDuration exports, streaming responses, business logic, and status codes. Middleware: unauthenticated /api/* now gets a 401 JSON envelope instead of a 307 redirect to /login (fetch clients can't follow that). Page navigations still redirect; webhook routes and /api/auth stay bypassed. Excluded (still hand-roll auth, intentionally): auth/callback (Supabase login plumbing), integrations/slack/oauth + slack/callback (delicate OAuth redirect flows). Integration webhook routes never used getUser. Minor behavior notes: signals 401 body drops a `success:false` field (401 path only); convergence/snapshots runs auth before its 400 param check (401 now precedes 400 for that edge case). graph GET reads were already public and stay unwrapped (protected by middleware). Updated co-located route tests to pass a Request to wrapped handlers. Verified: tsc 0, lint 0, 560 tests pass. --- src/app/api/capture/process/route.ts | 13 +++------- src/app/api/capture/route.ts | 13 +++------- src/app/api/convergence/snapshot/route.ts | 13 +++------- src/app/api/convergence/snapshots/route.ts | 12 +++------ .../api/distill/__tests__/candidates.test.ts | 6 ++--- src/app/api/distill/candidates/route.ts | 18 ++++--------- src/app/api/edges/[id]/route.ts | 16 +++--------- src/app/api/feedback/route.ts | 9 +++---- src/app/api/graph/edges/route.ts | 9 +++---- src/app/api/graph/nodes/route.ts | 9 +++---- src/app/api/lifecycle/promote/route.ts | 10 +++---- src/app/api/lifecycle/stage/route.ts | 10 +++---- src/app/api/newsletters/route.ts | 18 ++++--------- src/app/api/nodes/[id]/route.ts | 16 +++--------- src/app/api/nodes/search/route.ts | 13 +++------- src/app/api/portfolios/[id]/route.ts | 18 ++++--------- .../[id]/steps/[step]/generate/route.ts | 12 +++------ .../api/portfolios/[id]/steps/[step]/route.ts | 23 +++++----------- .../api/portfolios/__tests__/route.test.ts | 4 +-- src/app/api/portfolios/route.ts | 18 ++++--------- .../api/process/suggest/commitments/route.ts | 13 +++------- src/app/api/process/suggest/hunch/route.ts | 13 +++------- src/app/api/process/suggest/nodes/route.ts | 13 +++------- src/app/api/query/route.ts | 12 +++------ src/app/api/query/save/route.ts | 10 +++---- src/app/api/query/tour/route.ts | 22 ++++------------ src/app/api/reflect/analyse/route.ts | 12 +++------ src/app/api/reflect/session/route.ts | 12 +++------ src/app/api/reflection/run/route.ts | 14 +++------- .../settings/usage/__tests__/route.test.ts | 2 +- src/app/api/settings/usage/route.ts | 10 +++---- src/app/api/setup/goal-suggest/route.ts | 10 +++---- src/app/api/setup/goals/route.ts | 10 +++---- src/app/api/setup/seed/route.ts | 10 +++---- src/app/api/setup/sites/route.ts | 10 +++---- src/app/api/setup/stats/route.ts | 10 +++---- src/app/api/setup/team/route.ts | 10 +++---- src/app/api/setup/workspace/route.ts | 10 +++---- src/app/api/signals/route.ts | 13 +++------- src/app/api/signals/sources/route.ts | 26 +++++-------------- src/app/api/upload/route.ts | 12 +++------ src/middleware.ts | 12 +++++++-- 42 files changed, 148 insertions(+), 378 deletions(-) diff --git a/src/app/api/capture/process/route.ts b/src/app/api/capture/process/route.ts index 8dfe693..994fbd8 100644 --- a/src/app/api/capture/process/route.ts +++ b/src/app/api/capture/process/route.ts @@ -1,5 +1,5 @@ -import { createClient } from '@/lib/supabase/server'; import { createAdminClient } from '@/lib/supabase/admin'; +import { withAuth } from '@/lib/api/withAuth'; import { runExtraction, runMeetingExtraction, runDocumentExtraction, type GoalContext } from '@/lib/agents/extraction'; import type { AttachmentContent } from '@/lib/agents/extraction'; import { getCaptureType } from '@/lib/config/captureTypes'; @@ -9,14 +9,7 @@ import type { MeetingExtraction } from '@/lib/types/nodes'; export const maxDuration = 300; import { NextResponse } from 'next/server'; -export async function POST(request: Request) { - const supabase = await createClient(); - - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ request, user, supabase }) => { const { node_id } = await request.json(); if (!node_id) { @@ -342,4 +335,4 @@ export async function POST(request: Request) { return NextResponse.json({ error: errorMessage }, { status: 500 }); } -} +}); diff --git a/src/app/api/capture/route.ts b/src/app/api/capture/route.ts index e9d7f50..9080bc2 100644 --- a/src/app/api/capture/route.ts +++ b/src/app/api/capture/route.ts @@ -1,15 +1,8 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse, after } from 'next/server'; import { isOwnedStoragePath } from '@/lib/files/storagePath'; -export async function POST(request: Request) { - const supabase = await createClient(); - - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ request, user, supabase }) => { const body = await request.json(); const { title, @@ -104,4 +97,4 @@ export async function POST(request: Request) { }); return NextResponse.json({ data: node }, { status: 201 }); -} +}); diff --git a/src/app/api/convergence/snapshot/route.ts b/src/app/api/convergence/snapshot/route.ts index 6c2e1f0..b94ccfa 100644 --- a/src/app/api/convergence/snapshot/route.ts +++ b/src/app/api/convergence/snapshot/route.ts @@ -1,18 +1,11 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { computeConvergenceScore } from '@/lib/graph/convergence'; import { NextResponse } from 'next/server'; import type { Node } from '@/lib/types/nodes'; import type { Edge } from '@/lib/types/edges'; -export async function POST(request: Request) { +export const POST = withAuth(async ({ request, user, supabase }) => { try { - const supabase = await createClient(); - - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - const body = await request.json(); const { goal_space_id, all } = body as { goal_space_id?: string; all?: boolean }; @@ -90,4 +83,4 @@ export async function POST(request: Request) { const message = error instanceof Error ? error.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } -} +}); diff --git a/src/app/api/convergence/snapshots/route.ts b/src/app/api/convergence/snapshots/route.ts index 7ff90ba..cd7f196 100644 --- a/src/app/api/convergence/snapshots/route.ts +++ b/src/app/api/convergence/snapshots/route.ts @@ -1,8 +1,8 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import type { FactorBreakdown } from '@/lib/graph/convergence'; -export async function GET(request: Request) { +export const GET = withAuth(async ({ request, supabase }) => { try { const { searchParams } = new URL(request.url); const goalSpaceId = searchParams.get('goal_space_id'); @@ -11,12 +11,6 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'goal_space_id required' }, { status: 400 }); } - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - // Query 1: Latest snapshot (for badge + breakdown) const { data: latestRow } = await supabase .from('convergence_snapshots') @@ -55,4 +49,4 @@ export async function GET(request: Request) { const message = error instanceof Error ? error.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } -} +}); diff --git a/src/app/api/distill/__tests__/candidates.test.ts b/src/app/api/distill/__tests__/candidates.test.ts index 76fdb40..619eaf4 100644 --- a/src/app/api/distill/__tests__/candidates.test.ts +++ b/src/app/api/distill/__tests__/candidates.test.ts @@ -67,12 +67,12 @@ describe('GET /api/distill/candidates', () => { it('returns 401 when unauthenticated', async () => { mockGetUser.mockResolvedValue({ data: { user: null }, error: new Error('Unauthorized') }); - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(401); }); it('returns enriched candidates with node details', async () => { - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(200); const body = await res.json() as { data: Array<{ id: string; nodes: Array<{ id: string }> }> }; expect(body.data).toHaveLength(1); @@ -88,7 +88,7 @@ describe('GET /api/distill/candidates', () => { }), }), }); - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(200); const body = await res.json() as { data: unknown[] }; expect(body.data).toHaveLength(0); diff --git a/src/app/api/distill/candidates/route.ts b/src/app/api/distill/candidates/route.ts index b67913e..3bf09d7 100644 --- a/src/app/api/distill/candidates/route.ts +++ b/src/app/api/distill/candidates/route.ts @@ -1,12 +1,8 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import { z } from 'zod'; -export async function GET(): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ user, supabase }) => { const { data: candidates, error } = await supabase .from('distillation_candidates') .select('id, node_ids, merged_title, merged_summary, merged_node_type, rationale, created_at') @@ -31,7 +27,7 @@ export async function GET(): Promise { })); return NextResponse.json({ data: enriched }); -} +}); const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; @@ -40,11 +36,7 @@ const actionSchema = z.object({ action: z.enum(['accept', 'reject']), }); -export async function PATCH(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const PATCH = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } @@ -121,4 +113,4 @@ export async function PATCH(request: Request): Promise { } return NextResponse.json({ data: { action: 'accepted', node_id: newNode.id } }); -} +}); diff --git a/src/app/api/edges/[id]/route.ts b/src/app/api/edges/[id]/route.ts index f4ffd3b..d1ca0ec 100644 --- a/src/app/api/edges/[id]/route.ts +++ b/src/app/api/edges/[id]/route.ts @@ -1,17 +1,7 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; -export async function DELETE( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const DELETE = withAuth<{ id: string }>(async ({ params, supabase }) => { const { id } = await params; const { error } = await supabase @@ -24,4 +14,4 @@ export async function DELETE( } return new NextResponse(null, { status: 204 }); -} +}); diff --git a/src/app/api/feedback/route.ts b/src/app/api/feedback/route.ts index fb90130..b62afd2 100644 --- a/src/app/api/feedback/route.ts +++ b/src/app/api/feedback/route.ts @@ -1,4 +1,5 @@ import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse, after } from 'next/server'; import { z } from 'zod'; import { applyCorrection } from '@/lib/correction/agent'; @@ -54,11 +55,7 @@ function extractGeneratedText(sourceType: SourceType, record: Record { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); @@ -110,4 +107,4 @@ export async function POST(request: Request): Promise { { id: feedbackId, created_at: (feedback as Record)['created_at'] }, { status: 201 } ); -} +}); diff --git a/src/app/api/graph/edges/route.ts b/src/app/api/graph/edges/route.ts index 87c5417..08822bf 100644 --- a/src/app/api/graph/edges/route.ts +++ b/src/app/api/graph/edges/route.ts @@ -1,4 +1,5 @@ import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; export async function GET() { @@ -15,11 +16,7 @@ export async function GET() { return NextResponse.json({ data }); } -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { const body = await request.json(); const { data, error } = await supabase .from('edges') @@ -37,4 +34,4 @@ export async function POST(request: Request) { }); return NextResponse.json({ data }, { status: 201 }); -} +}); diff --git a/src/app/api/graph/nodes/route.ts b/src/app/api/graph/nodes/route.ts index 8a29f86..70dff86 100644 --- a/src/app/api/graph/nodes/route.ts +++ b/src/app/api/graph/nodes/route.ts @@ -1,4 +1,5 @@ import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { computeConvergenceScore, shouldTriggerSnapshot } from '@/lib/graph/convergence'; import { NextResponse } from 'next/server'; import { nodeCreateSchema } from '@/lib/api/nodeInput'; @@ -101,11 +102,7 @@ export async function GET(request: Request) { return NextResponse.json({ data }); } -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { const parsed = nodeCreateSchema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json( @@ -126,4 +123,4 @@ export async function POST(request: Request) { void checkAndTriggerSnapshots(supabase, user.id); return NextResponse.json({ data }, { status: 201 }); -} +}); diff --git a/src/app/api/lifecycle/promote/route.ts b/src/app/api/lifecycle/promote/route.ts index 1e5eb94..f4b9e9a 100644 --- a/src/app/api/lifecycle/promote/route.ts +++ b/src/app/api/lifecycle/promote/route.ts @@ -1,12 +1,8 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import { checkHunchPromotion } from '@/lib/lifecycle/autoPromote'; -export async function POST(): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ user, supabase }) => { const { data: hunches, error } = await supabase .from('nodes') .select('id, lifecycle_stage') @@ -50,4 +46,4 @@ export async function POST(): Promise { } return NextResponse.json({ data: { promoted: promoted.length, ids: promoted, failed: failed.length } }); -} +}); diff --git a/src/app/api/lifecycle/stage/route.ts b/src/app/api/lifecycle/stage/route.ts index da290dc..6cce793 100644 --- a/src/app/api/lifecycle/stage/route.ts +++ b/src/app/api/lifecycle/stage/route.ts @@ -1,5 +1,5 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import { z } from 'zod'; const VALID_STAGES = ['hypothesis', 'uncertainty', 'navigation', 'coherence', 'holding', 'archived'] as const; @@ -10,11 +10,7 @@ const schema = z.object({ reason: z.string().max(500).optional(), }); -export async function PATCH(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const PATCH = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); @@ -47,4 +43,4 @@ export async function PATCH(request: Request): Promise { }); return NextResponse.json({ data: { node_id, stage } }); -} +}); diff --git a/src/app/api/newsletters/route.ts b/src/app/api/newsletters/route.ts index 83d8712..bad880b 100644 --- a/src/app/api/newsletters/route.ts +++ b/src/app/api/newsletters/route.ts @@ -1,5 +1,5 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import { z } from 'zod'; import { callLLM } from '@/lib/llm'; import { selectMissionPathwaysNodes, selectCloseContactsNodes } from '@/lib/newsletter/select'; @@ -13,11 +13,7 @@ import { const typeSchema = z.enum(['mission_pathways', 'close_contacts']); const postSchema = z.object({ type: typeSchema }); -export async function GET(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ user, supabase, request }) => { const { searchParams } = new URL(request.url); const typeResult = typeSchema.safeParse(searchParams.get('type')); if (!typeResult.success) return NextResponse.json({ error: 'Invalid type' }, { status: 400 }); @@ -33,13 +29,9 @@ export async function GET(request: Request): Promise { if (error) return NextResponse.json({ error: 'Failed to load newsletters' }, { status: 500 }); return NextResponse.json({ data: data ?? [] }); -} - -export async function POST(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const POST = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); @@ -95,4 +87,4 @@ export async function POST(request: Request): Promise { } return NextResponse.json({ data: newsletter }, { status: 201 }); -} +}); diff --git a/src/app/api/nodes/[id]/route.ts b/src/app/api/nodes/[id]/route.ts index 4fa5f10..87ddd9b 100644 --- a/src/app/api/nodes/[id]/route.ts +++ b/src/app/api/nodes/[id]/route.ts @@ -1,5 +1,5 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import type { NodeStatus, ConfidenceBasis } from '@/lib/types/nodes'; const ALLOWED_STATUSES: readonly NodeStatus[] = ['promoted', 'archived', 'falsified', 'suspended']; @@ -12,17 +12,7 @@ const ALLOWED_CONFIDENCE_BASES: readonly ConfidenceBasis[] = [ 'strong_evidence', ]; -export async function PATCH( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const PATCH = withAuth<{ id: string }>(async ({ request, supabase, params }) => { const { id } = await params; let body: Record; @@ -110,4 +100,4 @@ export async function PATCH( } return NextResponse.json({ data }); -} +}); diff --git a/src/app/api/nodes/search/route.ts b/src/app/api/nodes/search/route.ts index 2d320f4..24f80e3 100644 --- a/src/app/api/nodes/search/route.ts +++ b/src/app/api/nodes/search/route.ts @@ -1,14 +1,7 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; -export async function GET(request: Request) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const GET = withAuth(async ({ supabase, request }) => { const { searchParams } = new URL(request.url); const q = searchParams.get('q'); const type = searchParams.get('type'); @@ -35,4 +28,4 @@ export async function GET(request: Request) { } return NextResponse.json({ data: data ?? [] }); -} +}); diff --git a/src/app/api/portfolios/[id]/route.ts b/src/app/api/portfolios/[id]/route.ts index 185a4a4..32dbda2 100644 --- a/src/app/api/portfolios/[id]/route.ts +++ b/src/app/api/portfolios/[id]/route.ts @@ -1,6 +1,6 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { z } from 'zod'; +import { withAuth } from '@/lib/api/withAuth'; const patchSchema = z.object({ title: z.string().min(1).max(200).optional(), @@ -9,11 +9,7 @@ const patchSchema = z.object({ status: z.enum(['in_progress', 'complete', 'paused', 'archived']).optional(), }); -export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth<{ id: string }>(async ({ user, supabase, params }) => { const { id } = await params; const { data: portfolio, error } = await supabase @@ -32,13 +28,9 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str .order('step_number', { ascending: true }); return NextResponse.json({ data: { ...portfolio, steps: steps ?? [] } }); -} - -export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const PATCH = withAuth<{ id: string }>(async ({ user, supabase, request, params }) => { const { id } = await params; let body: unknown; @@ -60,4 +52,4 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id if (error || !data) return NextResponse.json({ error: 'Not found or update failed' }, { status: 404 }); return NextResponse.json({ data }); -} +}); diff --git a/src/app/api/portfolios/[id]/steps/[step]/generate/route.ts b/src/app/api/portfolios/[id]/steps/[step]/generate/route.ts index 348e84b..6a4f19e 100644 --- a/src/app/api/portfolios/[id]/steps/[step]/generate/route.ts +++ b/src/app/api/portfolios/[id]/steps/[step]/generate/route.ts @@ -1,15 +1,9 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { generateStepContent } from '@/lib/portfolio/generate'; import { STEP_AGENTS } from '@/lib/portfolio/agents'; +import { withAuth } from '@/lib/api/withAuth'; -type Params = { id: string; step: string }; - -export async function POST(_req: Request, { params }: { params: Promise }): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth<{ id: string; step: string }>(async ({ user, supabase, params }) => { const { id, step } = await params; const stepNumber = parseInt(step, 10); if (isNaN(stepNumber) || stepNumber < 1 || stepNumber > 13) { @@ -45,4 +39,4 @@ export async function POST(_req: Request, { params }: { params: Promise const message = err instanceof Error ? err.message : 'Generation failed'; return NextResponse.json({ error: message }, { status: 500 }); } -} +}); diff --git a/src/app/api/portfolios/[id]/steps/[step]/route.ts b/src/app/api/portfolios/[id]/steps/[step]/route.ts index cb0d9ce..a1073e7 100644 --- a/src/app/api/portfolios/[id]/steps/[step]/route.ts +++ b/src/app/api/portfolios/[id]/steps/[step]/route.ts @@ -1,6 +1,7 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { z } from 'zod'; +import type { SupabaseClient } from '@supabase/supabase-js'; +import { withAuth } from '@/lib/api/withAuth'; const patchSchema = z.object({ content: z.record(z.string(), z.unknown()).optional(), @@ -8,9 +9,7 @@ const patchSchema = z.object({ status: z.enum(['not_started', 'ai_drafted', 'in_review', 'complete']).optional(), }); -type Params = { id: string; step: string }; - -async function getPortfolioForUser(supabase: Awaited>, portfolioId: string, userId: string) { +async function getPortfolioForUser(supabase: SupabaseClient, portfolioId: string, userId: string) { const { data } = await supabase .from('portfolios') .select('id, current_step') @@ -20,11 +19,7 @@ async function getPortfolioForUser(supabase: Awaited }): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth<{ id: string; step: string }>(async ({ user, supabase, params }) => { const { id, step } = await params; const stepNumber = parseInt(step, 10); if (isNaN(stepNumber) || stepNumber < 1 || stepNumber > 13) { @@ -44,13 +39,9 @@ export async function GET(_req: Request, { params }: { params: Promise } if (error || !stepData) return NextResponse.json({ error: 'Step not found' }, { status: 404 }); return NextResponse.json({ data: stepData }); -} - -export async function PATCH(request: Request, { params }: { params: Promise }): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const PATCH = withAuth<{ id: string; step: string }>(async ({ user, supabase, request, params }) => { const { id, step } = await params; const stepNumber = parseInt(step, 10); if (isNaN(stepNumber) || stepNumber < 1 || stepNumber > 13) { @@ -91,4 +82,4 @@ export async function PATCH(request: Request, { params }: { params: Promise { it('returns 401 when not authenticated', async () => { mockSupabase.auth.getUser.mockResolvedValueOnce({ data: { user: null }, error: new Error('no user') }); const { GET } = await import('../route'); - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(401); }); @@ -30,7 +30,7 @@ describe('GET /api/portfolios', () => { }), }); const { GET } = await import('../route'); - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(200); const body = await res.json() as { data: unknown[] }; expect(body.data).toHaveLength(1); diff --git a/src/app/api/portfolios/route.ts b/src/app/api/portfolios/route.ts index 43aac0a..c631112 100644 --- a/src/app/api/portfolios/route.ts +++ b/src/app/api/portfolios/route.ts @@ -1,7 +1,7 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { z } from 'zod'; import { STEP_NAMES } from '@/lib/portfolio/agents'; +import { withAuth } from '@/lib/api/withAuth'; const createSchema = z.object({ title: z.string().min(1).max(200), @@ -9,11 +9,7 @@ const createSchema = z.object({ description: z.string().max(2000).optional(), }); -export async function GET(): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ user, supabase }) => { const { data, error } = await supabase .from('portfolios') .select('id, title, subtitle, status, current_step, created_at, updated_at') @@ -23,13 +19,9 @@ export async function GET(): Promise { if (error) return NextResponse.json({ error: 'Failed to load portfolios' }, { status: 500 }); return NextResponse.json({ data: data ?? [] }); -} - -export async function POST(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const POST = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); @@ -63,4 +55,4 @@ export async function POST(request: Request): Promise { } return NextResponse.json({ data: { id: portfolio.id } }, { status: 201 }); -} +}); diff --git a/src/app/api/process/suggest/commitments/route.ts b/src/app/api/process/suggest/commitments/route.ts index e7d4f10..a6a8874 100644 --- a/src/app/api/process/suggest/commitments/route.ts +++ b/src/app/api/process/suggest/commitments/route.ts @@ -1,17 +1,10 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { suggestCommitmentAssessments } from '@/lib/agents/process'; import type { Node } from '@/lib/types/nodes'; import type { CommitmentWithAssumptions } from '@/lib/agents/process'; +import { withAuth } from '@/lib/api/withAuth'; -export async function POST(request: Request) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ supabase, request }) => { let body: Record; try { body = await request.json(); @@ -61,4 +54,4 @@ export async function POST(request: Request) { { status: 500 } ); } -} +}); diff --git a/src/app/api/process/suggest/hunch/route.ts b/src/app/api/process/suggest/hunch/route.ts index 9883253..90c91b4 100644 --- a/src/app/api/process/suggest/hunch/route.ts +++ b/src/app/api/process/suggest/hunch/route.ts @@ -1,17 +1,10 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { suggestHunch } from '@/lib/agents/process'; import type { Node } from '@/lib/types/nodes'; import type { GoalContext } from '@/lib/agents/extraction'; +import { withAuth } from '@/lib/api/withAuth'; -export async function POST(request: Request) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ supabase, request }) => { let body: Record; try { body = await request.json(); @@ -58,4 +51,4 @@ export async function POST(request: Request) { { status: 500 } ); } -} +}); diff --git a/src/app/api/process/suggest/nodes/route.ts b/src/app/api/process/suggest/nodes/route.ts index e063cc9..15a359c 100644 --- a/src/app/api/process/suggest/nodes/route.ts +++ b/src/app/api/process/suggest/nodes/route.ts @@ -1,16 +1,9 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; import { suggestAffectedNodes } from '@/lib/agents/process'; import type { Node } from '@/lib/types/nodes'; +import { withAuth } from '@/lib/api/withAuth'; -export async function POST(request: Request) { - const supabase = await createClient(); - - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ supabase, request }) => { let body: Record; try { body = await request.json(); @@ -62,4 +55,4 @@ export async function POST(request: Request) { { status: 500 } ); } -} +}); diff --git a/src/app/api/query/route.ts b/src/app/api/query/route.ts index 77a465c..97fd7e0 100644 --- a/src/app/api/query/route.ts +++ b/src/app/api/query/route.ts @@ -2,7 +2,7 @@ import Anthropic from '@anthropic-ai/sdk'; export const maxDuration = 300; import { z } from 'zod'; -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { buildQuerySystemPrompt, serializeNodesForQuery } from '@/lib/agents/query'; import type { QuerySerializedNode } from '@/lib/agents/query'; @@ -25,13 +25,7 @@ interface EdgeRow { target_id: string; } -export async function POST(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ user, supabase, request }) => { let body: z.infer; try { const raw = await request.json(); @@ -143,4 +137,4 @@ export async function POST(request: Request): Promise { 'X-Query-Session-Id': sessionId, }, }); -} +}); diff --git a/src/app/api/query/save/route.ts b/src/app/api/query/save/route.ts index 72331b0..56cd400 100644 --- a/src/app/api/query/save/route.ts +++ b/src/app/api/query/save/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -9,11 +9,7 @@ const SaveSchema = z.object({ context_node_ids: z.array(z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)).max(50).default([]), }); -export async function POST(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } @@ -63,4 +59,4 @@ export async function POST(request: Request): Promise { } return NextResponse.json({ data: { node, edges_created: edgesCreated } }, { status: 201 }); -} +}); diff --git a/src/app/api/query/tour/route.ts b/src/app/api/query/tour/route.ts index 70f95ad..1d5e40c 100644 --- a/src/app/api/query/tour/route.ts +++ b/src/app/api/query/tour/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { createAdminClient } from '@/lib/supabase/admin'; import { callLLM } from '@/lib/llm'; @@ -43,13 +43,7 @@ function normalizeTour(v: unknown): TourResponse | null { return chapters.length > 0 ? { chapters } : null; } -export async function GET(_request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const GET = withAuth(async ({ user, supabase }) => { const { data: profile } = await supabase .from('profiles') .select('guided_tour, guided_tour_generated_at') @@ -61,15 +55,9 @@ export async function GET(_request: Request): Promise { } return Response.json({ tour: profile.guided_tour, generatedAt: profile.guided_tour_generated_at }); -} - -export async function POST(_request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } +}); +export const POST = withAuth(async ({ user, supabase }) => { const { data: nodesData, error: dbError } = await supabase .from('nodes') .select('id, node_type, title, description, status') @@ -124,4 +112,4 @@ export async function POST(_request: Request): Promise { console.error('[tour] JSON parse failed:', err, '| raw (200):', llmText.slice(0, 200)); return Response.json({ error: 'Failed to parse tour response' }, { status: 500 }); } -} +}); diff --git a/src/app/api/reflect/analyse/route.ts b/src/app/api/reflect/analyse/route.ts index c7a6748..4e3ba0c 100644 --- a/src/app/api/reflect/analyse/route.ts +++ b/src/app/api/reflect/analyse/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; export const maxDuration = 300; @@ -45,13 +45,7 @@ function bfsConnectedIds( return visited; } -export async function POST(request: Request) { - const supabase = await createClient(); - - // Fix 1: Destructure authError and guard on both - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ supabase, request }) => { let body: { type?: string; value?: string; label?: string }; try { body = await request.json() as { type?: string; value?: string; label?: string }; @@ -139,4 +133,4 @@ export async function POST(request: Request) { } catch { return NextResponse.json({ error: 'LLM call failed' }, { status: 500 }); } -} +}); diff --git a/src/app/api/reflect/session/route.ts b/src/app/api/reflect/session/route.ts index 8364324..2d90990 100644 --- a/src/app/api/reflect/session/route.ts +++ b/src/app/api/reflect/session/route.ts @@ -1,15 +1,9 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import type { ReflectionSessionPayload } from '@/app/reflect/types'; -export async function POST(request: Request): Promise { +export const POST = withAuth(async ({ user, supabase, request }) => { try { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - const body: ReflectionSessionPayload = await request.json(); // Validate required fields @@ -40,4 +34,4 @@ export async function POST(request: Request): Promise { const message = error instanceof Error ? error.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } -} +}); diff --git a/src/app/api/reflection/run/route.ts b/src/app/api/reflection/run/route.ts index 6196130..29ffad7 100644 --- a/src/app/api/reflection/run/route.ts +++ b/src/app/api/reflection/run/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; export const maxDuration = 300; import { @@ -8,15 +8,7 @@ import { type ReflectionContext, } from '@/lib/agents/reflection'; -export async function POST(_request: Request): Promise { - const supabase = await createClient(); - - // Auth check - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ user, supabase }) => { // Rate limit check: has reflection run in last 24 hours? const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const { count } = await supabase @@ -143,4 +135,4 @@ export async function POST(_request: Request): Promise { 'Cache-Control': 'no-cache', }, }); -} +}); diff --git a/src/app/api/settings/usage/__tests__/route.test.ts b/src/app/api/settings/usage/__tests__/route.test.ts index d79a8a4..63e6681 100644 --- a/src/app/api/settings/usage/__tests__/route.test.ts +++ b/src/app/api/settings/usage/__tests__/route.test.ts @@ -43,7 +43,7 @@ describe('GET /api/settings/usage', () => { it('returns 200 with usage data', async () => { const { GET } = await import('../route'); - const res = await GET(); + const res = await GET(new Request('http://t', { method: 'GET' })); expect(res.status).toBe(200); const body = await res.json() as { data: { totalCalls: number; cachedCalls: number } }; expect(body.data.totalCalls).toBe(2); diff --git a/src/app/api/settings/usage/route.ts b/src/app/api/settings/usage/route.ts index ce5ee51..59ed493 100644 --- a/src/app/api/settings/usage/route.ts +++ b/src/app/api/settings/usage/route.ts @@ -1,5 +1,5 @@ -import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/withAuth'; import { estimateCostMicroCents } from '@/lib/llm/usage'; interface UsageRow { @@ -10,11 +10,7 @@ interface UsageRow { cached: boolean; } -export async function GET(): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ supabase }) => { const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(); const { data, error } = await supabase @@ -54,4 +50,4 @@ export async function GET(): Promise { byAgent, }, }); -} +}); diff --git a/src/app/api/setup/goal-suggest/route.ts b/src/app/api/setup/goal-suggest/route.ts index 56bae7a..e9b81f1 100644 --- a/src/app/api/setup/goal-suggest/route.ts +++ b/src/app/api/setup/goal-suggest/route.ts @@ -1,15 +1,11 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; import { suggestGoal } from '@/lib/agents/setup'; const schema = z.object({ input: z.string().min(1) }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request }) => { let body: unknown; try { body = await request.json(); @@ -25,4 +21,4 @@ export async function POST(request: Request) { } catch { return NextResponse.json({ error: 'Failed to generate suggestion' }, { status: 500 }); } -} +}); diff --git a/src/app/api/setup/goals/route.ts b/src/app/api/setup/goals/route.ts index 06f3d47..2383e58 100644 --- a/src/app/api/setup/goals/route.ts +++ b/src/app/api/setup/goals/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -6,11 +6,7 @@ const schema = z.object({ goals: z.array(z.object({ title: z.string().min(1), description: z.string().optional() })).min(1), }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); @@ -34,4 +30,4 @@ export async function POST(request: Request) { const { data, error } = await supabase.from('nodes').insert(nodes).select(); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ data }, { status: 201 }); -} +}); diff --git a/src/app/api/setup/seed/route.ts b/src/app/api/setup/seed/route.ts index 3a4c8c7..0b47151 100644 --- a/src/app/api/setup/seed/route.ts +++ b/src/app/api/setup/seed/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; import { processSeedChat } from '@/lib/agents/setup'; @@ -24,11 +24,7 @@ const writeSchema = z.object({ goals: z.array(z.object({ title: z.string() })), }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); @@ -104,4 +100,4 @@ export async function POST(request: Request) { } return NextResponse.json({ error: 'Invalid mode. Expected chat or write.' }, { status: 400 }); -} +}); diff --git a/src/app/api/setup/sites/route.ts b/src/app/api/setup/sites/route.ts index 1824234..a27b57d 100644 --- a/src/app/api/setup/sites/route.ts +++ b/src/app/api/setup/sites/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -11,11 +11,7 @@ const schema = z.object({ })), }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); @@ -82,4 +78,4 @@ export async function POST(request: Request) { } return NextResponse.json({ data: { created: createdCount } }, { status: 201 }); -} +}); diff --git a/src/app/api/setup/stats/route.ts b/src/app/api/setup/stats/route.ts index 5dc5891..956b156 100644 --- a/src/app/api/setup/stats/route.ts +++ b/src/app/api/setup/stats/route.ts @@ -1,11 +1,7 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; -export async function GET() { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ supabase }) => { const nodeTypeGroups = [ 'goal_space', 'site', @@ -33,4 +29,4 @@ export async function GET() { .select('*', { count: 'exact', head: true }); return NextResponse.json({ data: { nodes: counts, edges: edgeCount ?? 0 } }, { status: 200 }); -} +}); diff --git a/src/app/api/setup/team/route.ts b/src/app/api/setup/team/route.ts index 835371d..938cc97 100644 --- a/src/app/api/setup/team/route.ts +++ b/src/app/api/setup/team/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -6,11 +6,7 @@ const schema = z.object({ members: z.array(z.object({ name: z.string().min(1), role: z.string().optional() })).min(1), }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); @@ -34,4 +30,4 @@ export async function POST(request: Request) { const { data, error } = await supabase.from('nodes').insert(nodes).select(); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ data }, { status: 201 }); -} +}); diff --git a/src/app/api/setup/workspace/route.ts b/src/app/api/setup/workspace/route.ts index 77cbe32..e1d51e2 100644 --- a/src/app/api/setup/workspace/route.ts +++ b/src/app/api/setup/workspace/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -7,11 +7,7 @@ const schema = z.object({ description: z.string().optional(), }); -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const POST = withAuth(async ({ request, user, supabase }) => { let body: unknown; try { body = await request.json(); @@ -29,4 +25,4 @@ export async function POST(request: Request) { if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ data }, { status: 201 }); -} +}); diff --git a/src/app/api/signals/route.ts b/src/app/api/signals/route.ts index 9db2466..01d248e 100644 --- a/src/app/api/signals/route.ts +++ b/src/app/api/signals/route.ts @@ -1,15 +1,8 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { propagateSignal } from '@/lib/signals/propagate'; -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - - if (authError || !user) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ request }) => { let body: unknown; try { body = await request.json(); @@ -32,4 +25,4 @@ export async function POST(request: Request) { { status: 500 } ); } -} +}); diff --git a/src/app/api/signals/sources/route.ts b/src/app/api/signals/sources/route.ts index 629bbb3..a04b9eb 100644 --- a/src/app/api/signals/sources/route.ts +++ b/src/app/api/signals/sources/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { NextResponse } from 'next/server'; import { z } from 'zod'; @@ -8,11 +8,7 @@ const createSchema = z.object({ config: z.record(z.string(), z.unknown()), }); -export async function GET(): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - +export const GET = withAuth(async ({ user, supabase }) => { const { data, error } = await supabase .from('auto_signal_sources') .select('id, source_type, topic_node_id, config, enabled, last_run_at, created_at') @@ -21,13 +17,9 @@ export async function GET(): Promise { if (error) return NextResponse.json({ error: 'Failed to load sources' }, { status: 500 }); return NextResponse.json({ data }); -} - -export async function POST(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const POST = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } @@ -41,13 +33,9 @@ export async function POST(request: Request): Promise { if (error) return NextResponse.json({ error: 'Failed to create source' }, { status: 500 }); return NextResponse.json({ data }, { status: 201 }); -} - -export async function PATCH(request: Request): Promise { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +}); +export const PATCH = withAuth(async ({ user, supabase, request }) => { let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } @@ -64,4 +52,4 @@ export async function PATCH(request: Request): Promise { if (error) return NextResponse.json({ error: 'Failed to update source' }, { status: 500 }); return NextResponse.json({ data }); -} +}); diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 112742f..119732e 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/supabase/server'; +import { withAuth } from '@/lib/api/withAuth'; import { createAdminClient } from '@/lib/supabase/admin'; import { NextResponse } from 'next/server'; @@ -16,13 +16,7 @@ const EXT_MAP: Record = { 'text/plain': 'txt', }; -export async function POST(request: Request) { - const supabase = await createClient(); - const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - +export const POST = withAuth(async ({ request, user }) => { let formData: FormData; try { formData = await request.formData(); @@ -70,4 +64,4 @@ export async function POST(request: Request) { mime_type: file.type, size: file.size, }); -} +}); diff --git a/src/middleware.ts b/src/middleware.ts index 0d443a4..7e62f98 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -34,9 +34,17 @@ export async function middleware(request: NextRequest) { '/api/integrations/notion/webhook', '/api/integrations/folk/sync', ]; - const isWebhook = WEBHOOK_PATHS.some((p) => request.nextUrl.pathname.startsWith(p)); + const { pathname } = request.nextUrl; + const isWebhook = WEBHOOK_PATHS.some((p) => pathname.startsWith(p)); + const isPublic = pathname.startsWith('/login') || pathname.startsWith('/api/auth') || isWebhook; - if (!user && !request.nextUrl.pathname.startsWith('/login') && !request.nextUrl.pathname.startsWith('/api/auth') && !isWebhook) { + if (!user && !isPublic) { + // API routes get a 401 JSON envelope (matching withAuth) — fetch clients + // can't follow a login redirect and would otherwise receive an opaque 307 + // to an HTML page. Page navigations still redirect to /login. + if (pathname.startsWith('/api/')) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } const url = request.nextUrl.clone(); url.pathname = '/login'; return NextResponse.redirect(url);