Migrate middleware.ts to proxy.ts for Next.js 16 and fix build errors

Next.js 16 renamed middleware to proxy. Merged session-based auth and
API key auth into a single proxy.ts. Also fixed: auth route handler
exports, missing card component, Button asChild type errors, signIn
return type, Drizzle eq() type narrowing, and useSearchParams suspense
boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 21:52:38 +01:00
parent 76cb49e908
commit 3e242c3359
9 changed files with 176 additions and 110 deletions

2
next-env.d.ts vendored
View file

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, Suspense } from "react";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
@ -13,6 +13,14 @@ import { AlertCircle } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export default function LoginPage() { export default function LoginPage() {
return (
<Suspense>
<LoginPageContent />
</Suspense>
);
}
function LoginPageContent() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -36,15 +44,11 @@ export default function LoginPage() {
setError(null); setError(null);
setIsLoading(true); setIsLoading(true);
try { try {
const result = await signIn("credentials", { await signIn("credentials", {
email, email,
password, password,
redirectTo: "/app", redirectTo: "/app",
}); });
// If signIn returns with ok: false, it means auth failed
if (result && !result.ok) {
setError("Invalid email or password");
}
} catch { } catch {
setError("An error occurred. Please try again."); setError("An error occurred. Please try again.");
} finally { } finally {

View file

@ -2,8 +2,9 @@
import Link from "next/link"; import Link from "next/link";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { Button } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { ChartColumn } from "lucide-react"; import { ChartColumn } from "lucide-react";
import { cn } from "@/lib/utils";
export function Navbar() { export function Navbar() {
const { data: session } = useSession(); const { data: session } = useSession();
@ -19,17 +20,26 @@ export function Navbar() {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{session ? ( {session ? (
<Button size="sm" className="font-mono text-xs" asChild> <Link
<Link href="/app">Go to App</Link> href="/app"
</Button> className={cn(buttonVariants({ size: "sm" }), "font-mono text-xs")}
>
Go to App
</Link>
) : ( ) : (
<> <>
<Button variant="ghost" size="sm" className="font-mono text-xs" asChild> <Link
<Link href="/login">Log in</Link> href="/login"
</Button> className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "font-mono text-xs")}
<Button size="sm" className="font-mono text-xs" asChild> >
<Link href="/register">Get Started</Link> Log in
</Button> </Link>
<Link
href="/register"
className={cn(buttonVariants({ size: "sm" }), "font-mono text-xs")}
>
Get Started
</Link>
</> </>
)} )}
</div> </div>

View file

@ -1,5 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Navbar } from "./navbar"; import { Navbar } from "./navbar";
import { import {
ChartColumn, ChartColumn,
@ -36,19 +37,18 @@ export default function LandingPage() {
keyboard-driven workspace. keyboard-driven workspace.
</p> </p>
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<Button size="lg" className="font-mono text-sm gap-2" asChild> <Link
<Link href="/register"> href="/register"
className={cn(buttonVariants({ size: "lg" }), "font-mono text-sm gap-2")}
>
Start Annotating <ArrowRight className="w-4 h-4" /> Start Annotating <ArrowRight className="w-4 h-4" />
</Link> </Link>
</Button> <Link
<Button href="/app"
size="lg" className={cn(buttonVariants({ size: "lg", variant: "outline" }), "font-mono text-sm")}
variant="outline"
className="font-mono text-sm"
asChild
> >
<Link href="/app">Try Demo</Link> Try Demo
</Button> </Link>
</div> </div>
</div> </div>
</section> </section>
@ -162,11 +162,12 @@ export default function LandingPage() {
<p className="text-muted-foreground text-sm mb-6"> <p className="text-muted-foreground text-sm mb-6">
No credit card required. Start annotating charts in seconds. No credit card required. Start annotating charts in seconds.
</p> </p>
<Button size="lg" className="font-mono text-sm gap-2" asChild> <Link
<Link href="/register"> href="/register"
className={cn(buttonVariants({ size: "lg" }), "font-mono text-sm gap-2")}
>
Create Free Account <ArrowRight className="w-4 h-4" /> Create Free Account <ArrowRight className="w-4 h-4" />
</Link> </Link>
</Button>
</section> </section>
{/* Footer */} {/* Footer */}

View file

@ -1 +1,2 @@
export { GET, POST } from "@/auth"; import { handlers } from "@/auth";
export const { GET, POST } = handlers;

View file

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -1,6 +1,10 @@
import { auth } from "@/auth"; import { auth } from "@/auth";
export async function getAuthUser() { export type AuthUser = { id: string; name?: string | null; email?: string | null; image?: string | null };
export async function getAuthUser(): Promise<AuthUser | null> {
const session = await auth(); const session = await auth();
return session?.user ?? null; const user = session?.user;
if (!user?.id) return null;
return user as AuthUser;
} }

View file

@ -1,44 +0,0 @@
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export const middleware = auth((req) => {
const { pathname } = req.nextUrl;
const isAuthenticated = !!req.auth;
// Protect /api/* except /api/auth/* and /api/health
if (pathname.startsWith("/api/")) {
const isAuthRoute = pathname.startsWith("/api/auth/");
const isHealthRoute = pathname === "/api/health";
if (!isAuthRoute && !isHealthRoute && !isAuthenticated) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.next();
}
// Redirect authenticated users away from /login and /register
if (isAuthenticated && (pathname === "/login" || pathname === "/register")) {
return NextResponse.redirect(new URL("/app", req.nextUrl.origin));
}
// Protect /app/* routes — redirect unauthenticated users to /login
if (pathname.startsWith("/app") && !isAuthenticated) {
return NextResponse.redirect(new URL("/login", req.nextUrl.origin));
}
return NextResponse.next();
});
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimisation)
* - favicon.ico / favicon.png / favicon.svg
* - public assets (top-level files that are not pages)
*/
"/((?!_next/static|_next/image|favicon\\.ico|favicon\\.png|favicon\\.svg).*)",
],
};

View file

@ -1,42 +1,53 @@
import { auth } from "@/auth";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) { export const proxy = auth((req) => {
const { pathname } = request.nextUrl; const { pathname } = req.nextUrl;
const isAuthenticated = !!req.auth;
// Skip auth check for the health endpoint // Protect /api/* except /api/auth/* and /api/health
if (pathname === "/api/health") { if (pathname.startsWith("/api/")) {
return NextResponse.next(); const isAuthRoute = pathname.startsWith("/api/auth/");
} const isHealthRoute = pathname === "/api/health";
if (!isAuthRoute && !isHealthRoute && !isAuthenticated) {
// Allow external clients with a valid API key
const apiKey = process.env.API_KEY; const apiKey = process.env.API_KEY;
if (apiKey) {
// If API_KEY is not configured, skip auth check (fail-open for development) const requestApiKey = req.headers.get("X-API-Key");
if (!apiKey) { if (requestApiKey === apiKey) {
console.warn(
"Warning: API_KEY environment variable is not set. API authentication is disabled."
);
return NextResponse.next(); return NextResponse.next();
} }
const requestApiKey = request.headers.get("X-API-Key");
// Allow same-origin browser requests (UI -> /api/*) without exposing API_KEY to client JS.
// Keep API key auth for non-browser/external clients.
const fetchSite = request.headers.get("sec-fetch-site");
const isSameOriginBrowserRequest = fetchSite === "same-origin";
if (isSameOriginBrowserRequest) {
return NextResponse.next();
} }
if (!requestApiKey || requestApiKey !== apiKey) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
return NextResponse.next(); return NextResponse.next();
} }
// Redirect authenticated users away from /login and /register
if (isAuthenticated && (pathname === "/login" || pathname === "/register")) {
return NextResponse.redirect(new URL("/app", req.nextUrl.origin));
}
// Protect /app/* routes — redirect unauthenticated users to /login
if (pathname.startsWith("/app") && !isAuthenticated) {
return NextResponse.redirect(new URL("/login", req.nextUrl.origin));
}
return NextResponse.next();
});
export const config = { export const config = {
matcher: ["/api/:path*"], matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimisation)
* - favicon.ico / favicon.png / favicon.svg
* - public assets (top-level files that are not pages)
*/
"/((?!_next/static|_next/image|favicon\\.ico|favicon\\.png|favicon\\.svg).*)",
],
}; };