Compare commits

..

4 commits

Author SHA1 Message Date
Ashwin Kumar Sivakumar
e63f47195e fix: use dind service for forgejo builds
Some checks failed
build-and-push / build (push) Failing after 29s
2026-06-12 22:09:56 +05:30
Ashwin Kumar Sivakumar
bfdea41bd1 fix: install docker cli in forgejo workflow namespace
Some checks failed
build-and-push / build (push) Failing after 21s
2026-06-12 22:08:11 +05:30
Ashwin Kumar Sivakumar
9c472ce9a5 fix: add main forgejo sync workflow 2026-06-12 22:00:25 +05:30
Ashwin Kumar Sivakumar
6887e64ddb fix: restore forgejo build workflow filename 2026-06-12 21:58:20 +05:30
30 changed files with 300 additions and 231 deletions

View file

@ -1,13 +0,0 @@
#!/busybox/sh
set -eu
mkdir -p /kaniko/.docker
cat > /kaniko/.docker/config.json <<JSON
{"auths":{"${REGISTRY_HOSTPORT}":{"username":"${REGISTRY_USERNAME}","password":"${REGISTRY_PASSWORD}"}}}
JSON
/kaniko/executor \
--context "${GITHUB_WORKSPACE}" \
--dockerfile "${GITHUB_WORKSPACE}/Dockerfile" \
--destination "${REGISTRY_HOSTPORT}/${IMAGE_NAME}:${COMMIT_SHA}" \
--destination "${REGISTRY_HOSTPORT}/${IMAGE_NAME}:${LATEST_TAG}"

View file

@ -9,22 +9,73 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
services:
docker:
image: docker:27-dind
env:
DOCKER_TLS_CERTDIR: ""
options: --privileged
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Build and push - name: Install Docker CLI
uses: docker://gcr.io/kaniko-project/executor:v1.23.2-debug run: |
apt-get update
apt-get install -y docker.io
- name: Set up Docker Buildx
run: |
export DOCKER_HOST=tcp://docker:2375
docker version
docker buildx create --use || true
docker buildx inspect --bootstrap
- name: Login to Registry
env: env:
REGISTRY_HOSTPORT: ${{ secrets.REGISTRY_HOSTPORT }} REGISTRY_HOSTPORT: ${{ secrets.REGISTRY_HOSTPORT }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
IMAGE_NAME: nxtgauge-frontend-solid run: |
LATEST_TAG: high-performance-latest set -euo pipefail
COMMIT_SHA: ${{ github.sha }} export DOCKER_HOST=tcp://docker:2375
with: SHA="$(git rev-parse HEAD)"
entrypoint: /busybox/sh test -n "$REGISTRY_HOSTPORT"
args: ${{ github.workspace }}/.forgejo/scripts/kaniko-build.sh echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOSTPORT" -u "$REGISTRY_USERNAME" --password-stdin
- name: Build and push
env:
REGISTRY_HOSTPORT: ${{ secrets.REGISTRY_HOSTPORT }}
run: |
set -euo pipefail
export DOCKER_HOST=tcp://docker:2375
SHA="$(git rev-parse HEAD)"
build_and_push() {
docker buildx build --push \
-f Dockerfile \
-t "$REGISTRY_HOSTPORT/nxtgauge-frontend-solid:${SHA}" \
-t "$REGISTRY_HOSTPORT/nxtgauge-frontend-solid:high-performance-latest" \
.
}
for attempt in 1 2 3; do
echo "Build attempt $attempt"
if build_and_push; then
exit 0
fi
echo "Build attempt $attempt failed; recreating builder and retrying"
docker buildx rm --all-inactive --force || true
docker buildx create --use || true
docker buildx inspect --bootstrap
sleep $((attempt * 10))
done
echo "Build failed after retries"
exit 1
- name: Prune old image tags (keep latest 1 SHA) - name: Prune old image tags (keep latest 1 SHA)
if: success() if: success()

View file

@ -11,7 +11,7 @@ Usage:
This script: This script:
1. Updates the newTag for the specified service to the SHA 1. Updates the newTag for the specified service to the SHA
2. Commits and pushes to the gitops repo 2. Commits and pushes to the gitops repo
3. Flux detects the change and deploys 3. ArgoCD detects the change and deploys
""" """
import argparse import argparse
@ -98,14 +98,10 @@ def main():
image_name = f"nxtgauge-{args.service}" image_name = f"nxtgauge-{args.service}"
# Find the right kustomization file based on service # Find the right kustomization file based on service
if "frontend" in args.service: if "frontend" in args.service or "admin" in args.service:
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/overlays/prod/kustomization.yaml") kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/overlays/prod/kustomization.yaml")
if not os.path.exists(kustomization_path): if not os.path.exists(kustomization_path):
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/base/kustomization.yaml") kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/base/kustomization.yaml")
elif "admin" in args.service:
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-admin-solid/overlays/prod/kustomization.yaml")
if not os.path.exists(kustomization_path):
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-admin-solid/base/kustomization.yaml")
elif "ai-assistant" in args.service: elif "ai-assistant" in args.service:
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-ai-assistant/overlays/prod/kustomization.yaml") kustomization_path = os.path.join(args.repo, "apps/nxtgauge-ai-assistant/overlays/prod/kustomization.yaml")
if not os.path.exists(kustomization_path): if not os.path.exists(kustomization_path):

46
.github/workflows/sync-to-gitea.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: sync-to-gitea
on:
push:
branches:
- high-performance
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sync to Gitea
env:
GITEA_TOKEN: ${{ secrets.GITEA_SECRET }}
REPO: ${{ github.event.repository.name }}
BRANCH: ${{ github.ref_name }}
run: |
set -euxo pipefail
export GIT_TERMINAL_PROMPT=0
export GIT_TRACE=1
export GIT_CURL_VERBOSE=1
USER="Admin"
TARGET="https://ci.nxtgauge.com/Admin/${REPO}.git"
AUTH="$(printf '%s' "${USER}:${GITEA_TOKEN}" | base64 -w0)"
test -n "${GITEA_TOKEN:-}" || (echo "GITEA_TOKEN empty" && exit 1)
curl -fsS -H "Authorization: token ${GITEA_TOKEN}" https://ci.nxtgauge.com/api/v1/user >/dev/null
curl -fsS -H "Authorization: Basic ${AUTH}" "${TARGET}/info/refs?service=git-receive-pack" >/dev/null
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global http.version HTTP/1.1
git config --global http.postBuffer 524288000
git remote remove gitea 2>/dev/null || true
git remote add gitea "${TARGET}"
git -c http.extraheader="Authorization: Basic ${AUTH}" push gitea "HEAD:${BRANCH}" --force
git -c http.extraheader="Authorization: Basic ${AUTH}" push gitea --tags --force

View file

@ -12,6 +12,3 @@ See `docs/MIGRATION_MASTER_PLAN.md` for the staged plan.
Required secrets: Required secrets:
- `REGISTRY_USERNAME` - `REGISTRY_USERNAME`
- `REGISTRY_PASSWORD` - `REGISTRY_PASSWORD`
# Mon Jun 8 09:06:25 PM IST 2026
# Mon Jun 8 09:16:18 PM IST 2026

View file

@ -102,9 +102,9 @@ Visual tests compare screenshots against baselines in `tests/e2e/visual/`.
## CI / Nightly Runs ## CI / Nightly Runs
GitHub Actions runs tests nightly via `.forgejo/workflows/test.yaml`: GitHub Actions runs tests nightly via `.gitea/workflows/test.yaml`:
- **2:30 AM daily** — all test suites - **2:30 AM daily** — all test suites
- **On-demand** — use `workflow_dispatch` trigger in Forgejo - **On-demand** — use `workflow_dispatch` trigger in Gitea
Artifacts are uploaded: Artifacts are uploaded:
- `vitest-coverage/` — coverage reports - `vitest-coverage/` — coverage reports

View file

@ -21,52 +21,53 @@ export default function CaptchaCanvas(props: CaptchaCanvasProps) {
const width = 176; const width = 176;
const height = 52; const height = 52;
const dpr = typeof window !== 'undefined' ? Math.max(1, window.devicePixelRatio || 1) : 1;
// Set canvas resolution (fixed at 1x to prevent zoom issues) // Set canvas resolution first (before any drawing)
canvas.width = width; canvas.width = Math.floor(width * dpr);
canvas.height = height; canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Clear and fill background // Clear and fill background
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw decorative lines (within bounds) // Draw decorative lines
for (let i = 0; i < 2; i += 1) { for (let i = 0; i < 2; i += 1) {
ctx.strokeStyle = i % 2 === 0 ? 'rgba(253,98,22,0.16)' : 'rgba(27,36,64,0.14)'; ctx.strokeStyle = i % 2 === 0 ? 'rgba(253,98,22,0.16)' : 'rgba(27,36,64,0.14)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(Math.random() * width, Math.random() * height); ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * width, Math.random() * height); ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke(); ctx.stroke();
} }
// Draw decorative circles (within bounds) // Draw decorative circles
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
ctx.fillStyle = i % 2 === 0 ? 'rgba(253,98,22,0.10)' : 'rgba(27,36,64,0.09)'; ctx.fillStyle = i % 2 === 0 ? 'rgba(253,98,22,0.10)' : 'rgba(27,36,64,0.09)';
ctx.beginPath(); ctx.beginPath();
ctx.arc(Math.random() * width, Math.random() * height, Math.random() * 1.8 + 0.6, 0, Math.PI * 2); ctx.arc(Math.random() * canvas.width, Math.random() * canvas.height, Math.random() * 1.8 + 0.6, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
// Draw characters (fixed positioning) // Draw characters
const chars = String(props.code || '').slice(0, 6).split(''); const chars = String(props.code || '').slice(0, 6).split('');
const startX = 16; const startX = 16 * dpr;
const charGap = 24; const charGap = 24 * dpr;
chars.forEach((char, index) => { chars.forEach((char, index) => {
const x = startX + index * charGap; const x = startX + index * charGap;
const y = height / 2; const y = canvas.height / 2;
const rotation = 0; const rotation = 0;
ctx.save(); ctx.save();
ctx.translate(x, y); ctx.translate(x, y);
ctx.rotate(rotation); ctx.rotate(rotation);
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `800 22px "Courier New", monospace`; ctx.font = `800 ${22 * dpr}px "Courier New", monospace`;
ctx.fillStyle = index % 2 === 0 ? '#0f172a' : '#c2410c'; ctx.fillStyle = index % 2 === 0 ? '#0f172a' : '#c2410c';
ctx.lineWidth = 0; ctx.lineWidth = 0;
ctx.fillText(char, 0, 0); ctx.fillText(char, 0, 0);

View file

@ -98,7 +98,7 @@ export default function DashboardLayout(props: ParentProps) {
const token = sessionStorage.getItem("nxtgauge_access_token"); const token = sessionStorage.getItem("nxtgauge_access_token");
if (token) { if (token) {
try { try {
const res = await fetch("/api/gateway/auth/session", { const res = await fetch("/api/auth/session", {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View file

@ -115,7 +115,7 @@ export default function RoleLandingPage(props: Props) {
return item; return item;
}); });
const canonical = createMemo(() => `https://test111.nxtgauge.com${props.pathBase}/${encodeURIComponent(String(props.slug || ''))}`); const canonical = createMemo(() => `https://test121.nxtgauge.com${props.pathBase}/${encodeURIComponent(String(props.slug || ''))}`);
const pageTitle = createMemo(() => (content() ? `${content()!.shortTitle} | Nxtgauge` : 'Role | Nxtgauge')); const pageTitle = createMemo(() => (content() ? `${content()!.shortTitle} | Nxtgauge` : 'Role | Nxtgauge'));
const pageDescription = createMemo(() => const pageDescription = createMemo(() =>
content() ? `${content()!.heroDescription} Most role reviews complete in 24-48 hours.` : 'Role landing page on Nxtgauge.' content() ? `${content()!.heroDescription} Most role reviews complete in 24-48 hours.` : 'Role landing page on Nxtgauge.'

View file

@ -1054,7 +1054,7 @@ export default function DashboardDesignPreview(props: {
try { try {
const token = getToken(); const token = getToken();
if (!token) return; if (!token) return;
const res = await fetch('/api/gateway/me/notifications/unread-count', { const res = await fetch('/api/me/notifications/unread-count', {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
credentials: 'include', credentials: 'include',
}); });

View file

@ -267,14 +267,14 @@ export default function MyDashboardPage(props: Props) {
try { try {
if (roleKey === 'COMPANY') { if (roleKey === 'COMPANY') {
const [jobsRes, appsRes] = await Promise.all([ const [jobsRes, appsRes] = await Promise.all([
fetch('/api/gateway/companies/jobs?page=1&limit=100', { fetch('/api/companies/jobs?page=1&limit=100', {
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${window.sessionStorage.getItem('nxtgauge_access_token') || ''}`, Authorization: `Bearer ${window.sessionStorage.getItem('nxtgauge_access_token') || ''}`,
}, },
}), }),
fetch('/api/gateway/companies/jobs?page=1&limit=1', { fetch('/api/companies/jobs?page=1&limit=1', {
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -293,7 +293,7 @@ export default function MyDashboardPage(props: Props) {
); );
if (!jobsRes.ok && !appsRes.ok) setErr('Some company metrics could not be loaded.'); if (!jobsRes.ok && !appsRes.ok) setErr('Some company metrics could not be loaded.');
} else if (roleKey === 'CUSTOMER') { } else if (roleKey === 'CUSTOMER') {
const res = await fetch('/api/gateway/customers/requirements?page=1&limit=100', { const res = await fetch('/api/customers/requirements?page=1&limit=100', {
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -311,14 +311,14 @@ export default function MyDashboardPage(props: Props) {
if (!res.ok) setErr('Some customer metrics could not be loaded.'); if (!res.ok) setErr('Some customer metrics could not be loaded.');
} else if (roleKey === 'JOB_SEEKER') { } else if (roleKey === 'JOB_SEEKER') {
const [jobsRes, appsRes] = await Promise.all([ const [jobsRes, appsRes] = await Promise.all([
fetch('/api/gateway/jobseeker/jobs?page=1&limit=100', { fetch('/api/jobseeker/jobs?page=1&limit=100', {
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${window.sessionStorage.getItem('nxtgauge_access_token') || ''}`, Authorization: `Bearer ${window.sessionStorage.getItem('nxtgauge_access_token') || ''}`,
}, },
}), }),
fetch('/api/gateway/jobseeker/applications?page=1&limit=100', { fetch('/api/jobseeker/applications?page=1&limit=100', {
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -82,7 +82,7 @@ async function fetchSession(): Promise<AuthUser | null> {
const token = getToken(); const token = getToken();
if (!token) return null; if (!token) return null;
try { try {
const res = await fetch("/api/gateway/auth/session", { const res = await fetch("/api/auth/session", {
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View file

@ -42,7 +42,7 @@ export async function fetchHelpCenterArticles(input: {
// Fallback: when backend search returns sparse/empty data, apply local filtering // Fallback: when backend search returns sparse/empty data, apply local filtering
// so users can still find articles by simple keywords. // so users can still find articles by simple keywords.
if (input.q && items.length === 0) { if (input.q && items.length === 0) {
const allRes = await fetch("/api/gateway/kb/articles"); const allRes = await fetch("/api/kb/articles");
if (allRes.ok) { if (allRes.ok) {
const allData = await allRes.json(); const allData = await allRes.json();
const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []); const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []);
@ -62,7 +62,7 @@ export async function fetchHelpCenterArticles(input: {
export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> { export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> {
try { try {
const res = await fetch("/api/gateway/kb/categories"); const res = await fetch("/api/kb/categories");
if (!res.ok) return HELP_CENTER_SEED_CATEGORIES; if (!res.ok) return HELP_CENTER_SEED_CATEGORIES;
const data = await res.json(); const data = await res.json();
const raw: any[] = Array.isArray(data) ? data : (data.categories ?? []); const raw: any[] = Array.isArray(data) ? data : (data.categories ?? []);
@ -98,7 +98,7 @@ export async function fetchRelatedArticles(input: {
limit?: number; limit?: number;
}): Promise<HelpArticle[]> { }): Promise<HelpArticle[]> {
try { try {
const res = await fetch("/api/gateway/kb/articles"); const res = await fetch("/api/kb/articles");
if (!res.ok) if (!res.ok)
return pickRelated( return pickRelated(
HELP_CENTER_SEED_ARTICLES as HelpArticle[], HELP_CENTER_SEED_ARTICLES as HelpArticle[],

View file

@ -1,147 +0,0 @@
/**
* 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,
});
}
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);
}
// Everything else under /api/* — let it fall through.
// Returning undefined tells the framework to continue.
return;
},
});

View file

@ -0,0 +1,87 @@
import { gatewayUrl, withAuthHeaders } from '~/lib/server/gateway';
/**
* Generic gateway proxy endpoint
* Forwards all requests to the Rust backend gateway with proper auth headers
* Usage: /api/gateway/api/companies/jobs forwards to gateway /api/companies/jobs
*/
export async function GET({ request, params }: { request: Request; params: any }) {
return proxyRequest('GET', request, params);
}
export async function POST({ request, params }: { request: Request; params: any }) {
return proxyRequest('POST', request, params);
}
export async function PUT({ request, params }: { request: Request; params: any }) {
return proxyRequest('PUT', request, params);
}
export async function DELETE({ request, params }: { request: Request; params: any }) {
return proxyRequest('DELETE', request, params);
}
export async function PATCH({ request, params }: { request: Request; params: any }) {
return proxyRequest('PATCH', request, params);
}
async function proxyRequest(method: string, request: Request, params: any) {
try {
// Handle different param structures
let pathArray = params.path;
if (!Array.isArray(pathArray)) {
pathArray = [pathArray];
}
const rawPath = `/${pathArray.join('/')}`;
// Normalize all forwarded routes to the Rust gateway's /api/* contract.
const path = rawPath.startsWith('/api/') || rawPath === '/api'
? rawPath
: `/api${rawPath}`;
// Preserve query string
const url = new URL(request.url);
const queryString = url.search ? url.search : '';
// Build request body if needed
let body: string | undefined;
if (['POST', 'PUT', 'PATCH'].includes(method)) {
body = await request.text();
}
// Forward to gateway
const upstreamUrl = gatewayUrl(path + queryString);
const upstreamRequest = new Request(upstreamUrl, {
method,
headers: withAuthHeaders(request, {
'Content-Type': request.headers.get('Content-Type') || 'application/json',
}),
body,
cache: 'no-store',
});
const response = await fetch(upstreamRequest);
// Copy response headers and return
const responseHeaders = new Headers();
response.headers.forEach((value, key) => {
if (!['server', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) {
responseHeaders.set(key, value);
}
});
responseHeaders.set('Content-Type', 'application/json');
const responseBody = await response.text();
return new Response(responseBody, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error: any) {
return new Response(
JSON.stringify({ success: false, error: error?.message || 'Internal Server Error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } },
);
}
}

View file

@ -0,0 +1,19 @@
import { gatewayUrl } from '~/lib/server/gateway';
export async function GET({ request }: { request: Request }) {
const url = new URL(request.url);
const upstream = gatewayUrl('/api/kb/articles' + url.search);
try {
const res = await fetch(upstream, { cache: 'no-store' });
const body = await res.text();
return new Response(body, {
status: res.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (err: any) {
return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
}

View file

@ -0,0 +1,18 @@
import { gatewayUrl } from '~/lib/server/gateway';
export async function GET({ params }: { params: { slug: string } }) {
const upstream = gatewayUrl(`/api/kb/articles/${params.slug}`);
try {
const res = await fetch(upstream, { cache: 'no-store' });
const body = await res.text();
return new Response(body, {
status: res.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (err: any) {
return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
}

View file

@ -0,0 +1,18 @@
import { gatewayUrl } from '~/lib/server/gateway';
export async function GET() {
const upstream = gatewayUrl('/api/kb/categories');
try {
const res = await fetch(upstream, { cache: 'no-store' });
const body = await res.text();
return new Response(body, {
status: res.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (err: any) {
return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
}

View file

@ -158,7 +158,7 @@ export default function ContactPage() {
"Job Seeker (Apply jobs)": "GENERAL", "Job Seeker (Apply jobs)": "GENERAL",
}; };
const category = userTypeToCategory[values().userType] || "GENERAL"; const category = userTypeToCategory[values().userType] || "GENERAL";
const res = await fetch("/api/gateway/support/tickets", { const res = await fetch("/api/support/tickets", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",

View file

@ -648,7 +648,7 @@ export default function RuntimeDashboardPage() {
const email = authEmail || getEmailFromStorage(); const email = authEmail || getEmailFromStorage();
if (!email) return; if (!email) return;
const checkRes = await fetch("/api/gateway/auth/check-email", { const checkRes = await fetch("/api/auth/check-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -671,7 +671,7 @@ export default function RuntimeDashboardPage() {
const token = getToken(); const token = getToken();
if (token) { if (token) {
const switchRes = await fetch("/api/gateway/auth/switch-role", { const switchRes = await fetch("/api/auth/switch-role", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View file

@ -48,7 +48,7 @@ export default function ForgotPasswordRoute() {
} }
setSubmitting(true); setSubmitting(true);
try { try {
const res = await fetch('/api/gateway/auth/forgot-password', { const res = await fetch('/api/auth/forgot-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email().trim().toLowerCase() }), body: JSON.stringify({ email: email().trim().toLowerCase() }),
@ -82,7 +82,7 @@ export default function ForgotPasswordRoute() {
} }
setSubmitting(true); setSubmitting(true);
try { try {
const res = await fetch('/api/gateway/auth/reset-password', { const res = await fetch('/api/auth/reset-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({

View file

@ -26,7 +26,7 @@ export default function HelpCenterArticlePage() {
(item) => (item ? fetchRelatedArticles({ article: item, limit: 4 }) : []) (item) => (item ? fetchRelatedArticles({ article: item, limit: 4 }) : [])
); );
const canonical = createMemo( const canonical = createMemo(
() => `https://test111.nxtgauge.com/help-center/article/${encodeURIComponent(slug())}` () => `https://test121.nxtgauge.com/help-center/article/${encodeURIComponent(slug())}`
); );
const pageTitle = createMemo(() => { const pageTitle = createMemo(() => {
const a = article(); const a = article();

View file

@ -10,7 +10,7 @@ export default function HelpCenterPage() {
const title = "Help Center | Nxtgauge"; const title = "Help Center | Nxtgauge";
const description = const description =
"Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting."; "Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting.";
const canonical = "https://test111.nxtgauge.com/help-center"; const canonical = "https://test121.nxtgauge.com/help-center";
const [query, setQuery] = createSignal(""); const [query, setQuery] = createSignal("");
const [category, setCategory] = createSignal(""); const [category, setCategory] = createSignal("");

View file

@ -4,7 +4,7 @@ import PublicLanding from '~/components/PublicLanding';
export default function Home() { export default function Home() {
const title = 'Nxtgauge | Verified Jobs & Professional Services Platform'; const title = 'Nxtgauge | Verified Jobs & Professional Services Platform';
const description = 'Trusted hiring and opportunity discovery for customers, companies, professionals, and job seekers with verification built in.'; const description = 'Trusted hiring and opportunity discovery for customers, companies, professionals, and job seekers with verification built in.';
const canonical = 'https://test111.nxtgauge.com/'; const canonical = 'https://test121.nxtgauge.com/';
return ( return (
<> <>

View file

@ -133,7 +133,7 @@ export default function LoginRoute() {
} }
setCheckingRole(true); setCheckingRole(true);
try { try {
const response = await fetch("/api/gateway/auth/check-email", { const response = await fetch("/api/auth/check-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -243,7 +243,7 @@ export default function LoginRoute() {
} }
setSubmitting(true); setSubmitting(true);
try { try {
const res = await fetch("/api/gateway/auth/login", { const res = await fetch("/api/auth/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -283,7 +283,7 @@ export default function LoginRoute() {
if (discoveredRoleKeys.length === 0 || isJobSeekerRole(discoveredActiveRole)) { if (discoveredRoleKeys.length === 0 || isJobSeekerRole(discoveredActiveRole)) {
try { try {
const checkRes = await fetch("/api/gateway/auth/check-email", { const checkRes = await fetch("/api/auth/check-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -324,7 +324,7 @@ export default function LoginRoute() {
let desiredRoleKey = discoveredActiveRole; let desiredRoleKey = discoveredActiveRole;
if (finalAccessToken && requestedRoleKey && requestedRoleKey !== discoveredActiveRole) { if (finalAccessToken && requestedRoleKey && requestedRoleKey !== discoveredActiveRole) {
try { try {
const switchRes = await fetch("/api/gateway/auth/switch-role", { const switchRes = await fetch("/api/auth/switch-role", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -385,7 +385,7 @@ export default function LoginRoute() {
setError(""); setError("");
setSubmitting(true); setSubmitting(true);
try { try {
const res = await fetch("/api/gateway/auth/resend-otp", { const res = await fetch("/api/auth/resend-otp", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -409,7 +409,7 @@ export default function LoginRoute() {
} }
setSubmitting(true); setSubmitting(true);
try { try {
const verifyRes = await fetch("/api/gateway/auth/verify-email", { const verifyRes = await fetch("/api/auth/verify-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",

View file

@ -17,7 +17,7 @@ const chipNodes = [
export default function ProfessionalsIndexPage() { export default function ProfessionalsIndexPage() {
const [scrollY, setScrollY] = createSignal(0); const [scrollY, setScrollY] = createSignal(0);
const [reduceMotion, setReduceMotion] = createSignal(false); const [reduceMotion, setReduceMotion] = createSignal(false);
const canonical = 'https://test111.nxtgauge.com/professionals'; const canonical = 'https://test121.nxtgauge.com/professionals';
const title = 'Professionals | Nxtgauge Verified Service Categories'; const title = 'Professionals | Nxtgauge Verified Service Categories';
const description = 'Explore verified professional categories on Nxtgauge and register with a trust-first workflow built for better opportunity quality.'; const description = 'Explore verified professional categories on Nxtgauge and register with a trust-first workflow built for better opportunity quality.';

View file

@ -133,7 +133,7 @@ export default function SignupRoute() {
} }
try { try {
const response = await fetch("/api/gateway/auth/check-email", { const response = await fetch("/api/auth/check-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -218,7 +218,7 @@ export default function SignupRoute() {
setSubmitting(true); setSubmitting(true);
try { try {
console.log('[register] after canSubmit guard, calling API...'); console.log('[register] after canSubmit guard, calling API...');
const res = await fetch("/api/gateway/auth/register", { const res = await fetch("/api/auth/register", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -311,7 +311,7 @@ export default function SignupRoute() {
} }
setSubmitting(true); setSubmitting(true);
try { try {
const verifyRes = await fetch("/api/gateway/auth/verify-email", { const verifyRes = await fetch("/api/auth/verify-email", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",
@ -376,7 +376,7 @@ export default function SignupRoute() {
setServerError(""); setServerError("");
setSubmitting(true); setSubmitting(true);
try { try {
const res = await fetch("/api/gateway/auth/resend-otp", { const res = await fetch("/api/auth/resend-otp", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" }, headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include", credentials: "include",

View file

@ -2,10 +2,6 @@
import { defineConfig } from "@solidjs/start/config"; import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ export default defineConfig({
// Register our Nitro middleware that handles all /api/* paths.
// Workaround for Vinxi 0.5.7 + @solidjs/start 1.3.2 build issue
// where file-based API routes are not mounted as Nitro handlers.
middleware: "./src/middleware.ts",
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],
server: { server: {
@ -18,7 +14,7 @@ export default defineConfig({
.replace(/^\/api\/gateway\/api(\/|$)/, "/api$1") .replace(/^\/api\/gateway\/api(\/|$)/, "/api$1")
.replace(/^\/api\/gateway(\/|$)/, "/api$1"), .replace(/^\/api\/gateway(\/|$)/, "/api$1"),
}, },
"/api/kb": { "/api": {
target: "http://localhost:9100", target: "http://localhost:9100",
changeOrigin: true, changeOrigin: true,
}, },