fix(auth): resolve middleware Edge Runtime + layout redirect loop

Two bugs:
1. auth.ts imported postgres (Node.js TCP) which crashes in Edge Runtime,
   causing Auth.js to silently fall back to redirecting all requests to login.
   Fix: split into auth.config.ts (Edge-safe, no DB) used by middleware,
   and auth.ts (full, with DB) used by server components.

2. /agent/layout.tsx applied to /agent/login, so unauthenticated login page
   visits redirected to themselves in an infinite loop.
   Fix: moved dashboard + layout into (protected) route group so login page
   has no auth layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chandler Copeland
2026-03-19 14:08:04 -06:00
parent 0a75442af3
commit 39af0f19ba
5 changed files with 94 additions and 39 deletions

View File

@@ -1,25 +1,9 @@
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
import NextAuth from "next-auth";
import { authConfig } from "@/lib/auth.config";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAgentRoute = req.nextUrl.pathname.startsWith("/agent");
const isLoginPage = req.nextUrl.pathname === "/agent/login";
// Protect all /agent/* routes except the login page itself
if (isAgentRoute && !isLoginPage && !isLoggedIn) {
return NextResponse.redirect(new URL("/agent/login", req.nextUrl.origin));
}
// Redirect already-authenticated users away from the login page
if (isLoginPage && isLoggedIn) {
return NextResponse.redirect(new URL("/agent/dashboard", req.nextUrl.origin));
}
});
const { auth } = NextAuth(authConfig);
export default auth;
export const config = {
matcher: [
// Run on /agent/* routes, excluding Next.js internals and static files
"/((?!_next/static|_next/image|favicon.ico).*)",
],
matcher: ["/agent/:path*"],
};

View File

@@ -0,0 +1,17 @@
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
// Defense-in-depth session check (layout also checks, this is belt-and-suspenders)
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div>
<h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
<p className="mt-2 text-gray-500">
Welcome back, {session.user?.email}. Portal content coming in Phase 3.
</p>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { LogoutButton } from "@/components/ui/LogoutButton";
export default async function AgentLayout({
children,
}: {
children: React.ReactNode;
}) {
// Defense-in-depth: middleware handles most cases, this catches edge cases
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Agent Portal</span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">{session.user?.email}</span>
<LogoutButton />
</div>
</header>
<main className="p-6">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import type { NextAuthConfig } from "next-auth";
/**
* Edge-compatible auth config — no DB imports.
* Used by middleware.ts (Edge Runtime).
* Full auth.ts adds the Credentials provider with DB access.
*/
export const authConfig = {
session: {
strategy: "jwt",
maxAge: 7 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
},
pages: {
signIn: "/agent/login",
},
providers: [], // Providers added in auth.ts — not needed for middleware JWT check
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isLoginPage = nextUrl.pathname === "/agent/login";
const isAgentRoute = nextUrl.pathname.startsWith("/agent");
if (isLoginPage) {
if (isLoggedIn) return Response.redirect(new URL("/agent/dashboard", nextUrl.origin));
return true; // Always allow unauthenticated access to login page
}
if (isAgentRoute) {
return isLoggedIn; // Redirect unauthenticated users to login
}
return true;
},
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
session.user.id = token.id as string;
return session;
},
},
} satisfies NextAuthConfig;

View File

@@ -5,6 +5,7 @@ import { users } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { authConfig } from "./auth.config";
const loginSchema = z.object({
email: z.string().email(),
@@ -12,14 +13,7 @@ const loginSchema = z.object({
});
export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
maxAge: 7 * 24 * 60 * 60, // 7 days — rolling session per user decision
updateAge: 24 * 60 * 60, // Refresh cookie once per day (not every request)
},
pages: {
signIn: "/agent/login", // Custom login page — per user decision
},
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
@@ -44,14 +38,4 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
session.user.id = token.id as string;
return session;
},
},
});