-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathmiddleware.ts
More file actions
101 lines (82 loc) · 2.65 KB
/
Copy pathmiddleware.ts
File metadata and controls
101 lines (82 loc) · 2.65 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
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/lib/jwt";
const REQUEST_START_HEADER = "x-request-start";
const REQUEST_ID_HEADER = "x-request-id";
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
// Auth endpoints & public endpoints must stay reachable without a token.
const PUBLIC_PATHS = [
"/api/auth/nonce",
"/api/auth/verify",
"/api/health",
"/api/ready",
"/api/lists",
"/api/schedules",
"/api/events",
"/api/contracts",
"/api/analytics",
"/api/stats",
"/api/streams",
"/api/addresses",
"/api/openapi",
"/api/profile",
];
const EXCLUDED_PATHS = ["/api/health", "/api/ready"];
function shouldExclude(pathname: string): boolean {
return EXCLUDED_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/"));
}
function generateRequestId(): string {
return crypto.randomUUID();
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (!pathname.startsWith("/api/")) {
return NextResponse.next();
}
if (shouldExclude(pathname)) {
return NextResponse.next();
}
// Echo client-sent X-Request-ID or generate a new one
const requestId =
request.headers.get(REQUEST_ID_HEADER) || generateRequestId();
const startMs = Date.now();
const requestHeaders = new Headers(request.headers);
requestHeaders.set(REQUEST_ID_HEADER, requestId);
requestHeaders.set(REQUEST_START_HEADER, String(startMs));
// If write method and not in public path, verify auth
if (
WRITE_METHODS.has(request.method) &&
!PUBLIC_PATHS.some((path) => pathname.startsWith(path))
) {
const authHeader = request.headers.get("authorization") || "";
const [scheme, token] = authHeader.split(" ");
if (scheme !== "Bearer" || !token) {
const unauthorizedResponse = NextResponse.json(
{ error: "Missing or invalid Authorization header" },
{ status: 401 }
);
unauthorizedResponse.headers.set(REQUEST_ID_HEADER, requestId);
return unauthorizedResponse;
}
const payload = verifyJWT(token);
if (!payload) {
const invalidTokenResponse = NextResponse.json(
{ error: "Invalid or expired token" },
{ status: 401 }
);
invalidTokenResponse.headers.set(REQUEST_ID_HEADER, requestId);
return invalidTokenResponse;
}
requestHeaders.set("x-wallet-address", payload.sub);
}
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Set X-Request-ID on the response so the client can correlate
response.headers.set(REQUEST_ID_HEADER, requestId);
return response;
}
export const config = {
matcher: "/api/:path*",
};