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 NextAuth from "next-auth";
|
||||||
import { NextResponse } from "next/server";
|
import { authConfig } from "@/lib/auth.config";
|
||||||
|
|
||||||
export default auth((req) => {
|
const { auth } = NextAuth(authConfig);
|
||||||
const isLoggedIn = !!req.auth;
|
export default 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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: ["/agent/:path*"],
|
||||||
// Run on /agent/* routes, excluding Next.js internals and static files
|
|
||||||
"/((?!_next/static|_next/image|favicon.ico).*)",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { eq } from "drizzle-orm";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { authConfig } from "./auth.config";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
@@ -12,14 +13,7 @@ const loginSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||||
session: {
|
...authConfig,
|
||||||
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
|
|
||||||
},
|
|
||||||
providers: [
|
providers: [
|
||||||
Credentials({
|
Credentials({
|
||||||
async authorize(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