feat(01-01): configure Auth.js v5 JWT + Credentials, route protection middleware
- Created src/lib/auth.ts with NextAuth JWT strategy, 7-day rolling session, Credentials provider - Created src/app/api/auth/[...nextauth]/route.ts with GET/POST handlers and force-dynamic - Created middleware.ts at project root (not src/) protecting /agent/* routes - Fixed db/index.ts: lazy Proxy singleton prevents neon() crash during Next.js build - npm run build passes; /api/auth/[...nextauth] renders as Dynamic route
This commit is contained in:
25
teressa-copeland-homes/middleware.ts
Normal file
25
teressa-copeland-homes/middleware.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Run on /agent/* routes, excluding Next.js internals and static files
|
||||
"/((?!_next/static|_next/image|favicon.ico).*)",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
57
teressa-copeland-homes/src/lib/auth.ts
Normal file
57
teressa-copeland-homes/src/lib/auth.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { db } from "@/lib/db";
|
||||
import { users } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { z } from "zod";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
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
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
async authorize(credentials) {
|
||||
const parsed = loginSchema.safeParse(credentials);
|
||||
if (!parsed.success) return null;
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, parsed.data.email))
|
||||
.limit(1);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const passwordMatch = await bcrypt.compare(
|
||||
parsed.data.password,
|
||||
user.passwordHash
|
||||
);
|
||||
if (!passwordMatch) return null;
|
||||
|
||||
return { id: user.id, email: user.email };
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
jwt({ token, user }) {
|
||||
if (user) token.id = user.id;
|
||||
return token;
|
||||
},
|
||||
session({ session, token }) {
|
||||
session.user.id = token.id as string;
|
||||
return session;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -2,5 +2,23 @@ import { drizzle } from "drizzle-orm/neon-http";
|
||||
import { neon } from "@neondatabase/serverless";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
export const db = drizzle({ client: sql, schema });
|
||||
type DrizzleDb = ReturnType<typeof createDb>;
|
||||
|
||||
function createDb() {
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) {
|
||||
throw new Error("DATABASE_URL environment variable is not set");
|
||||
}
|
||||
const sql = neon(url);
|
||||
return drizzle({ client: sql, schema });
|
||||
}
|
||||
|
||||
// Lazy singleton — created on first use, not at module load time
|
||||
let _db: DrizzleDb | undefined;
|
||||
|
||||
export const db = new Proxy({} as DrizzleDb, {
|
||||
get(_target, prop: string | symbol) {
|
||||
if (!_db) _db = createDb();
|
||||
return (_db as unknown as Record<string | symbol, unknown>)[prop];
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user