nxtgauge-frontend-solid/src/middleware.ts

210 lines
6.8 KiB
TypeScript
Raw Normal View History

/**
* 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<string, string> = {
"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<Response> {
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;
},
});