2026-06-11 15:36:44 +05:30
|
|
|
/**
|
|
|
|
|
* 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 = (
|
2026-07-17 00:47:32 +02:00
|
|
|
process.env.GATEWAY_URL || "http://localhost:9100"
|
2026-06-11 15:36:44 +05:30
|
|
|
).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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
feat: notifications page, auto-apply settings, customer response profiles, job status UI
- NotificationsPage: full paginated notification list with unread filter, mark read, load more
- SettingsPage: AI auto-apply section for job seekers (toggle, preferences, skills/titles/locations, salary range)
- CustomerResponsesPage: enriched professional response cards with avatar, bio, skills, location
- CompanyJobsPage: show rejection reason banner and pending-approval notice on job cards
- NotificationBell: fix "View all" link to /dashboard?nav=notifications (deep-link support)
- dashboard.tsx: ?nav= param reads sidebar page on mount; Notifications added to all role sidebars
- PayU integration: payu.ts lib, payu-return route, wallet buy/invoice pages, marketplace route
- Razorpay removed, replaced by PayU across payments flow
- ProfilePage: photo upload UI with avatar preview for all roles
- PortfolioPage: showcase image upload with file picker and preview
- CompanyApplicationsPage: applicant profile snapshot with avatar, headline, skills, resume download
- profile-fields-config: removed resume_doc from job seeker (resume is now AI-generated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:33:42 +02:00
|
|
|
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 },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:36:44 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
feat: notifications page, auto-apply settings, customer response profiles, job status UI
- NotificationsPage: full paginated notification list with unread filter, mark read, load more
- SettingsPage: AI auto-apply section for job seekers (toggle, preferences, skills/titles/locations, salary range)
- CustomerResponsesPage: enriched professional response cards with avatar, bio, skills, location
- CompanyJobsPage: show rejection reason banner and pending-approval notice on job cards
- NotificationBell: fix "View all" link to /dashboard?nav=notifications (deep-link support)
- dashboard.tsx: ?nav= param reads sidebar page on mount; Notifications added to all role sidebars
- PayU integration: payu.ts lib, payu-return route, wallet buy/invoice pages, marketplace route
- Razorpay removed, replaced by PayU across payments flow
- ProfilePage: photo upload UI with avatar preview for all roles
- PortfolioPage: showcase image upload with file picker and preview
- CompanyApplicationsPage: applicant profile snapshot with avatar, headline, skills, resume download
- profile-fields-config: removed resume_doc from job seeker (resume is now AI-generated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:33:42 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 00:47:32 +02:00
|
|
|
// All other /api/* paths — proxy to the gateway
|
|
|
|
|
return proxyToGateway(fetchEvent, path);
|
2026-06-11 15:36:44 +05:30
|
|
|
},
|
|
|
|
|
});
|