/** * SolidStart server middleware (runs on every request). * * Workaround for a Vinxi 0.5.7 + @solidjs/start 1.3.2 build issue where * file-based API routes in `src/routes/api/*` are registered in the page * router tree but never mounted as Nitro handlers, so every `/api/*` * request returns a 404 from the SolidStart page renderer. * * This middleware intercepts `/api/*` paths at the SolidStart middleware * layer (which IS in the request pipeline) and proxies them to the Rust * gateway. * * Responsibilities: * - /api/gateway/*path → proxy to Rust gateway * - /api/kb/categories → proxy to Rust gateway * - /api/kb/articles → proxy to Rust gateway * - /api/kb/articles/:slug → proxy to Rust gateway * * Uses the @solidjs/start `createMiddleware` pattern, which Vinxi/h3 will * actually invoke via the `onRequest` hook. */ import { createMiddleware } from "@solidjs/start/middleware"; const GATEWAY_URL = ( process.env.GATEWAY_URL || "http://nxtgauge-rust-gateway:9100" ).replace(/\/+$/, ""); const PUBLIC_API_URL = ( process.env.PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL || `${GATEWAY_URL}/api` ).replace(/\/+$/, ""); function buildUpstream(path: string, query: string = ""): string { // PUBLIC_API_URL ends with /api; path starts with /api/... // Strip the /api prefix from path so we don't double up. if (PUBLIC_API_URL.endsWith("/api")) { const stripped = path.replace(/^\/api/, ""); return `${PUBLIC_API_URL}${stripped}${query}`; } return `${PUBLIC_API_URL}${path}${query}`; } async function proxyToGateway(fetchEvent: any, upstreamPath: string) { const req = fetchEvent.request; const method = req.method.toUpperCase(); const url = new URL(req.url); const queryString = url.search || ""; // Read body for methods that have one let body: BodyInit | undefined; if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { try { body = await req.clone().text(); } catch { body = undefined; } } // Forward auth + content-type + cookie const headers: Record = { "Content-Type": req.headers.get("content-type") || "application/json", }; const auth = req.headers.get("authorization"); if (auth) headers["Authorization"] = auth; const cookie = req.headers.get("cookie"); if (cookie) headers["Cookie"] = cookie; const upstream = buildUpstream(upstreamPath, queryString); let response: Response; try { response = await fetch(upstream, { method, headers, body, cache: "no-store", }); } catch (err: any) { return new Response( JSON.stringify({ success: false, error: `Gateway unreachable: ${err?.message || "unknown"}`, }), { status: 502, headers: { "Content-Type": "application/json" }, }, ); } // Copy response headers (skip hop-by-hop), ensure Content-Type const respHeaders = new Headers(); response.headers.forEach((value, key) => { const k = key.toLowerCase(); if (k === "server" || k === "transfer-encoding" || k === "connection") return; respHeaders.set(key, value); }); if (!respHeaders.get("content-type")) { respHeaders.set("Content-Type", "application/json"); } const respBody = await response.text(); return new Response(respBody, { status: response.status, statusText: response.statusText, headers: respHeaders, }); } async function handlePayuReturn(fetchEvent: any): Promise { const req = fetchEvent.request; const url = new URL(req.url); const isFailure = url.pathname.endsWith("/payu-return-failure"); let formBody: URLSearchParams; try { const text = await req.text(); formBody = new URLSearchParams(text); } catch { formBody = new URLSearchParams(); } const txnid = formBody.get("txnid") || url.searchParams.get("txnid") || ""; const status = formBody.get("status") || (isFailure ? "failure" : url.searchParams.get("status") || "failure"); const mihpayid = formBody.get("mihpayid") || ""; const hash = formBody.get("hash") || ""; const amount = formBody.get("amount") || ""; const productinfo = formBody.get("productinfo") || ""; const firstname = formBody.get("firstname") || ""; const email = formBody.get("email") || ""; const phone = formBody.get("phone") || ""; const udf1 = formBody.get("udf1") || ""; const udf2 = formBody.get("udf2") || ""; const udf3 = formBody.get("udf3") || ""; const udf4 = formBody.get("udf4") || ""; const udf5 = formBody.get("udf5") || ""; const query = new URLSearchParams(); query.set("txnid", txnid); query.set("status", status); if (mihpayid) query.set("mihpayid", mihpayid); if (hash) query.set("hash", hash); if (amount) query.set("amount", amount); if (productinfo) query.set("productinfo", productinfo); if (firstname) query.set("firstname", firstname); if (email) query.set("email", email); if (phone) query.set("phone", phone); if (udf1) query.set("udf1", udf1); if (udf2) query.set("udf2", udf2); if (udf3) query.set("udf3", udf3); if (udf4) query.set("udf4", udf4); if (udf5) query.set("udf5", udf5); const target = `/dashboard/wallet/payu-return?${query.toString()}`; return new Response(null, { status: 302, headers: { Location: target }, }); } export default createMiddleware({ onRequest: async (fetchEvent) => { const url = new URL(fetchEvent.request.url); const path = url.pathname; // Only handle /api/* paths if (!path.startsWith("/api/")) return; // Gateway proxy catch-all: /api/gateway/* → strip /api/gateway, send rest if (path === "/api/gateway" || path.startsWith("/api/gateway/")) { const subPath = path.slice("/api/gateway".length) || "/"; // Normalize to /api/... contract for the Rust gateway const normalized = subPath.startsWith("/api/") || subPath === "/api" ? subPath : `/api${subPath}`; return proxyToGateway(fetchEvent, normalized); } // Knowledge base routes if (path === "/api/kb/categories") { return proxyToGateway(fetchEvent, "/api/kb/categories"); } if (path === "/api/kb/articles") { return proxyToGateway(fetchEvent, "/api/kb/articles"); } if (path.startsWith("/api/kb/articles/")) { return proxyToGateway(fetchEvent, path); } // PayU form POST return handler. PayU submits an HTML form to either the // success or failure URL with all transaction data in the request body. // We read the form fields, forward them to the verify endpoint, and // redirect the user to the SPA payu-return page with a query string. if (path === "/api/payments/payu-return") { return handlePayuReturn(fetchEvent); } // Everything else under /api/* — let it fall through. // Returning undefined tells the framework to continue. return; }, });