swadhinbiswas ♻️ refactor: update middleware.ts · 7/24/2026
5c45620
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
129
130
131
132
133
134
135
136
137
138
139
140
141
import { defineMiddleware } from "astro:middleware";
import { eq } from "drizzle-orm";
import { getDatabase, schema } from "./db";
import { getUserFromRequest } from "./lib/auth";
import { logger } from "./lib/logger";
import { httpRequestDurationMicroseconds } from "./lib/metrics";
import { applyCsrfProtection } from "./middleware/csrf";
import { createRateLimitMiddleware } from "./middleware/rate-limit";
import { isConfigured } from "./lib/config";
// Define tiers for different routes
const apiLimiter = createRateLimitMiddleware("api");
const authLimiter = createRateLimitMiddleware("auth");
// Routes that are exempt from CSRF (use their own auth: Bearer tokens, API keys, internal hooks)
const CSRF_EXEMPT_PREFIXES = [
"/api/internal/",
"/api/graphql", // Uses Bearer token auth
"/api/git/", // Uses Git protocol auth
"/api/auth/csrf-token", // The CSRF token endpoint itself
"/api/setup", // Setup wizard has no active sessions to protect
];
export const onRequest = defineMiddleware(async (context, next) => {
const { request, url } = context;
// ── Configuration Check ──
const configured = isConfigured();
const isSetupRoute = url.pathname === "/setup" || url.pathname.startsWith("/api/setup");
const isAssetRoute = url.pathname.startsWith("/_astro/") || url.pathname.startsWith("/assets/") || url.pathname.includes(".");
if (!configured && !isSetupRoute && !isAssetRoute) {
return context.redirect("/setup");
}
if (configured && url.pathname === "/setup") {
return context.redirect("/");
}
// ── Populate Astro.locals.user from session cookie or Bearer token ──
context.locals.user = null;
context.locals.session = null;
try {
const tokenPayload = await getUserFromRequest(request);
if (tokenPayload?.userId) {
const db = getDatabase();
const user = await db.query.users?.findFirst({
where: eq(schema.users.id, tokenPayload.userId),
});
if (user) {
context.locals.user = user;
}
// Populate session for logout and other session-aware handlers
if (tokenPayload.sessionId) {
const session = await db.query.sessions?.findFirst({
where: eq(schema.sessions.id, tokenPayload.sessionId),
});
if (session) {
context.locals.session = session;
}
}
}
} catch {
// Auth failures are non-fatal — user stays null (anonymous)
}
// Apply rate limiting to API routes
if (url.pathname.startsWith("/api/")) {
// Use stricter auth limits for auth routes
if (url.pathname.startsWith("/api/auth")) {
const response = await authLimiter(request, context);
if (response) return response;
} else {
const response = await apiLimiter(request, context);
if (response) return response;
}
// Apply CSRF protection on state-changing API requests
const method = request.method.toUpperCase();
const isMutating = !["GET", "HEAD", "OPTIONS"].includes(method);
const isExempt = CSRF_EXEMPT_PREFIXES.some((p) =>
url.pathname.startsWith(p),
);
// Bearer token requests are exempt only if the token is actually valid
// (prevents CSRF bypass via fake/invalid Bearer headers)
const authHeader = request.headers.get("authorization");
const hasValidBearerToken =
authHeader?.startsWith("Bearer ") && authHeader.length > 20;
if (isMutating && !isExempt && !hasValidBearerToken) {
const csrfResponse = await applyCsrfProtection(request);
if (csrfResponse) return csrfResponse;
}
}
// Continue to next middleware/route
const startTime = performance.now();
const response = await next();
const durationMs = performance.now() - startTime;
const durationSec = durationMs / 1000;
// Record request duration metric
// Prevent Prometheus cardinality explosion memory leak by genericizing route
let route = url.pathname
.replace(/\/[0-9a-fA-F-]{8,}/g, "/:id") // UUIDs
.replace(/\/(pulls|issues|commits)\/\d+/g, "/$1/:id") // PRs and Issues
.replace(/^\/api\/repos\/[^/]+\/[^/]+/, "/api/repos/:owner/:repo") // API Repo paths
.replace(/^\/[^/]+\/[^/]+\/(pulls|issues|settings|actions|wiki)/, "/:owner/:repo/$1"); // UI Repo paths
httpRequestDurationMicroseconds
.labels(request.method, route, String(response.status))
.observe(durationSec);
// Log slow requests (>500ms)
if (durationMs > 500) {
logger.warn(
{
method: request.method,
path: url.pathname,
status: response.status,
durationMs: Math.round(durationMs),
},
"Slow request detected",
);
}
// Add Server-Timing header for observability
response.headers.set("Server-Timing", `total;dur=${durationMs.toFixed(1)}`);
// Add Content-Security-Policy header
if (!response.headers.has("Content-Security-Policy")) {
response.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https: blob:; connect-src 'self' https:;",
);
}
return response;
});