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:
@@ -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*"],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
26
teressa-copeland-homes/src/app/agent/(protected)/layout.tsx
Normal file
26
teressa-copeland-homes/src/app/agent/(protected)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
teressa-copeland-homes/src/lib/auth.config.ts
Normal file
44
teressa-copeland-homes/src/lib/auth.config.ts
Normal 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;
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user