All files / src proxy.ts

100% Statements 112/112
100% Branches 12/12
100% Functions 2/2
100% Lines 112/112

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 1131x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 57x 3x 3x 3x 3x 3x 57x 57x 57x 1x 1x 57x 57x 26x 26x 26x 26x 26x 14x 14x 12x 12x 26x 26x 6x 6x 6x 26x 37x 37x 37x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth/auth";
 
interface SessionUser {
  id?: number;
  email?: string;
  name?: string;
  role?: string;
}
 
/**
 * Add security headers to the response
 */
function addSecurityHeaders(response: NextResponse): NextResponse {
  // Content Security Policy
  // Allows Stripe, inline styles (required by many React libraries), and self
  const cspDirectives = [
    "default-src 'self'",
    // Scripts: self, Stripe, and inline (needed for Next.js)
    "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com",
    // Styles: self and inline (needed for Tailwind and component libraries)
    "style-src 'self' 'unsafe-inline'",
    // Images: self, data URIs, blobs, and HTTPS sources
    "img-src 'self' blob: data: https:",
    // Fonts: self
    "font-src 'self' data:",
    // Connect: self, Stripe API, EmailJS
    "connect-src 'self' https://api.stripe.com https://api.emailjs.com",
    // Frames: Stripe (for payment elements)
    "frame-src https://js.stripe.com https://hooks.stripe.com",
    // Object: none (security best practice)
    "object-src 'none'",
    // Base URI: self only
    "base-uri 'self'",
    // Form actions: self only
    "form-action 'self'",
    // Prevent framing by other sites
    "frame-ancestors 'none'",
    // Upgrade insecure requests in production
    process.env.NODE_ENV === "production" ? "upgrade-insecure-requests" : "",
  ]
    .filter(Boolean)
    .join("; ");
 
  response.headers.set("Content-Security-Policy", cspDirectives);
 
  // Prevent MIME type sniffing
  response.headers.set("X-Content-Type-Options", "nosniff");
 
  // Prevent clickjacking
  response.headers.set("X-Frame-Options", "DENY");
 
  // Enable XSS protection (for older browsers)
  response.headers.set("X-XSS-Protection", "1; mode=block");
 
  // Control referrer information
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
 
  // Disable browser features we don't use
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=(), payment=(self)"
  );
 
  // Prevent cross-domain policies
  response.headers.set("X-Permitted-Cross-Domain-Policies", "none");
 
  // HSTS in production
  if (process.env.NODE_ENV === "production") {
    response.headers.set(
      "Strict-Transport-Security",
      "max-age=31536000; includeSubDomains; preload"
    );
  }
 
  return response;
}
 
export async function proxy(request: NextRequest) {
  // Check if request is to admin routes
  if (request.nextUrl.pathname.startsWith("/admin")) {
    // Get the session
    const session = await auth();
 
    // If not authenticated, redirect to login
    if (!session) {
      return addSecurityHeaders(NextResponse.redirect(new URL("/signin", request.url)));
    }
 
    // Check user role
    const userRole = (session.user as SessionUser)?.role;
    if (userRole !== "ADMIN") {
      // Redirect non-admin users to home page
      return addSecurityHeaders(NextResponse.redirect(new URL("/", request.url)));
    }
  }
 
  return addSecurityHeaders(NextResponse.next());
}
 
export const config = {
  matcher: [
    /*
     * Match all request paths except for:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public folder
     * - API webhooks (they need raw body for signature verification)
     */
    "/((?!_next/static|_next/image|favicon.ico|.*\\..*|api/webhooks).*)",
  ]};