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*",
};