diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index 27e5a59..95b8aef 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -26,7 +26,7 @@ ## 5. Auth Middleware & Helpers -- [ ] 5.1 `[sonnet]` Create `proxy.ts` at project root: protect `/app/*` routes (redirect to `/login`), protect `/api/*` except `/api/auth/*` and `/api/health` (return 401), redirect authenticated users from `/login` and `/register` to `/app` +- [x] 5.1 `[sonnet]` Create `proxy.ts` at project root: protect `/app/*` routes (redirect to `/login`), protect `/api/*` except `/api/auth/*` and `/api/health` (return 401), redirect authenticated users from `/login` and `/register` to `/app` - [ ] 5.2 `[haiku]` Create `src/lib/auth.ts` with `getAuthUser()` helper that extracts user from Auth.js session ## 6. User Settings API diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..1509671 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,44 @@ +import { auth } from "@/auth"; +import { NextResponse } from "next/server"; + +export const middleware = auth((req) => { + const { pathname } = req.nextUrl; + const isAuthenticated = !!req.auth; + + // Protect /api/* except /api/auth/* and /api/health + if (pathname.startsWith("/api/")) { + const isAuthRoute = pathname.startsWith("/api/auth/"); + const isHealthRoute = pathname === "/api/health"; + + if (!isAuthRoute && !isHealthRoute && !isAuthenticated) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + return NextResponse.next(); + } + + // Redirect authenticated users away from /login and /register + if (isAuthenticated && (pathname === "/login" || pathname === "/register")) { + return NextResponse.redirect(new URL("/app", req.nextUrl.origin)); + } + + // Protect /app/* routes — redirect unauthenticated users to /login + if (pathname.startsWith("/app") && !isAuthenticated) { + return NextResponse.redirect(new URL("/login", req.nextUrl.origin)); + } + + return NextResponse.next(); +}); + +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimisation) + * - favicon.ico / favicon.png / favicon.svg + * - public assets (top-level files that are not pages) + */ + "/((?!_next/static|_next/image|favicon\\.ico|favicon\\.png|favicon\\.svg).*)", + ], +};