solve middleware problem and navigation problem
This commit is contained in:
120
middleware.ts
Normal file
120
middleware.ts
Normal file
@@ -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<string, unknown> | 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<string, unknown>;
|
||||||
|
} 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*",
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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<string, unknown> | 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<string, unknown>;
|
|
||||||
} 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*",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user