diff --git a/next.config.ts b/next.config.ts index 2e27cc2..3850d87 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,10 @@ import type { NextConfig } from "next" +import crypto from "crypto" + +// Generate a random JWT secret on every dev server start to invalidate all prior sessions +if (process.env.NODE_ENV !== "production") { + process.env.JWT_SECRET = crypto.randomUUID() +} const nextConfig: NextConfig = { eslint: { diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 53f5638..24cf19f 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,5 +1,7 @@ "use client" +import { useEffect } from "react" +import { useRouter } from "next/navigation" import { AppShell } from "@/components/layout/app-shell" import { UserProvider, useUser } from "@/providers/user-provider" import { NotificationProvider } from "@/providers/notification-provider" @@ -8,8 +10,15 @@ import { Loader2 } from "lucide-react" function DashboardContent({ children }: { children: React.ReactNode }) { const { user, loading } = useUser() + const router = useRouter() - if (loading) { + useEffect(() => { + if (!user && !loading) { + router.push("/login") + } + }, [user, loading, router]) + + if (loading || !user) { return (
@@ -17,8 +26,6 @@ function DashboardContent({ children }: { children: React.ReactNode }) { ) } - if (!user) return null - return {children} } diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 5f24e89..2d23832 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -10,6 +10,7 @@ import { isAccountLocked, createSession, setSessionContext, + SESSION_COOKIE, } from "@/lib/auth" export async function POST(request: NextRequest) { @@ -106,12 +107,19 @@ export async function POST(request: NextRequest) { true ) - await createSession(dbUser.id, dbUser.role_name) + const token = await createSession(dbUser.id, dbUser.role_name) await setSessionContext(dbUser.id, ipAddress) const user = mapDbUserToSessionUser(dbUser) - return NextResponse.json({ user }, { status: 200 }) + const response = NextResponse.json({ user }, { status: 200 }) + response.cookies.set(SESSION_COOKIE, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + path: "/", + }) + return response } catch (error) { console.error("Login error:", error) return NextResponse.json( diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index fb16836..f510ec9 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,10 +1,17 @@ import { NextResponse } from "next/server" -import { destroySession } from "@/lib/auth" +import { SESSION_COOKIE } from "@/lib/auth" export async function POST() { try { - await destroySession() - return NextResponse.json({ success: true }, { status: 200 }) + const response = NextResponse.json({ success: true }, { status: 200 }) + response.cookies.set(SESSION_COOKIE, "", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + path: "/", + maxAge: 0, + }) + return response } catch (error) { console.error("Logout error:", error) return NextResponse.json( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c1ebbef..587932b 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, Suspense } from "react" import { useSearchParams, useRouter } from "next/navigation" import { Eye, EyeOff, Loader2 } from "lucide-react" @@ -13,7 +13,20 @@ const waves = [ ] export default function LoginPage() { + return ( + +
+
+ }> + +
+ ) +} + +function LoginForm() { const router = useRouter() + const searchParams = useSearchParams() const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [showPassword, setShowPassword] = useState(false) @@ -214,7 +227,8 @@ export default function LoginPage() { }) if (res.ok) { - router.push("/dashboard") + const redirectTo = searchParams.get("redirect") || "/dashboard" + router.push(redirectTo) } else { const data = await res.json().catch(() => ({})) setError(data.error || "Invalid email or password.") diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8794412..1f395cf 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -9,7 +9,7 @@ if (!RAW_SECRET) { } const JWT_SECRET = new TextEncoder().encode(RAW_SECRET); -const SESSION_COOKIE = "session"; +export const SESSION_COOKIE = "session"; const MAX_FAILED_ATTEMPTS = 5; const LOCKOUT_DURATION_MINUTES = 15; @@ -195,28 +195,7 @@ export function mapDbUserToSessionUser( } export async function createSession(userId: string, role: string) { - const token = await signToken({ userId, role }); - - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE, token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - path: "/", - }); - - return token; -} - -export async function destroySession() { - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE, "", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - path: "/", - maxAge: 0, - }); + return signToken({ userId, role }); } export async function encryptPassword(password: string): Promise {