diff --git a/src/app/api/workspaces/[id]/events/route.ts b/src/app/api/workspaces/[id]/events/route.ts index 3f148ab9..ef481b1a 100644 --- a/src/app/api/workspaces/[id]/events/route.ts +++ b/src/app/api/workspaces/[id]/events/route.ts @@ -1,56 +1,36 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import type { WorkspaceEvent, EventResponse } from "@/lib/workspace/events"; import { checkAndCreateSnapshot } from "@/lib/workspace/snapshot-manager"; -import { db, workspaces, workspaceEvents, workspaceSnapshots } from "@/lib/db/client"; -import { eq, gt, desc, asc, sql, and } from "drizzle-orm"; +import { db, workspaceEvents } from "@/lib/db/client"; +import { eq, gt, asc, sql, and } from "drizzle-orm"; +import { requireAuth, verifyWorkspaceOwnership, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces/[id]/events * Fetch all events for a workspace (owner only) */ -export async function GET( +async function handleGET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const startTime = Date.now(); const timings: Record = {}; - let id: string | undefined; - - try { - const paramsResolved = await params; - id = paramsResolved.id; - - const authStart = Date.now(); - const session = await auth.api.getSession({ - headers: await headers(), - }); - timings.auth = Date.now() - authStart; - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Check if user is workspace owner - const workspaceCheckStart = Date.now(); - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - timings.workspaceCheck = Date.now() - workspaceCheckStart; - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const paramsResolved = await paramsPromise; + const id = paramsResolved.id; + + const authStart = Date.now(); + const userId = await authPromise; + timings.auth = Date.now() - authStart; + + // Check if user is workspace owner + const workspaceCheckStart = Date.now(); + await verifyWorkspaceOwnership(id, userId); + timings.workspaceCheck = Date.now() - workspaceCheckStart; // Get only the latest snapshot (not all snapshots - loaded on demand for version history) // Use optimized function that bypasses RLS (access already verified above) @@ -207,71 +187,50 @@ export async function GET( const totalTime = Date.now() - startTime; timings.total = totalTime; - return NextResponse.json(response); - } catch (error) { - const totalTime = Date.now() - startTime; - console.error(`[GET /api/workspaces/${id || '[unknown]'}/events] Error after ${totalTime}ms:`, error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json(response); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/[id]/events"); + /** * POST /api/workspaces/[id]/events * Append new event(s) to workspace event log (owner only) */ -export async function POST( +async function handlePOST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const startTime = Date.now(); const timings: Record = {}; - let id: string | undefined; - - try { - const paramsResolved = await params; - id = paramsResolved.id; - - const authStart = Date.now(); - const session = await auth.api.getSession({ - headers: await headers(), - }); - timings.auth = Date.now() - authStart; - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - const bodyStart = Date.now(); - const body = await request.json(); - timings.bodyParse = Date.now() - bodyStart; - const { event, baseVersion } = body; - - if (!event || baseVersion === undefined || isNaN(baseVersion)) { - return NextResponse.json( - { error: "Event and valid baseVersion are required" }, - { status: 400 } - ); - } - - // Check if user is workspace owner - const workspaceCheckStart = Date.now(); - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - timings.workspaceCheck = Date.now() - workspaceCheckStart; - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } + + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + const bodyPromise = request.json(); + + const paramsResolved = await paramsPromise; + const id = paramsResolved.id; + + const authStart = Date.now(); + const userId = await authPromise; + timings.auth = Date.now() - authStart; + + const bodyStart = Date.now(); + const body = await bodyPromise; + timings.bodyParse = Date.now() - bodyStart; + const { event, baseVersion } = body; + + if (!event || baseVersion === undefined || isNaN(baseVersion)) { + return NextResponse.json( + { error: "Event and valid baseVersion are required" }, + { status: 400 } + ); + } - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + // Check if user is workspace owner + const workspaceCheckStart = Date.now(); + await verifyWorkspaceOwnership(id, userId); + timings.workspaceCheck = Date.now() - workspaceCheckStart; // Use the append function to handle versioning and conflicts const appendStart = Date.now(); @@ -353,15 +312,12 @@ export async function POST( const totalTime = Date.now() - startTime; timings.total = totalTime; - return NextResponse.json({ - success: true, - version: appendResult.version, - conflict: false, - }); - } catch (error) { - const totalTime = Date.now() - startTime; - console.error(`[POST /api/workspaces/${id || '[unknown]'}/events] Error after ${totalTime}ms:`, error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ + success: true, + version: appendResult.version, + conflict: false, + }); } +export const POST = withErrorHandling(handlePOST, "POST /api/workspaces/[id]/events"); + diff --git a/src/app/api/workspaces/[id]/route.ts b/src/app/api/workspaces/[id]/route.ts index 9a3f475f..0d134a7f 100644 --- a/src/app/api/workspaces/[id]/route.ts +++ b/src/app/api/workspaces/[id]/route.ts @@ -1,108 +1,67 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import { db, workspaces } from "@/lib/db/client"; import { eq } from "drizzle-orm"; import { loadWorkspaceState } from "@/lib/workspace/state-loader"; +import { requireAuth, verifyWorkspaceOwnership, verifyWorkspaceOwnershipWithData, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces/[id] * Get a specific workspace with its state * Note: Only owners can access (sharing is fork-based - users import copies) */ -export async function GET( +async function handleGET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - // Allow anonymous users - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Get workspace - const workspace = await db - .select() - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + // Get workspace and verify ownership + const workspace = await verifyWorkspaceOwnershipWithData(id, userId); // Get workspace state by replaying events const state = await loadWorkspaceState(id); - // Ensure state has workspace metadata if empty - if (!state.globalTitle && !state.globalDescription) { - state.globalTitle = workspace[0].name || ""; - state.globalDescription = workspace[0].description || ""; - } - - return NextResponse.json({ - workspace: { - ...workspace[0], - state, - }, - }); - } catch (error) { - console.error("Error in GET /api/workspaces/[id]:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + // Ensure state has workspace metadata if empty + if (!state.globalTitle && !state.globalDescription) { + state.globalTitle = workspace.name || ""; + state.globalDescription = workspace.description || ""; } + + return NextResponse.json({ + workspace: { + ...workspace, + state, + }, + }); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/[id]"); + /** * PATCH /api/workspaces/[id] * Update workspace metadata (owner only) */ -export async function PATCH( +async function handlePATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - // Allow anonymous users - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - const body = await request.json(); - const { name, description, is_public, icon, color } = body; - - // Check ownership - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + const bodyPromise = request.json(); + + const { id } = await paramsPromise; + const userId = await authPromise; + const body = await bodyPromise; + const { name, description, is_public, icon, color } = body; + + // Check ownership + await verifyWorkspaceOwnership(id, userId); // Update workspace const updateData: { @@ -124,58 +83,36 @@ export async function PATCH( .where(eq(workspaces.id, id)) .returning(); - return NextResponse.json({ workspace: updatedWorkspace }); - } catch (error) { - console.error("Error in PATCH /api/workspaces/[id]:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ workspace: updatedWorkspace }); } +export const PATCH = withErrorHandling(handlePATCH, "PATCH /api/workspaces/[id]"); + /** * DELETE /api/workspaces/[id] * Delete a workspace (owner only) */ -export async function DELETE( +async function handleDELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - // Allow anonymous users - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Check ownership - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Only owners can delete workspaces" }, { status: 403 }); - } + // Check ownership + await verifyWorkspaceOwnership(id, userId); // Delete workspace (cascade will delete events and snapshots) await db .delete(workspaces) .where(eq(workspaces.id, id)); - return NextResponse.json({ success: true }); - } catch (error) { - console.error("Error in DELETE /api/workspaces/[id]:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ success: true }); } +export const DELETE = withErrorHandling(handleDELETE, "DELETE /api/workspaces/[id]"); + diff --git a/src/app/api/workspaces/[id]/snapshot/route.ts b/src/app/api/workspaces/[id]/snapshot/route.ts index 51dd314b..cd746fda 100644 --- a/src/app/api/workspaces/[id]/snapshot/route.ts +++ b/src/app/api/workspaces/[id]/snapshot/route.ts @@ -1,6 +1,4 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import { checkNeedsSnapshot, createSnapshot, @@ -8,42 +6,25 @@ import { } from "@/lib/workspace/snapshot-manager"; import { db, workspaces } from "@/lib/db/client"; import { eq, sql } from "drizzle-orm"; +import { requireAuth, verifyWorkspaceOwnership, verifyWorkspaceOwnershipWithData, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces/[id]/snapshot * Check snapshot status for a workspace (owner only) */ -export async function GET( +async function handleGET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - // Check if user is workspace owner - const workspace = await db - .select({ userId: workspaces.userId, name: workspaces.name }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + // Check if user is workspace owner + const workspace = await verifyWorkspaceOwnershipWithData(id, userId); // Check snapshot status using utility functions const snapshotStatus = await checkNeedsSnapshot(id); @@ -53,57 +34,35 @@ export async function GET( SELECT * FROM get_latest_snapshot(${id}::uuid) `); - return NextResponse.json({ - workspace: { - id, - title: workspace[0].name, - }, - snapshot: latestSnapshot[0] || null, - status: snapshotStatus, - }); - } catch (error) { - console.error("Error in GET /api/workspaces/[id]/snapshot:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ + workspace: { + id, + title: workspace.name, + }, + snapshot: latestSnapshot[0] || null, + status: snapshotStatus, + }); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/[id]/snapshot"); + /** * POST /api/workspaces/[id]/snapshot * Manually create a snapshot for a workspace (owner only) */ -export async function POST( +async function handlePOST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Check if user is workspace owner - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - if (workspace[0].userId !== userId) { - return NextResponse.json( - { error: "Only workspace owner can create snapshots" }, - { status: 403 } - ); - } + // Check if user is workspace owner + await verifyWorkspaceOwnership(id, userId); // Create snapshot using utility function const result = await createSnapshot(id); @@ -115,54 +74,32 @@ export async function POST( ); } - return NextResponse.json({ - success: true, - version: result.version, - message: `Snapshot created at version ${result.version}`, - }); - } catch (error) { - console.error("Error in POST /api/workspaces/[id]/snapshot:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ + success: true, + version: result.version, + message: `Snapshot created at version ${result.version}`, + }); } +export const POST = withErrorHandling(handlePOST, "POST /api/workspaces/[id]/snapshot"); + /** * PUT /api/workspaces/[id]/snapshot * Check and create snapshot if needed (owner only) */ -export async function PUT( +async function handlePUT( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Check if user is workspace owner - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - if (workspace[0].userId !== userId) { - return NextResponse.json( - { error: "Only workspace owner can manage snapshots" }, - { status: 403 } - ); - } + // Check if user is workspace owner + await verifyWorkspaceOwnership(id, userId); // Check and create snapshot if needed const statusBefore = await checkNeedsSnapshot(id); @@ -179,14 +116,12 @@ export async function PUT( const statusAfter = await checkNeedsSnapshot(id); - return NextResponse.json({ - message: "Snapshot check complete", - created: true, - before: statusBefore, - after: statusAfter, - }); - } catch (error) { - console.error("Error in PUT /api/workspaces/[id]/snapshot:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ + message: "Snapshot check complete", + created: true, + before: statusBefore, + after: statusAfter, + }); } + +export const PUT = withErrorHandling(handlePUT, "PUT /api/workspaces/[id]/snapshot"); diff --git a/src/app/api/workspaces/[id]/snapshots/route.ts b/src/app/api/workspaces/[id]/snapshots/route.ts index f173b4bd..17f4f66c 100644 --- a/src/app/api/workspaces/[id]/snapshots/route.ts +++ b/src/app/api/workspaces/[id]/snapshots/route.ts @@ -1,46 +1,27 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import type { SnapshotInfo } from "@/lib/workspace/events"; -import { db, workspaces, workspaceSnapshots } from "@/lib/db/client"; +import { db, workspaceSnapshots } from "@/lib/db/client"; import { eq, desc } from "drizzle-orm"; +import { requireAuth, verifyWorkspaceOwnership, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces/[id]/snapshots * Fetch all snapshots for a workspace (for version history) * Note: Owner only (sharing is fork-based) */ -export async function GET( +async function handleGET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - try { - const { id } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { id } = await paramsPromise; + const userId = await authPromise; - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Check if user is workspace owner - const workspace = await db - .select({ userId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, id)) - .limit(1); - - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } - - // Enforce strict ownership (sharing is fork-based) - if (workspace[0].userId !== userId) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } + // Check if user is workspace owner + await verifyWorkspaceOwnership(id, userId); // Get ALL snapshots for version history const allSnapshotsData = await db @@ -58,10 +39,7 @@ export async function GET( state: s.state as any, })); - return NextResponse.json({ snapshots }); - } catch (error) { - console.error("Error in GET /api/workspaces/[id]/snapshots:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ snapshots }); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/[id]/snapshots"); diff --git a/src/app/api/workspaces/reorder/route.ts b/src/app/api/workspaces/reorder/route.ts index 5cf6f587..be262185 100644 --- a/src/app/api/workspaces/reorder/route.ts +++ b/src/app/api/workspaces/reorder/route.ts @@ -1,24 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import { db, workspaces } from "@/lib/db/client"; import { eq, inArray, and } from "drizzle-orm"; +import { requireAuth, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * POST /api/workspaces/reorder * Update the sort_order for multiple workspaces */ -export async function POST(request: NextRequest) { - try { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; +async function handlePOST(request: NextRequest) { + const userId = await requireAuth(); const body = await request.json(); const { workspaceIds } = body; @@ -62,10 +52,7 @@ export async function POST(request: NextRequest) { ); } - return NextResponse.json({ success: true }); - } catch (error) { - console.error("Error in POST /api/workspaces/reorder:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ success: true }); } +export const POST = withErrorHandling(handlePOST, "POST /api/workspaces/reorder"); diff --git a/src/app/api/workspaces/route.ts b/src/app/api/workspaces/route.ts index 04a8457e..10b8b3c9 100644 --- a/src/app/api/workspaces/route.ts +++ b/src/app/api/workspaces/route.ts @@ -1,30 +1,19 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import { getTemplateInitialState } from "@/lib/workspace/templates"; import type { WorkspaceWithState, WorkspaceTemplate } from "@/lib/workspace-state/types"; import type { CardColor } from "@/lib/workspace-state/colors"; import { randomUUID } from "crypto"; import { db, workspaces } from "@/lib/db/client"; import { eq, desc, asc, sql } from "drizzle-orm"; +import { requireAuth, requireAuthWithUserInfo, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces * List all workspaces for the authenticated user * Note: Sharing is fork-based - users import copies, not access the original */ -export async function GET() { - try { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - // Allow anonymous users - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; +async function handleGET() { + const userId = await requireAuth(); // Get workspaces owned by user // Order by sort_order ASC, fallback to updated_at DESC for null sort_order values @@ -53,29 +42,19 @@ export async function GET() { color: w.color as CardColor | null, })); - return NextResponse.json({ workspaces: workspaceList }); - } catch (error) { - console.error("Error in GET /api/workspaces:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ workspaces: workspaceList }); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces"); + /** * POST /api/workspaces * Create a new workspace */ -export async function POST(request: NextRequest) { - try { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - // Allow anonymous users - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; +async function handlePOST(request: NextRequest) { + // Use requireAuthWithUserInfo to avoid duplicate session fetch + const user = await requireAuthWithUserInfo(); + const userId = user.userId; const body = await request.json(); const { name, description, template, is_public, icon, color, initialState: customInitialState } = body; @@ -143,15 +122,8 @@ export async function POST(request: NextRequest) { const eventId = randomUUID(); const timestamp = Date.now(); - // Get user's display name from Better Auth session - // Better Auth stores name and email in the session - let userName: string | undefined; - try { - userName = session.user.name || session.user.email || undefined; - } catch { - // Could not get user name - // Continue without userName - } + // Use user info from requireAuthWithUserInfo to avoid duplicate session fetch + const userName = user.name || user.email || undefined; // Create snapshot event first (starts at version 0, creates version 1) try { @@ -223,10 +195,7 @@ export async function POST(request: NextRequest) { ...workspace, state: initialState, } - }, { status: 201 }); - } catch (error) { - console.error("Error in POST /api/workspaces:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + }, { status: 201 }); } +export const POST = withErrorHandling(handlePOST, "POST /api/workspaces"); diff --git a/src/app/api/workspaces/slug/[slug]/route.ts b/src/app/api/workspaces/slug/[slug]/route.ts index 19601ff2..3c5dd243 100644 --- a/src/app/api/workspaces/slug/[slug]/route.ts +++ b/src/app/api/workspaces/slug/[slug]/route.ts @@ -1,46 +1,40 @@ import { NextRequest, NextResponse } from "next/server"; -import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; import { db, workspaces } from "@/lib/db/client"; import { eq, and } from "drizzle-orm"; import { loadWorkspaceState } from "@/lib/workspace/state-loader"; +import { requireAuth, withErrorHandling } from "@/lib/api/workspace-helpers"; /** * GET /api/workspaces/slug/[slug] * Get a workspace by slug (more user-friendly than UUID) * Note: Owner only (sharing is fork-based) */ -export async function GET( +async function handleGET( request: NextRequest, { params }: { params: Promise<{ slug: string }> } ) { - try { - const { slug } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = session.user.id; - - // Get workspace by slug for this user (ownership only) - const workspace = await db - .select() - .from(workspaces) - .where( - and( - eq(workspaces.slug, slug), - eq(workspaces.userId, userId) - ) + // Start independent operations in parallel + const paramsPromise = params; + const authPromise = requireAuth(); + + const { slug } = await paramsPromise; + const userId = await authPromise; + + // Get workspace by slug for this user (ownership only) + const workspace = await db + .select() + .from(workspaces) + .where( + and( + eq(workspaces.slug, slug), + eq(workspaces.userId, userId) ) - .limit(1); + ) + .limit(1); - if (!workspace[0]) { - return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); - } + if (!workspace[0]) { + return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + } // Get workspace state by replaying events const state = await loadWorkspaceState(workspace[0].id); @@ -51,15 +45,13 @@ export async function GET( state.globalDescription = workspace[0].description || ""; } - return NextResponse.json({ - workspace: { - ...workspace[0], - state, - }, - }); - } catch (error) { - console.error("Error in GET /api/workspaces/slug/[slug]:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } + return NextResponse.json({ + workspace: { + ...workspace[0], + state, + }, + }); } +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/slug/[slug]"); + diff --git a/src/lib/api/workspace-helpers.ts b/src/lib/api/workspace-helpers.ts new file mode 100644 index 00000000..69ca89b8 --- /dev/null +++ b/src/lib/api/workspace-helpers.ts @@ -0,0 +1,120 @@ +import { NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { db, workspaces } from "@/lib/db/client"; +import { eq } from "drizzle-orm"; + +/** + * Get authenticated user from session + * Returns userId, name, and email or null if not authenticated + */ +export async function getAuthenticatedUser(): Promise<{ userId: string; name?: string; email?: string } | null> { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return null; + } + + return { + userId: session.user.id, + name: session.user.name ?? undefined, + email: session.user.email ?? undefined, + }; +} + +/** + * Verify workspace ownership and return workspace data + * Throws NextResponse errors for unauthorized/not found cases + */ +export async function verifyWorkspaceOwnership( + workspaceId: string, + userId: string +): Promise<{ userId: string }> { + const workspace = await db + .select({ userId: workspaces.userId }) + .from(workspaces) + .where(eq(workspaces.id, workspaceId)) + .limit(1); + + if (!workspace[0]) { + throw NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + } + + if (workspace[0].userId !== userId) { + throw NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + return workspace[0]; +} + +/** + * Verify workspace ownership and return full workspace data + * Throws NextResponse errors for unauthorized/not found cases + */ +export async function verifyWorkspaceOwnershipWithData( + workspaceId: string, + userId: string +): Promise { + const workspace = await db + .select() + .from(workspaces) + .where(eq(workspaces.id, workspaceId)) + .limit(1); + + if (!workspace[0]) { + throw NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + } + + if (workspace[0].userId !== userId) { + throw NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + return workspace[0]; +} + +/** + * Wrapper for API route handlers with error handling + */ +export function withErrorHandling( + handler: (...args: T) => Promise, + routeName: string +) { + return async (...args: T): Promise => { + try { + return await handler(...args); + } catch (error) { + // If error is already a Response/NextResponse, return it + if (error instanceof Response) { + return error as NextResponse; + } + + console.error(`Error in ${routeName}:`, error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } + }; +} + +/** + * Require authentication - returns userId or throws 401 + */ +export async function requireAuth(): Promise { + const user = await getAuthenticatedUser(); + if (!user) { + throw NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + return user.userId; +} + +/** + * Require authentication with user info - returns userId, name, and email or throws 401 + * Use this when you need user name/email to avoid duplicate session fetches + */ +export async function requireAuthWithUserInfo(): Promise<{ userId: string; name?: string; email?: string }> { + const user = await getAuthenticatedUser(); + if (!user) { + throw NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + return user; +}