Spaces:
Sleeping
Sleeping
| import { NextResponse } from "next/server"; | |
| import type { NextRequest } from "next/server"; | |
| import { isPublicRoute, requiresAuth, getRequiredRole } from "@/lib/routeConfig"; | |
| // CORS configuration | |
| const allowedOrigins = [ | |
| "http://localhost:5173", // Vite dev | |
| "http://localhost:3000", // Next.js dev | |
| "https://open-triage.vercel.app", | |
| "https://opentriage.onrender.com", | |
| "https://open-triage-6r2y.vercel.app", // Specific preview deployment | |
| ]; | |
| // JWT Secret for token verification | |
| const JWT_SECRET = process.env.JWT_SECRET; | |
| /** | |
| * Verify JWT token and extract payload | |
| */ | |
| function verifyToken(token: string): { user_id: string; role: string | null } | null { | |
| try { | |
| // Simple JWT verification (base64 decode and verify signature) | |
| const parts = token.split('.'); | |
| if (parts.length !== 3) return null; | |
| const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); | |
| // Check expiration | |
| if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) { | |
| return null; | |
| } | |
| return { | |
| user_id: payload.user_id, | |
| role: payload.role || null | |
| }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function middleware(request: NextRequest) { | |
| const origin = request.headers.get("origin") || ""; | |
| // Allow any Vercel preview deployment or listed origins | |
| const isAllowedOrigin = allowedOrigins.includes(origin) || origin.endsWith(".vercel.app"); | |
| const pathname = request.nextUrl.pathname; | |
| // Handle preflight requests | |
| if (request.method === "OPTIONS") { | |
| return new NextResponse(null, { | |
| status: 204, | |
| headers: { | |
| "Access-Control-Allow-Origin": isAllowedOrigin ? origin : "", | |
| "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", | |
| "Access-Control-Allow-Headers": "Content-Type, Authorization", | |
| "Access-Control-Max-Age": "86400", | |
| }, | |
| }); | |
| } | |
| // Check if route requires authentication | |
| if (requiresAuth(pathname)) { | |
| const authHeader = request.headers.get("Authorization"); | |
| if (!authHeader || !authHeader.startsWith("Bearer ")) { | |
| return NextResponse.json( | |
| { error: "Unauthorized", message: "Authentication required" }, | |
| { | |
| status: 401, | |
| headers: { | |
| "Access-Control-Allow-Origin": isAllowedOrigin ? origin : "", | |
| "Access-Control-Allow-Credentials": "true", | |
| } | |
| } | |
| ); | |
| } | |
| const token = authHeader.substring(7); | |
| const payload = verifyToken(token); | |
| if (!payload) { | |
| return NextResponse.json( | |
| { error: "Unauthorized", message: "Invalid or expired token" }, | |
| { | |
| status: 401, | |
| headers: { | |
| "Access-Control-Allow-Origin": isAllowedOrigin ? origin : "", | |
| "Access-Control-Allow-Credentials": "true", | |
| } | |
| } | |
| ); | |
| } | |
| // Check role-based access | |
| const requiredRole = getRequiredRole(pathname); | |
| if (requiredRole && payload.role !== requiredRole) { | |
| return NextResponse.json( | |
| { error: "Forbidden", message: `${requiredRole} role required` }, | |
| { | |
| status: 403, | |
| headers: { | |
| "Access-Control-Allow-Origin": isAllowedOrigin ? origin : "", | |
| "Access-Control-Allow-Credentials": "true", | |
| } | |
| } | |
| ); | |
| } | |
| } | |
| // Handle actual requests | |
| const response = NextResponse.next(); | |
| if (isAllowedOrigin) { | |
| response.headers.set("Access-Control-Allow-Origin", origin); | |
| response.headers.set("Access-Control-Allow-Credentials", "true"); | |
| } | |
| return response; | |
| } | |
| // Apply middleware to all API routes | |
| export const config = { | |
| matcher: "/api/:path*", | |
| }; | |