diff --git a/teressa-copeland-homes/middleware.ts b/teressa-copeland-homes/middleware.ts new file mode 100644 index 0000000..561ec35 --- /dev/null +++ b/teressa-copeland-homes/middleware.ts @@ -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).*)", + ], +}; diff --git a/teressa-copeland-homes/src/app/api/auth/[...nextauth]/route.ts b/teressa-copeland-homes/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..04b3dff --- /dev/null +++ b/teressa-copeland-homes/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,5 @@ +import { handlers } from "@/lib/auth"; + +export const dynamic = "force-dynamic"; + +export const { GET, POST } = handlers; diff --git a/teressa-copeland-homes/src/lib/auth.ts b/teressa-copeland-homes/src/lib/auth.ts new file mode 100644 index 0000000..e5288c5 --- /dev/null +++ b/teressa-copeland-homes/src/lib/auth.ts @@ -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; + }, + }, +}); diff --git a/teressa-copeland-homes/src/lib/db/index.ts b/teressa-copeland-homes/src/lib/db/index.ts index fabb6c0..aa0e644 100644 --- a/teressa-copeland-homes/src/lib/db/index.ts +++ b/teressa-copeland-homes/src/lib/db/index.ts @@ -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; + +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)[prop]; + }, +});