Implement session management (Task 5)
- Configure JWT session strategy with 30-day expiration - Implement session validation middleware for route protection - Create logout functionality with session termination - Handle session expiration automatically via JWT - Add session helper functions (getCurrentSession, isAuthenticated) - Protect routes: /, /list/*, /profile/*, /invitations/* - Redirect unauthenticated users to login with callback URL - Requirements: 2.4, 2.5, 3.1, 3.2, 3.4
This commit is contained in:
@@ -56,7 +56,7 @@
|
|||||||
- **Property 7: Invalid credentials rejection**
|
- **Property 7: Invalid credentials rejection**
|
||||||
- **Validates: Requirements 2.2**
|
- **Validates: Requirements 2.2**
|
||||||
|
|
||||||
- [ ] 5. Implement session management
|
- [x] 5. Implement session management
|
||||||
- Configure JWT session strategy in NextAuth
|
- Configure JWT session strategy in NextAuth
|
||||||
- Implement session validation middleware
|
- Implement session validation middleware
|
||||||
- Create logout functionality
|
- Create logout functionality
|
||||||
|
|||||||
15
auth.ts
15
auth.ts
@@ -13,6 +13,8 @@ const loginSchema = z.object({
|
|||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
|
||||||
|
updateAge: 24 * 60 * 60, // Update session every 24 hours
|
||||||
},
|
},
|
||||||
providers: [
|
providers: [
|
||||||
Credentials({
|
Credentials({
|
||||||
@@ -57,18 +59,27 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user, trigger }) {
|
||||||
// Add user ID to token on sign in
|
// Add user ID to token on sign in
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id
|
token.id = user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle session expiration
|
||||||
|
// Token will be automatically invalidated by NextAuth when it expires
|
||||||
|
// based on the maxAge setting in session config
|
||||||
|
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
// Add user ID to session from token
|
// Add user ID to session from token
|
||||||
if (session.user) {
|
if (session.user && token.id) {
|
||||||
session.user.id = token.id as string
|
session.user.id = token.id as string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session validation happens automatically via JWT expiration
|
||||||
|
// If token is expired, this callback won't be called and user will be redirected
|
||||||
|
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { prisma } from './db'
|
import { prisma } from './db'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { signIn } from '@/auth'
|
import { signIn, signOut } from '@/auth'
|
||||||
|
|
||||||
// Validation schemas
|
// Validation schemas
|
||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
@@ -132,3 +132,44 @@ export async function loginUser(
|
|||||||
return { success: false, error: 'Invalid email or password' }
|
return { success: false, error: 'Invalid email or password' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user and terminate session
|
||||||
|
* Requirement: 2.5 - Logout terminates session and clears authentication tokens
|
||||||
|
*/
|
||||||
|
export async function logoutUser(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Sign out using NextAuth - this will:
|
||||||
|
// 1. Clear the session token
|
||||||
|
// 2. Remove authentication cookies
|
||||||
|
// 3. Terminate the session
|
||||||
|
await signOut({ redirect: false })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
throw new Error('Logout failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current session
|
||||||
|
* Requirements: 3.2 - Session validation and persistence
|
||||||
|
*/
|
||||||
|
export async function getCurrentSession() {
|
||||||
|
try {
|
||||||
|
const { auth } = await import('@/auth')
|
||||||
|
const session = await auth()
|
||||||
|
return session
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Session retrieval error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if user is authenticated
|
||||||
|
* Requirements: 2.4, 3.3 - Session expiration and validation
|
||||||
|
*/
|
||||||
|
export async function isAuthenticated(): Promise<boolean> {
|
||||||
|
const session = await getCurrentSession()
|
||||||
|
return !!session?.user
|
||||||
|
}
|
||||||
|
|||||||
69
middleware.ts
Normal file
69
middleware.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { auth } from "@/auth"
|
||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware for session validation and route protection
|
||||||
|
*
|
||||||
|
* This middleware:
|
||||||
|
* - Validates session tokens on protected routes
|
||||||
|
* - Redirects unauthenticated users to login
|
||||||
|
* - Handles session expiration
|
||||||
|
* - Allows access to auth pages for unauthenticated users
|
||||||
|
*/
|
||||||
|
export default auth((req) => {
|
||||||
|
const { nextUrl } = req
|
||||||
|
const isLoggedIn = !!req.auth?.user
|
||||||
|
|
||||||
|
// Define protected routes
|
||||||
|
const isProtectedRoute =
|
||||||
|
nextUrl.pathname === "/" ||
|
||||||
|
nextUrl.pathname.startsWith("/list") ||
|
||||||
|
nextUrl.pathname.startsWith("/profile") ||
|
||||||
|
nextUrl.pathname.startsWith("/invitations")
|
||||||
|
|
||||||
|
// Define auth pages (login, register, etc.)
|
||||||
|
const isAuthPage = nextUrl.pathname.startsWith("/auth")
|
||||||
|
|
||||||
|
// Allow access to auth pages regardless of authentication status
|
||||||
|
if (isAuthPage) {
|
||||||
|
// If already logged in and trying to access login/register, redirect to home
|
||||||
|
if (isLoggedIn && (nextUrl.pathname === "/auth/login" || nextUrl.pathname === "/auth/register")) {
|
||||||
|
return NextResponse.redirect(new URL("/", req.url))
|
||||||
|
}
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login if accessing protected route without authentication
|
||||||
|
if (isProtectedRoute && !isLoggedIn) {
|
||||||
|
const loginUrl = new URL("/auth/login", req.url)
|
||||||
|
// Add callback URL to redirect back after login
|
||||||
|
loginUrl.searchParams.set("callbackUrl", nextUrl.pathname)
|
||||||
|
return NextResponse.redirect(loginUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow access to public routes
|
||||||
|
return NextResponse.next()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher configuration for middleware
|
||||||
|
* Applies middleware to all routes except:
|
||||||
|
* - API routes (handled separately)
|
||||||
|
* - Static files (_next/static)
|
||||||
|
* - Image optimization (_next/image)
|
||||||
|
* - Favicon and other public assets
|
||||||
|
*/
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
* - public assets (images, icons, manifest, sw.js)
|
||||||
|
*/
|
||||||
|
"/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|json|js)$).*)",
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user