forked from Swalla-Enterprise-Solutions/Decentralized-Ajo
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
109 lines (92 loc) · 4.27 KB
/
Copy pathmiddleware.ts
File metadata and controls
109 lines (92 loc) · 4.27 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
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { checkRateLimit, RATE_LIMITS, getRateLimitKey } from '@/lib/rate-limit';
const ALLOWED_METHODS = 'GET,POST,PUT,DELETE,OPTIONS';
const ALLOWED_HEADERS = 'Content-Type,Authorization,x-request-id';
/** Origins allowed to call backend operations. Evaluated once at cold-start. */
const allowedOrigins: ReadonlySet<string> = new Set(
[
process.env.FRONTEND_URL,
'http://localhost:3000',
].filter(Boolean) as string[],
);
export function middleware(request: NextRequest) {
const requestId = crypto.randomUUID();
const startTime = Date.now();
const { method, nextUrl } = request;
const origin = request.headers.get('origin') ?? '';
// ── CORS enforcement ────────────────────────────────────────────────────────
// Only run origin check when an Origin header is present (i.e. cross-origin
// browser requests). Server-to-server calls without Origin are unaffected.
if (origin && !allowedOrigins.has(origin)) {
return NextResponse.json(
{ error: 'Origin not allowed' },
{ status: 403 },
);
}
// Handle preflight (OPTIONS) immediately — no further processing needed.
if (method === 'OPTIONS') {
const preflight = new NextResponse(null, { status: 204 });
if (origin) preflight.headers.set('Access-Control-Allow-Origin', origin);
preflight.headers.set('Access-Control-Allow-Methods', ALLOWED_METHODS);
preflight.headers.set('Access-Control-Allow-Headers', ALLOWED_HEADERS);
preflight.headers.set('Access-Control-Allow-Credentials', 'true');
preflight.headers.set('Access-Control-Max-Age', '86400');
return preflight;
}
// ── Rate Limiting ───────────────────────────────────────────────────────────
if (nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ?? '127.0.0.1';
// Choose rate limit config based on path
const isAuthRoute = nextUrl.pathname.startsWith('/api/auth/');
const config = isAuthRoute ? RATE_LIMITS.auth : RATE_LIMITS.api;
const prefix = isAuthRoute ? 'auth' : 'api';
const limitKey = getRateLimitKey(prefix, ip);
const limitResult = checkRateLimit(limitKey, config);
if (limitResult) {
console.warn(JSON.stringify({
requestId,
method,
url: nextUrl.pathname,
type: 'rate_limit_exceeded',
ip,
retryAfter: limitResult.retryAfter
}));
return NextResponse.json(
{
error: 'Too many requests; please try again later.',
retryAfter: limitResult.retryAfter
},
{
status: 429,
headers: {
'Retry-After': limitResult.retryAfter.toString(),
}
},
);
}
}
// ── Request logging ─────────────────────────────────────────────────────────
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-request-id', requestId);
console.log(JSON.stringify({ requestId, method, url: nextUrl.pathname, type: 'request' }));
const response = NextResponse.next({ request: { headers: requestHeaders } });
const duration = Date.now() - startTime;
console.log(
JSON.stringify({ requestId, method, url: nextUrl.pathname, status: response.status, duration: `${duration}ms`, type: 'response' }),
);
// ── CORS response headers ───────────────────────────────────────────────────
response.headers.set('x-request-id', requestId);
if (origin) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
response.headers.set('Access-Control-Allow-Methods', ALLOWED_METHODS);
response.headers.set('Vary', 'Origin');
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};