-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
128 lines (103 loc) · 4.5 KB
/
middleware.ts
File metadata and controls
128 lines (103 loc) · 4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { validRoles } from "./constants/validRoles";
const roleRoutePrefixes: Record<string, string[]> = {
ADMIN: ["/dashboard/admin"],
OWNER: ["/dashboard/owner"],
MANAGER: ["/dashboard/manager"],
EMPLOYEE: ["/dashboard/employee"],
};
const allProtectedPrefixes = Object.values(roleRoutePrefixes).flat();
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const base64UrlToUint8Array = (base64Url: string) => {
// Normalize padding and replace URL-safe chars
const padded = base64Url.padEnd(base64Url.length + ((4 - (base64Url.length % 4)) % 4), "=").replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
const verifyJwt = async (jwt: string, secretKey: Uint8Array) => {
const [headerB64, payloadB64, signatureB64] = jwt.split(".");
if (!headerB64 || !payloadB64 || !signatureB64) {
throw new Error("Invalid JWT structure");
}
const data = `${headerB64}.${payloadB64}`;
const signature = base64UrlToUint8Array(signatureB64);
const cryptoKey = await crypto.subtle.importKey(
"raw",
secretKey,
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const verified = await crypto.subtle.verify("HMAC", cryptoKey, signature, textEncoder.encode(data));
if (!verified) {
throw new Error("Invalid JWT signature");
}
const payloadJson = textDecoder.decode(base64UrlToUint8Array(payloadB64));
return JSON.parse(payloadJson) as Record<string, unknown>;
};
const isRoleAllowedForPath = (role: string, pathname: string) => {
// Allow non-dashboard paths configured in matcher (profile/settings) for any logged-in role
if (pathname.startsWith("/profile") || pathname.startsWith("/settings")) { return true; }
// Only enforce when the path matches a known role prefix
const matchedPrefix = allProtectedPrefixes.find((prefix) => pathname.startsWith(prefix));
if (!matchedPrefix) { return true; }
const prefixes = roleRoutePrefixes[role];
if (!prefixes) { return false; }
return prefixes.some((prefix) => pathname.startsWith(prefix));
};
export async function middleware(request: NextRequest) {
//console.log(`Inside Middleware.ts`);
const jwt = request.cookies.get("accessToken")?.value;
const refreshToken = request.cookies.get("refreshToken")?.value;
if (!jwt && !refreshToken) {
//console.log("🚨 No tokens found. Redirecting to login.");
return NextResponse.redirect(new URL("/auth/login", request.url));
}
if (!jwt && refreshToken) {
//console.log("🔄 Access token missing, allowing frontend to refresh...");
return NextResponse.next(); // ✅ Let Axios handle refresh
}
try {
const secretEnv = process.env.JWT_SECRET || "";
// Edge runtime-safe decoding: try base64 first, fall back to UTF-8 bytes
const secretKey = (() => {
try {
const decoded = atob(secretEnv);
return Uint8Array.from(decoded, (char) => char.charCodeAt(0));
} catch {
return textEncoder.encode(secretEnv);
}
})();
const payload = await verifyJwt(jwt!, secretKey);
const role = payload["ROLE"] as string;
if (!validRoles.includes(role)) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Enforce role-based route access
const path = request.nextUrl.pathname;
if (!isRoleAllowedForPath(role, path)) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
//console.log("✅ Middleware validation passed.");
return NextResponse.next();
} catch (error) {
console.error("🚨 JWT verification failed:", error);
if (refreshToken) {
//console.log("🔄 JWT expired but refreshToken exists. Allowing frontend to refresh...");
return NextResponse.next(); // ✅ Let Axios handle it
}
const response = NextResponse.redirect(new URL("/auth/login", request.url));
response.cookies.delete("accessToken");
response.cookies.delete("refreshToken");
return response;
}
}
export const config = {
matcher: ["/dashboard/:path*", "/profile", "/settings"],
};