From 5d8c653f25ef44eb9cd7069aedb39fad61546791 Mon Sep 17 00:00:00 2001 From: bettie Date: Sun, 8 Mar 2026 11:17:53 +0700 Subject: [PATCH 1/2] fix api security and client/server implications --- app/actions/upload.ts | 6 + app/api/chat/route.ts | 28 +- app/api/upload-image/route.ts | 37 +- .../settings/_components/settings-content.tsx | 428 +++++++++++++ app/dashboard/settings/page.tsx | 563 +----------------- app/dashboard/upload/page.tsx | 12 +- db/schema.ts | 11 + lib/routeHandler.ts | 67 +++ lib/serverUtils.ts | 61 ++ lib/utils.ts | 5 + middleware.ts | 1 + 11 files changed, 625 insertions(+), 594 deletions(-) create mode 100644 app/actions/upload.ts create mode 100644 app/dashboard/settings/_components/settings-content.tsx create mode 100644 lib/routeHandler.ts create mode 100644 lib/serverUtils.ts diff --git a/app/actions/upload.ts b/app/actions/upload.ts new file mode 100644 index 00000000..2e0c1651 --- /dev/null +++ b/app/actions/upload.ts @@ -0,0 +1,6 @@ +'use server' +import { secureFetch } from '@/lib/serverUtils' + +export async function uploadImageAction(formData: FormData): Promise<{ url: string }> { + return secureFetch('/api/upload-image', { method: 'POST', body: formData }) +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index b54a8939..6220bc76 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,16 +1,22 @@ import { openai } from "@ai-sdk/openai"; import { streamText } from "ai"; +import { createRouteHandler } from "@/lib/routeHandler"; +import { NextResponse } from "next/server"; -export async function POST(req: Request) { - const { messages } = await req.json(); +// IF this is public you might want to add a rate limiter! However I have added authentication checks. +export const POST = createRouteHandler( + { isAuthenticated: true }, + async (req) => { + const { messages } = await req.json(); - const result = streamText({ - model: openai.responses("gpt-4o"), - messages, - tools: { - web_search_preview: openai.tools.webSearchPreview(), - }, - }); + const result = streamText({ + model: openai.responses("gpt-4o"), + messages, + tools: { + web_search_preview: openai.tools.webSearchPreview(), + }, + }); - return result.toDataStreamResponse(); -} + return result.toDataStreamResponse() as NextResponse; + }, +); diff --git a/app/api/upload-image/route.ts b/app/api/upload-image/route.ts index e426ba15..e288dafe 100644 --- a/app/api/upload-image/route.ts +++ b/app/api/upload-image/route.ts @@ -1,13 +1,16 @@ +import { db } from "@/db/drizzle"; +import { userImage } from "@/db/schema"; +import { createRouteHandler } from "@/lib/routeHandler"; import { uploadImageAssets } from "@/lib/upload-image"; -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; export const config = { - api: { bodyParser: false }, // Disable default body parsing + api: { bodyParser: false }, }; -export async function POST(req: NextRequest) { - try { - // Parse the form data +export const POST = createRouteHandler( + { isAuthenticated: true }, + async (req) => { const formData = await req.formData(); const file = formData.get("file") as File | null; @@ -15,7 +18,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No file provided" }, { status: 400 }); } - // Validate MIME type - only allow image files const allowedMimeTypes = [ "image/jpeg", "image/jpg", @@ -32,8 +34,7 @@ export async function POST(req: NextRequest) { ); } - // Validate file size - limit to 10MB - const maxSizeInBytes = 10 * 1024 * 1024; // 10MB + const maxSizeInBytes = 10 * 1024 * 1024; if (file.size > maxSizeInBytes) { return NextResponse.json( { error: "File too large. Maximum size allowed is 10MB." }, @@ -41,24 +42,22 @@ export async function POST(req: NextRequest) { ); } - // Convert file to buffer const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - // Generate a unique filename with original extension const fileExt = file.name.split(".").pop() || ""; const timestamp = Date.now(); const filename = `upload-${timestamp}.${fileExt || "png"}`; - // Upload the file const url = await uploadImageAssets(buffer, filename); + await db.insert(userImage).values({ + id: crypto.randomUUID(), + url, + filename, + userId: req.user.id, + }); + return NextResponse.json({ url }); - } catch (error) { - console.error("Upload error:", error); - return NextResponse.json( - { error: "Failed to process upload" }, - { status: 500 }, - ); - } -} + }, +); diff --git a/app/dashboard/settings/_components/settings-content.tsx b/app/dashboard/settings/_components/settings-content.tsx new file mode 100644 index 00000000..113efa30 --- /dev/null +++ b/app/dashboard/settings/_components/settings-content.tsx @@ -0,0 +1,428 @@ +"use client"; + +import { uploadImageAction } from "@/app/actions/upload"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { authClient } from "@/lib/auth-client"; +import { ExternalLink, Settings2 } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +interface User { + id: string; + name: string; + email: string; + image?: string | null; +} + +interface OrderItem { + label: string; + amount: number; +} + +interface Order { + id: string; + product?: { + name: string; + }; + createdAt: string; + totalAmount: number; + currency: string; + status: string; + subscription?: { + status: string; + endedAt?: string; + }; + items: OrderItem[]; +} + +interface OrdersResponse { + result: { + items: Order[]; + }; +} + +interface SettingsContentProps { + initialUser: User; +} + +export default function SettingsContent({ initialUser }: SettingsContentProps) { + const [orders, setOrders] = useState(null); + const [currentTab, setCurrentTab] = useState("profile"); + const router = useRouter(); + const searchParams = useSearchParams(); + + const [name, setName] = useState(initialUser.name || ""); + const [email] = useState(initialUser.email || ""); + + const [profileImage, setProfileImage] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [uploadingImage, setUploadingImage] = useState(false); + const [userImage, setUserImage] = useState( + initialUser.image, + ); + + const { data: organizations } = authClient.useListOrganizations(); + + useEffect(() => { + const tab = searchParams.get("tab"); + if (tab && ["profile", "organization", "billing"].includes(tab)) { + setCurrentTab(tab); + } + }, [searchParams]); + + useEffect(() => { + const fetchOrders = async () => { + try { + const ordersResponse = await authClient.customer.orders.list({}); + if (ordersResponse.data) { + setOrders(ordersResponse.data as unknown as OrdersResponse); + } + } catch { + setOrders(null); + } + + try { + const { data: customerState } = await authClient.customer.state(); + console.log("customerState", customerState); + } catch { + // customer may not exist in Polar yet + } + }; + + fetchOrders(); + }, [organizations]); + + const handleTabChange = (value: string) => { + setCurrentTab(value); + const url = new URL(window.location.href); + url.searchParams.set("tab", value); + router.replace(url.pathname + url.search, { scroll: false }); + }; + + const handleUpdateProfile = async () => { + try { + await authClient.updateUser({ name }); + toast.success("Profile updated successfully"); + } catch { + toast.error("Failed to update profile"); + } + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setProfileImage(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const handleUploadProfilePicture = async () => { + if (!profileImage) return; + + setUploadingImage(true); + try { + const formData = new FormData(); + formData.append("file", profileImage); + + const { url } = await uploadImageAction(formData); + + await authClient.updateUser({ name, image: url }); + + setUserImage(url); + setImagePreview(null); + setProfileImage(null); + toast.success("Profile picture updated successfully"); + } catch { + toast.error("Failed to upload profile picture"); + } finally { + setUploadingImage(false); + } + }; + + return ( +
+
+

Settings

+

+ Manage your account settings and preferences +

+
+ + + + Profile + Billing + + + + + + + + Profile Information + + + Update your personal information and profile settings + + + +
+ + + + {name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+
+ + {profileImage && ( + + )} + {imagePreview && ( + + )} +
+ +

+ JPG, GIF or PNG. 1MB max. +

+
+
+ +
+
+ + setName(e.target.value)} + placeholder="Enter your full name" + /> +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+

Billing History

+

+ View your past and upcoming invoices +

+
+ +
+ {orders?.result?.items && orders.result.items.length > 0 ? ( +
+ {(orders.result.items || []).map((order) => ( + + +
+
+
+
+

+ {order.product?.name || "Subscription"} +

+
+ {order.subscription?.status === "paid" ? ( + + Paid + + ) : order.subscription?.status === + "canceled" ? ( + + Canceled + + ) : order.subscription?.status === + "refunded" ? ( + + Refunded + + ) : ( + + {order.subscription?.status} + + )} + + {order.subscription?.status === "canceled" && ( + + • Canceled on{" "} + {order.subscription.endedAt + ? new Date( + order.subscription.endedAt, + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + : "N/A"} + + )} +
+
+
+ {new Date(order.createdAt).toLocaleDateString( + "en-US", + { + year: "numeric", + month: "short", + day: "numeric", + }, + )} +
+
+ +
+
+ ${(order.totalAmount / 100).toFixed(2)} +
+
+ {order.currency?.toUpperCase()} +
+
+
+ + {order.items?.length > 0 && ( +
+
    + {order.items.map((item, index: number) => ( +
  • + + {item.label} + + + ${(item.amount / 100).toFixed(2)} + +
  • + ))} +
+
+ )} +
+
+
+ ))} +
+ ) : ( + + +
+ + + +

+ No orders found +

+

+ {orders === null + ? "Unable to load billing history. This may be because your account is not yet set up for billing." + : "You don't have any orders yet. Your billing history will appear here."} +

+
+
+
+ )} +
+
+
+
+ ); +} diff --git a/app/dashboard/settings/page.tsx b/app/dashboard/settings/page.tsx index 04c87c73..21267da3 100644 --- a/app/dashboard/settings/page.tsx +++ b/app/dashboard/settings/page.tsx @@ -1,558 +1,13 @@ -"use client"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import SettingsContent from "./_components/settings-content"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { authClient } from "@/lib/auth-client"; -import { ExternalLink, Settings2 } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; -import { toast } from "sonner"; +export default async function SettingsPage() { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.user) redirect("/sign-in"); -interface User { - id: string; - name: string; - email: string; - image?: string | null; -} - -interface OrderItem { - label: string; - amount: number; -} - -interface Order { - id: string; - product?: { - name: string; - }; - createdAt: string; - totalAmount: number; - currency: string; - status: string; - subscription?: { - status: string; - endedAt?: string; - }; - items: OrderItem[]; -} - -interface OrdersResponse { - result: { - items: Order[]; - }; -} - -function SettingsContent() { - const [user, setUser] = useState(null); - const [orders, setOrders] = useState(null); - const [loading, setLoading] = useState(true); - const [currentTab, setCurrentTab] = useState("profile"); - const router = useRouter(); - const searchParams = useSearchParams(); - - // Profile form states - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - - // Profile picture upload states - const [profileImage, setProfileImage] = useState(null); - const [imagePreview, setImagePreview] = useState(null); - const [uploadingImage, setUploadingImage] = useState(false); - - const { data: organizations } = authClient.useListOrganizations(); - - // Handle URL tab parameter - useEffect(() => { - const tab = searchParams.get("tab"); - if (tab && ["profile", "organization", "billing"].includes(tab)) { - setCurrentTab(tab); - } - }, [searchParams]); - - useEffect(() => { - const fetchData = async () => { - try { - // Get user session - const session = await authClient.getSession(); - if (session.data?.user) { - setUser(session.data.user); - setName(session.data.user.name || ""); - setEmail(session.data.user.email || ""); - } - - // Try to fetch orders and customer state with better error handling - try { - const ordersResponse = await authClient.customer.orders.list({}); - - if (ordersResponse.data) { - setOrders(ordersResponse.data as unknown as OrdersResponse); - } else { - console.log("No orders found or customer not created yet"); - setOrders(null); - } - } catch (orderError) { - console.log( - "Orders fetch failed - customer may not exist in Polar yet:", - orderError, - ); - setOrders(null); - } - - try { - const { data: customerState } = await authClient.customer.state(); - console.log("customerState", customerState); - } catch (customerError) { - console.log("Customer state fetch failed:", customerError); - } - } catch (error) { - console.error("Error fetching data:", error); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, [organizations]); - - const handleTabChange = (value: string) => { - setCurrentTab(value); - const url = new URL(window.location.href); - url.searchParams.set("tab", value); - router.replace(url.pathname + url.search, { scroll: false }); - }; - - const handleUpdateProfile = async () => { - try { - await authClient.updateUser({ - name, - }); - toast.success("Profile updated successfully"); - } catch { - toast.error("Failed to update profile"); - } - }; - - const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - setProfileImage(file); - const reader = new FileReader(); - reader.onloadend = () => { - setImagePreview(reader.result as string); - }; - reader.readAsDataURL(file); - } - }; - - const handleUploadProfilePicture = async () => { - if (!profileImage) return; - - setUploadingImage(true); - try { - const formData = new FormData(); - formData.append("file", profileImage); - - // Upload to your R2 storage endpoint - const response = await fetch("/api/upload-image", { - method: "POST", - body: formData, - }); - - if (response.ok) { - const { url } = await response.json(); - - // Update user profile with new image URL - await authClient.updateUser({ - name, - image: url, - }); - - setUser((prev) => (prev ? { ...prev, image: url } : null)); - setImagePreview(null); - setProfileImage(null); - toast.success("Profile picture updated successfully"); - } else { - throw new Error("Upload failed"); - } - } catch { - toast.error("Failed to upload profile picture"); - } finally { - setUploadingImage(false); - } - }; - if (loading) { - return ( -
- {/* Header Skeleton */} -
- - -
- - {/* Tabs Skeleton */} -
-
- - - -
- -
- {/* Profile Information Card Skeleton */} - - -
- - -
- -
- -
- -
-
- - - -
- -
-
- -
-
- - -
-
- - -
-
- - -
-
- - {/* Change Password Card Skeleton */} - - - - - - -
- - -
-
- - -
-
- - -
- -
-
-
-
-
- ); - } - - return ( -
- {/* Header */} -
-

Settings

-

- Manage your account settings and preferences -

-
- - - - Profile - Billing - - - - {/* Profile Information */} - - - - - Profile Information - - - Update your personal information and profile settings - - - -
- - - - {name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-
- - {profileImage && ( - - )} - {imagePreview && ( - - )} -
- -

- JPG, GIF or PNG. 1MB max. -

-
-
- -
-
- - setName(e.target.value)} - placeholder="Enter your full name" - /> -
-
- - setEmail(e.target.value)} - placeholder="Enter your email" - disabled - /> -
-
- - -
-
-
- - -
-
-
-

Billing History

-

- View your past and upcoming invoices -

-
- -
- {orders?.result?.items && orders.result.items.length > 0 ? ( -
- {(orders.result.items || []).map((order) => ( - - -
- {/* Header Row */} -
-
-
-

- {order.product?.name || "Subscription"} -

-
- {order.subscription?.status === "paid" ? ( - - Paid - - ) : order.subscription?.status === - "canceled" ? ( - - Canceled - - ) : order.subscription?.status === - "refunded" ? ( - - Refunded - - ) : ( - - {order.subscription?.status} - - )} - - {order.subscription?.status === "canceled" && ( - - • Canceled on{" "} - {order.subscription.endedAt - ? new Date( - order.subscription.endedAt, - ).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }) - : "N/A"} - - )} -
-
-
- {new Date(order.createdAt).toLocaleDateString( - "en-US", - { - year: "numeric", - month: "short", - day: "numeric", - }, - )} -
-
- -
-
- ${(order.totalAmount / 100).toFixed(2)} -
-
- {order.currency?.toUpperCase()} -
-
-
- - {/* Order Items */} - {order.items?.length > 0 && ( -
-
    - {order.items.map((item, index: number) => ( -
  • - - {item.label} - - - ${(item.amount / 100).toFixed(2)} - -
  • - ))} -
-
- )} -
-
-
- ))} -
- ) : ( - - -
- - - -

- No orders found -

-

- {orders === null - ? "Unable to load billing history. This may be because your account is not yet set up for billing." - : "You don't have any orders yet. Your billing history will appear here."} -

-
-
-
- )} -
-
-
-
- ); -} - -export default function SettingsPage() { return ( } > - + ); } diff --git a/app/dashboard/upload/page.tsx b/app/dashboard/upload/page.tsx index e46d5335..c0e00978 100644 --- a/app/dashboard/upload/page.tsx +++ b/app/dashboard/upload/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { uploadImageAction } from "@/app/actions/upload"; import { Button } from "@/components/ui/button"; import { Card, @@ -62,20 +63,11 @@ export default function UploadPage() { }); }, 200); - const response = await fetch("/api/upload-image", { - method: "POST", - body: formData, - }); + const { url } = await uploadImageAction(formData); clearInterval(progressInterval); setUploadProgress(100); - if (!response.ok) { - throw new Error("Upload failed"); - } - - const { url } = await response.json(); - const uploadedFile: UploadedFile = { id: crypto.randomUUID(), name: file.name, diff --git a/db/schema.ts b/db/schema.ts index 183c3484..6fa2e7a5 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -57,6 +57,17 @@ export const verification = pgTable("verification", { updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); +// Uploaded images table +export const userImage = pgTable("userImage", { + id: text("id").primaryKey(), + url: text("url").notNull(), + filename: text("filename").notNull(), + userId: text("userId") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + createdAt: timestamp("createdAt").notNull().defaultNow(), +}); + // Subscription table for Polar webhook data export const subscription = pgTable("subscription", { id: text("id").primaryKey(), diff --git a/lib/routeHandler.ts b/lib/routeHandler.ts new file mode 100644 index 00000000..c60bf62d --- /dev/null +++ b/lib/routeHandler.ts @@ -0,0 +1,67 @@ +import 'server-only' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' +import { NextRequest, NextResponse } from 'next/server' + +type SessionData = typeof auth.$Infer.Session +type User = SessionData['user'] +type SessionObj = SessionData['session'] + +interface AuthenticatedRequest extends NextRequest { + user: User + session: SessionObj +} + +type RouteOptions = + | { isPublic: true; isAuthenticated?: never } + | { isAuthenticated: true; isPublic?: never } + +type PublicHandler = (req: NextRequest, ctx: RouteContext) => Promise +type AuthenticatedHandler = (req: AuthenticatedRequest, ctx: RouteContext) => Promise + +interface RouteContext { + params?: Promise> +} + +export function createRouteHandler( + options: { isPublic: true }, + handler: PublicHandler, +): (req: NextRequest, ctx: RouteContext) => Promise + +export function createRouteHandler( + options: { isAuthenticated: true }, + handler: AuthenticatedHandler, +): (req: NextRequest, ctx: RouteContext) => Promise + +export function createRouteHandler( + options: RouteOptions, + handler: PublicHandler | AuthenticatedHandler, +) { + return async (req: NextRequest, ctx: RouteContext): Promise => { + try { + if (options.isAuthenticated) { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user || !session?.session) { + return NextResponse.json( + { data: null, error: 'Unauthorized' }, + { status: 401 }, + ) + } + + const authedReq = req as AuthenticatedRequest + authedReq.user = session.user + authedReq.session = session.session + + return await (handler as AuthenticatedHandler)(authedReq, ctx) + } + + return await (handler as PublicHandler)(req, ctx) + } catch (error) { + console.error('Route handler error:', error) + return NextResponse.json( + { data: null, error: 'Internal server error' }, + { status: 500 }, + ) + } + } +} diff --git a/lib/serverUtils.ts b/lib/serverUtils.ts new file mode 100644 index 00000000..3029503e --- /dev/null +++ b/lib/serverUtils.ts @@ -0,0 +1,61 @@ +import 'server-only' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' +import { getBaseUrl } from '@/lib/utils' + +async function getAuthHeaders(): Promise { + const reqHeaders = await headers() + const cookie = reqHeaders.get('cookie') + return cookie ? { cookie } : {} +} + +export async function secureFetch( + path: string, + init?: RequestInit, +): Promise { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + throw new Error('Unauthorized') + } + + const authHeaders = await getAuthHeaders() + const isFormData = init?.body instanceof FormData + + const response = await fetch(`${getBaseUrl()}${path}`, { + ...init, + headers: { + ...authHeaders, + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...(init?.headers ?? {}), + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(error?.error ?? response.statusText) + } + + return response.json() as Promise +} + +export async function publicFetch( + path: string, + init?: RequestInit, +): Promise { + const isFormData = init?.body instanceof FormData + + const response = await fetch(`${getBaseUrl()}${path}`, { + ...init, + headers: { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...(init?.headers ?? {}), + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(error?.error ?? response.statusText) + } + + return response.json() as Promise +} diff --git a/lib/utils.ts b/lib/utils.ts index bd0c391d..bb08d403 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -4,3 +4,8 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function getBaseUrl() { + if (process.env.NEXT_PUBLIC_APP_URL) return process.env.NEXT_PUBLIC_APP_URL + return `http://localhost:${process.env.PORT ?? 3000}` +} diff --git a/middleware.ts b/middleware.ts index 83175c54..89a69cdb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -5,6 +5,7 @@ export async function middleware(request: NextRequest) { const sessionCookie = getSessionCookie(request); const { pathname } = request.nextUrl; + // api route protection is handled in each api route // /api/payments/webhooks is a webhook endpoint that should be accessible without authentication if (pathname.startsWith("/api/payments/webhooks")) { return NextResponse.next(); From 0d17fb3571c7e87f1e2a698900cf164184daccf1 Mon Sep 17 00:00:00 2001 From: JJ Eaton <58495321+jayleaton@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:46:31 +0700 Subject: [PATCH 2/2] Update app/api/chat/route.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- app/api/chat/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 6220bc76..06e319cf 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -17,6 +17,6 @@ export const POST = createRouteHandler( }, }); - return result.toDataStreamResponse() as NextResponse; + return result.toDataStreamResponse() as unknown as NextResponse; }, );