diff --git a/.kiro/specs/user-auth-and-sharing/tasks.md b/.kiro/specs/user-auth-and-sharing/tasks.md index f318f5f..d46b724 100644 --- a/.kiro/specs/user-auth-and-sharing/tasks.md +++ b/.kiro/specs/user-auth-and-sharing/tasks.md @@ -56,7 +56,7 @@ - **Property 7: Invalid credentials rejection** - **Validates: Requirements 2.2** -- [ ] 5. Implement session management +- [x] 5. Implement session management - Configure JWT session strategy in NextAuth - Implement session validation middleware - Create logout functionality diff --git a/auth.ts b/auth.ts index 4c05fae..c5f2396 100644 --- a/auth.ts +++ b/auth.ts @@ -13,6 +13,8 @@ const loginSchema = z.object({ export const { handlers, signIn, signOut, auth } = NextAuth({ session: { strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days in seconds + updateAge: 24 * 60 * 60, // Update session every 24 hours }, providers: [ Credentials({ @@ -57,18 +59,27 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }), ], callbacks: { - async jwt({ token, user }) { + async jwt({ token, user, trigger }) { // Add user ID to token on sign in if (user) { 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 }, async session({ session, token }) { // Add user ID to session from token - if (session.user) { + if (session.user && token.id) { 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 }, }, diff --git a/lib/auth-actions.ts b/lib/auth-actions.ts index af7f78c..1948446 100644 --- a/lib/auth-actions.ts +++ b/lib/auth-actions.ts @@ -3,7 +3,7 @@ import { prisma } from './db' import bcrypt from 'bcryptjs' import { z } from 'zod' -import { signIn } from '@/auth' +import { signIn, signOut } from '@/auth' // Validation schemas const registerSchema = z.object({ @@ -132,3 +132,44 @@ export async function loginUser( 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 { + 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 { + const session = await getCurrentSession() + return !!session?.user +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..0040ca2 --- /dev/null +++ b/middleware.ts @@ -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)$).*)", + ], +}