From b27da3f6e83abfcacabfc198a19d0d9a1c763310 Mon Sep 17 00:00:00 2001 From: m7amedez5511 Date: Sun, 23 Aug 2026 11:34:42 +0300 Subject: [PATCH] solve middleware problem and navigation problem --- middleware.ts | 120 +++++++++++++++++++++++++++++++++++ src/middleware/middleware.ts | 118 ---------------------------------- 2 files changed, 120 insertions(+), 118 deletions(-) create mode 100644 middleware.ts delete mode 100644 src/middleware/middleware.ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..7205dbf --- /dev/null +++ b/middleware.ts @@ -0,0 +1,120 @@ +// middleware.ts +// 1. Runs on the Edge runtime before any page component is rendered. +// 2. Handles three guards for the app: +// a. Authentication for all /dashboard/* routes — redirects to /login if +// no valid auth cookie is present. +// b. Role-based access control — blocks the "driver" role from admin routes. +// c. Keeps an authenticated user off the public home page — if a logged-in +// user manually navigates to "/" or "/home", they are sent back to +// /dashboard instead of letting the marketing/home page render. +// 3. IMPORTANT: this file must live at the project root (or as src/middleware.ts) +// for Next.js to auto-detect and execute it as middleware. It was previously +// located at src/middleware/middleware.ts, which Next.js does NOT recognize +// as the middleware entrypoint — so none of the guards below were ever +// actually running. That misplacement was the root cause of unauthenticated +// users being able to reach /dashboard/* pages directly, and of logged-in +// users being able to navigate back to "/" or "/home" unchecked. + +import { NextRequest, NextResponse } from "next/server"; + +// 4. The cookie name must match what the server-side login action sets +// (see app/api/auth/set-cookie/route.ts). +const AUTH_COOKIE_NAME = "auth_token"; + +// 5. Route groups this middleware cares about. +const PROTECTED_PREFIX = "/dashboard"; +const PUBLIC_HOME_PATHS = ["/", "/home"]; + +// 6. Lightweight JWT payload decoder. +// We only need the `role` claim — we do NOT verify the signature here +// because the backend already validates the token on every API request. +// Signature verification in middleware would require the secret to be +// bundled into the Edge runtime, which is its own security concern. +// The authoritative check is always the backend; middleware is a UX guard. +function decodeJwtPayload(token: string): Record | null { + try { + const parts = token.split("."); + if (parts.length !== 3) return null; + + // Base64url -> Base64 -> JSON + const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + // atob is available in the Edge runtime + const json = atob(base64); + return JSON.parse(json) as Record; + } catch { + return null; + } +} + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + const authCookie = request.cookies.get(AUTH_COOKIE_NAME); + const isAuthenticated = !!authCookie?.value && !!decodeJwtPayload(authCookie.value); + + // 7. Guard 1: public home ("/" and "/home"). An authenticated user must not + // be able to land back on the marketing/home page by manually editing the + // URL — bounce them to the dashboard instead. An unauthenticated visitor + // passes through normally. + if (PUBLIC_HOME_PATHS.includes(pathname)) { + if (isAuthenticated) { + return NextResponse.redirect(new URL("/dashboard", request.url)); + } + return NextResponse.next(); + } + + // 8. Guard 2: only the remaining checks apply to dashboard routes. + // (The `matcher` config below already limits execution, but this is an + // explicit check for clarity and defensive depth.) + if (!pathname.startsWith(PROTECTED_PREFIX)) { + return NextResponse.next(); + } + + // 9. Guard 3: authentication. No cookie at all -> redirect to /login, + // preserving the original URL so we can send the user back after + // a successful login. + if (!authCookie?.value) { + const loginUrl = new URL("/login", request.url); + loginUrl.searchParams.set("next", pathname); + return NextResponse.redirect(loginUrl); + } + + // 10. Decode the JWT payload (not verified — see decodeJwtPayload above). + const payload = decodeJwtPayload(authCookie.value); + + if (!payload) { + // 11. Malformed or expired token — treat exactly like "not authenticated". + const loginUrl = new URL("/login", request.url); + loginUrl.searchParams.set("next", pathname); + return NextResponse.redirect(loginUrl); + } + + // 12. Guard 4: role-based access control. The role field matches what + // lib/auth.ts stores: a plain string like "driver", "admin", "user" + // (extracted from the backend JWT during login). + const role = + typeof payload.role === "string" ? payload.role.toLowerCase() : null; + + const BLOCKED_ROLES = ["driver", "سائق"]; + + if (role && BLOCKED_ROLES.includes(role)) { + // 13. Driver accounts must never access the admin dashboard. + return NextResponse.redirect(new URL("/forbidden", request.url)); + } + + // 14. All checks passed — allow the request through. + return NextResponse.next(); +} + +export const config = { + // 15. Apply this middleware to the public home paths (so authenticated + // users get bounced off them) and to the dashboard root plus every + // sub-route (so unauthenticated visitors and blocked roles get + // redirected away from admin pages). + matcher: [ + "/", + "/home", + "/dashboard", + "/dashboard/:path*", + ], +}; \ No newline at end of file diff --git a/src/middleware/middleware.ts b/src/middleware/middleware.ts deleted file mode 100644 index 161be06..0000000 --- a/src/middleware/middleware.ts +++ /dev/null @@ -1,118 +0,0 @@ -// middleware.ts -// Runs on the Edge runtime before any page component is rendered. -// Handles two guards for all /dashboard/* routes (and their sub-routes): -// 1. Authentication — redirects to /login if no auth cookie is present. -// 2. Role-based access control — blocks the "driver" role from admin routes. - -import { NextRequest, NextResponse } from "next/server"; - -// The cookie name must match what the server-side login action sets. -// Previously lib/auth.ts wrote this as a JS-accessible cookie — after the -// auth.ts refactor (Issue 3) this same name is now an HttpOnly cookie. -const AUTH_COOKIE_NAME = "auth_token"; - -// Routes that require authentication (and block the driver role). -// The matcher below handles the routing; this constant is for documentation. -const PROTECTED_PREFIX = "/dashboard"; - -/** - * Lightweight JWT payload decoder. - * We only need the `role` claim — we do NOT verify the signature here - * because the backend already validates the token on every API request. - * Signature verification in middleware would require the secret to be - * bundled into the Edge runtime, which is its own security concern. - * The authoritative check is always the backend; middleware is a UX guard. - */ -function decodeJwtPayload(token: string): Record | null { - try { - const parts = token.split("."); - if (parts.length !== 3) return null; - - // Base64url → Base64 → JSON - const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - // atob is available in the Edge runtime - const json = atob(base64); - return JSON.parse(json) as Record; - } catch { - return null; - } -} - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // ── Guard: only apply to protected routes ───────────────────────────────── - // (The `matcher` config below already limits execution, but this is an - // explicit check for clarity and defensive depth.) - if (!pathname.startsWith(PROTECTED_PREFIX)) { - return NextResponse.next(); - } - - // ── Check 1: Authentication ──────────────────────────────────────────────── - // Read the HttpOnly auth cookie set by the server after login. - const authCookie = request.cookies.get(AUTH_COOKIE_NAME); - - if (!authCookie?.value) { - // No token → send to login, preserving the original URL so we can - // redirect back after successful login if needed. - const loginUrl = new URL("/login", request.url); - loginUrl.searchParams.set("next", pathname); - return NextResponse.redirect(loginUrl); - } - - // ── Check 2: Role-based access control ──────────────────────────────────── - // Decode the JWT payload (not verified — see note on decodeJwtPayload above). - const payload = decodeJwtPayload(authCookie.value); - - if (!payload) { - // Malformed token — treat as unauthenticated. - const loginUrl = new URL("/login", request.url); - return NextResponse.redirect(loginUrl); - } - - // The role field matches what lib/auth.ts stores: a plain string like - // "driver", "admin", "user" (extracted from the backend JWT during login). - const role = - typeof payload.role === "string" - ? payload.role.toLowerCase() - : null; - - const BLOCKED_ROLES = ["driver", "سائق"]; - - if (role && BLOCKED_ROLES.includes(role)) { - // Driver accounts must never access the admin dashboard. - return NextResponse.redirect(new URL("/forbidden", request.url)); - } - - // ── All checks passed — allow the request through ───────────────────────── - return NextResponse.next(); -} - -export const config = { - // Apply this middleware to the dashboard root and all sub-routes. - // Explicitly list the known sub-routes so the matcher is predictable; - // any new top-level pages added under /dashboard are automatically covered - // by the "/dashboard/:path*" pattern. - matcher: [ - "/dashboard", - "/dashboard/:path*", - "/users", - "/users/:path*", - "/cars", - "/cars/:path*", - "/orders", - "/orders/:path*", - "/clients", - "/clients/:path*", - "/drivers", - "/drivers/:path*", - "/roles", - "/roles/:path*", - "/audit", - "/audit/:path*", - "/trips", - "/trips/:path*", - "/branches", - "/branches/:path*", - ], -}; \ No newline at end of file