Spaces:
Sleeping
Sleeping
File size: 4,131 Bytes
8e23875 fa14516 8e23875 db8667c 8e23875 fa14516 8e23875 db8667c fa14516 8e23875 fa14516 8e23875 | 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 | 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*",
};
|