/ opencodehub / src / middleware.ts
Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
110 lines · 3.7 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
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";

// 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
];

export const onRequest = defineMiddleware(async (context, next) => {
  const { request, url } = context;

  // ── 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;
      }
    }
  } 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 (stateless API auth)
    const hasBearerToken = request.headers
      .get("authorization")
      ?.startsWith("Bearer ");

    if (isMutating && !isExempt && !hasBearerToken) {
      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
  const route = url.pathname.replace(/\/[0-9a-f-]{8,}/g, "/:id");
  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'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'",
    );
  }

  return response;
});