Compare commits

..

25 commits

Author SHA1 Message Date
Ashwin Kumar Sivakumar
494384e6c6 fix: use kaniko for forgejo image builds
Some checks failed
build-and-push / build (push) Failing after 14s
2026-06-12 22:55:00 +05:30
Ashwin Kumar Sivakumar
46b486fb52 chore: inspect forgejo service networking
Some checks failed
build-and-push / build (push) Failing after 21s
2026-06-12 22:24:12 +05:30
Ashwin Kumar Sivakumar
e856eae414 fix: use dind service for forgejo builds
Some checks failed
build-and-push / build (push) Failing after 21s
2026-06-12 22:09:33 +05:30
Ashwin Kumar Sivakumar
b55f69ffc4 fix: install docker cli in forgejo workflow namespace
Some checks failed
build-and-push / build (push) Failing after 21s
2026-06-12 22:07:26 +05:30
Ashwin Kumar Sivakumar
fd75e7a5ea chore: retrigger after forgejo restart
Some checks failed
build-and-push / build (push) Failing after 4s
2026-06-12 22:03:38 +05:30
Ashwin Kumar Sivakumar
7a7849f5a6 chore: retrigger forgejo after main sync fix
Some checks failed
build-and-push / build (push) Failing after 4s
2026-06-12 22:00:39 +05:30
Ashwin Kumar Sivakumar
c10668aa7d chore: retrigger forgejo build after workflow sync
Some checks failed
build-and-push / build (push) Failing after 4s
2026-06-12 21:59:01 +05:30
Ashwin Kumar Sivakumar
a4cae251d9 fix: restore forgejo build workflow filename
Some checks failed
build-and-push / build (push) Failing after 29s
2026-06-12 21:54:48 +05:30
Ashwin Kumar Sivakumar
abf4095bf2 fix: use jq encoded forgejo push sync
Some checks failed
build-and-push / build (push) Failing after 4s
2026-06-12 21:45:46 +05:30
Ashwin Kumar Sivakumar
d124a91dea fix: restore working github forgejo sync 2026-06-12 21:44:07 +05:30
Ashwin Kumar Sivakumar
ef9fed75cb fix: install docker cli in forgejo workflow 2026-06-12 21:39:04 +05:30
Ashwin Kumar Sivakumar
3c437b61b3 fix: use encoded forgejo remote for sync
Some checks failed
build-and-push / build (push) Failing after 41s
2026-06-12 20:18:28 +05:30
Ashwin Kumar Sivakumar
4fc874a44b ci: add forgejo sync and build pipeline 2026-06-12 20:17:00 +05:30
Ashwin Kumar Sivakumar
4c61bcaf31 fix: remove DPR scaling from CaptchaCanvas to fix zoomed-in appearance
- Fixed canvas dimensions to 176x52 pixels (no DPR scaling)
- All drawing operations now use fixed pixel values
- Prevents captcha from appearing zoomed in on high-DPI displays
- Characters, lines, and circles are now properly positioned within bounds
2026-06-12 05:03:54 +05:30
Ashwin Kumar Sivakumar
d8467d1aeb fix: remove push preflight from forgejo mirror sync 2026-06-11 19:29:41 +05:30
Ashwin Kumar Sivakumar
cf66611750 fix: trigger forgejo mirror sync via api 2026-06-11 19:14:46 +05:30
Ashwin Kumar Sivakumar
8a8072b305 fix: use basic auth for forgejo sync 2026-06-11 18:56:39 +05:30
Ashwin Kumar Sivakumar
5d10025c34 fix: use existing forgejo mirror secrets 2026-06-11 18:19:10 +05:30
Ashwin Kumar Sivakumar
93deed0d11 fix: point forgejo sync to ashwin namespace 2026-06-11 18:00:03 +05:30
Ashwin Kumar Sivakumar
c8ecb6bf81 chore: migrate ci naming to forgejo 2026-06-11 17:17:42 +05:30
Rimuru
6666cc5f67 fix(frontend): replace broken file-based API routes with SolidStart middleware
Vinxi 0.5.7 + @solidjs/start 1.3.2 has a build bug where file-based API
routes (src/routes/api/*) are registered in the page router tree but never
mounted as Nitro handlers in the production build, so every /api/* request
returns a framework 404.

Fix: register a SolidStart middleware (src/middleware.ts) via the
middleware config field. The middleware intercepts all /api/* paths and
proxies them to the Rust gateway, bypassing the broken page router.

Covers:
- /api/gateway/* (catch-all proxy to gateway)
- /api/kb/categories
- /api/kb/articles
- /api/kb/articles/:slug

Also tightens the dev-server vite proxy from /api to /api/kb so it
doesn't shadow the new middleware in dev.

Removes the dead src/routes/api/ tree (no longer used).
2026-06-11 15:36:44 +05:30
Ashwin Kumar Sivakumar
aabfacc735 fix: route client /api/* through /api/gateway/* proxy\n\nLogin, signup, forgot-password, dashboard, contact, help-center, and all dashboard component fetches were calling bare /api/* paths. The SolidStart server has no /api/auth/*, /api/support/*, or /api/kb/* handlers -- only /api/gateway/[...path] proxies to the Rust gateway. So those calls returned HTML (SPA catch-all), the JSON parse threw silently, and the buttons looked dead. Sign In / Sign Up / Forgot Password all appeared to be no-ops.\n\nThis routes every client-side fetch through the existing gateway proxy, matching the pattern already used by src/lib/api.ts.\n\nAlso fixes hardcoded test121 -> test111 in canonical/og:url tags across index, professionals, help-center, and RoleLandingPage. 2026-06-11 14:10:10 +05:30
Ashwin Kumar Sivakumar
31101db955 chore: rebuild trigger #2 2026-06-08 21:33:46 +05:30
Ashwin Kumar Sivakumar
4930a24e35 chore: trigger rebuild with base image now available 2026-06-08 21:16:18 +05:30
Ashwin Kumar Sivakumar
b21c3e43b1 chore: trigger build 2026-06-08 21:06:25 +05:30
30 changed files with 231 additions and 300 deletions

View file

@ -0,0 +1,13 @@
#!/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

@ -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. ArgoCD detects the change and deploys 3. Flux detects the change and deploys
""" """
import argparse import argparse
@ -98,10 +98,14 @@ 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 or "admin" in args.service: if "frontend" 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):

View file

@ -9,73 +9,22 @@ 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: Install Docker CLI - name: Build and push
run: | uses: docker://gcr.io/kaniko-project/executor:v1.23.2-debug
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 }}
run: | IMAGE_NAME: nxtgauge-frontend-solid
set -euo pipefail LATEST_TAG: high-performance-latest
export DOCKER_HOST=tcp://docker:2375 COMMIT_SHA: ${{ github.sha }}
SHA="$(git rev-parse HEAD)" with:
test -n "$REGISTRY_HOSTPORT" entrypoint: /busybox/sh
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOSTPORT" -u "$REGISTRY_USERNAME" --password-stdin args: ${{ github.workspace }}/.forgejo/scripts/kaniko-build.sh
- 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

@ -1,46 +0,0 @@
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,3 +12,6 @@ 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 `.gitea/workflows/test.yaml`: GitHub Actions runs tests nightly via `.forgejo/workflows/test.yaml`:
- **2:30 AM daily** — all test suites - **2:30 AM daily** — all test suites
- **On-demand** — use `workflow_dispatch` trigger in Gitea - **On-demand** — use `workflow_dispatch` trigger in Forgejo
Artifacts are uploaded: Artifacts are uploaded:
- `vitest-coverage/` — coverage reports - `vitest-coverage/` — coverage reports

View file

@ -21,53 +21,52 @@ 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 first (before any drawing) // Set canvas resolution (fixed at 1x to prevent zoom issues)
canvas.width = Math.floor(width * dpr); canvas.width = width;
canvas.height = Math.floor(height * dpr); canvas.height = height;
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
// Clear and fill background // Clear and fill background
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, width, height);
// Draw decorative lines // Draw decorative lines (within bounds)
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() * canvas.width, Math.random() * canvas.height); ctx.moveTo(Math.random() * width, Math.random() * height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.lineTo(Math.random() * width, Math.random() * height);
ctx.stroke(); ctx.stroke();
} }
// Draw decorative circles // Draw decorative circles (within bounds)
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() * canvas.width, Math.random() * canvas.height, Math.random() * 1.8 + 0.6, 0, Math.PI * 2); ctx.arc(Math.random() * width, Math.random() * height, Math.random() * 1.8 + 0.6, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
// Draw characters // Draw characters (fixed positioning)
const chars = String(props.code || '').slice(0, 6).split(''); const chars = String(props.code || '').slice(0, 6).split('');
const startX = 16 * dpr; const startX = 16;
const charGap = 24 * dpr; const charGap = 24;
chars.forEach((char, index) => { chars.forEach((char, index) => {
const x = startX + index * charGap; const x = startX + index * charGap;
const y = canvas.height / 2; const y = 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 ${22 * dpr}px "Courier New", monospace`; ctx.font = `800 22px "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/auth/session", { const res = await fetch("/api/gateway/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://test121.nxtgauge.com${props.pathBase}/${encodeURIComponent(String(props.slug || ''))}`); const canonical = createMemo(() => `https://test111.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/me/notifications/unread-count', { const res = await fetch('/api/gateway/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/companies/jobs?page=1&limit=100', { fetch('/api/gateway/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/companies/jobs?page=1&limit=1', { fetch('/api/gateway/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/customers/requirements?page=1&limit=100', { const res = await fetch('/api/gateway/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/jobseeker/jobs?page=1&limit=100', { fetch('/api/gateway/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/jobseeker/applications?page=1&limit=100', { fetch('/api/gateway/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/auth/session", { const res = await fetch("/api/gateway/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/kb/articles"); const allRes = await fetch("/api/gateway/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/kb/categories"); const res = await fetch("/api/gateway/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/kb/articles"); const res = await fetch("/api/gateway/kb/articles");
if (!res.ok) if (!res.ok)
return pickRelated( return pickRelated(
HELP_CENTER_SEED_ARTICLES as HelpArticle[], HELP_CENTER_SEED_ARTICLES as HelpArticle[],

147
src/middleware.ts Normal file
View file

@ -0,0 +1,147 @@
/**
* 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

@ -1,87 +0,0 @@
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

@ -1,19 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,18 +0,0 @@
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/support/tickets", { const res = await fetch("/api/gateway/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/auth/check-email", { const checkRes = await fetch("/api/gateway/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/auth/switch-role", { const switchRes = await fetch("/api/gateway/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/auth/forgot-password', { const res = await fetch('/api/gateway/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/auth/reset-password', { const res = await fetch('/api/gateway/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://test121.nxtgauge.com/help-center/article/${encodeURIComponent(slug())}` () => `https://test111.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://test121.nxtgauge.com/help-center"; const canonical = "https://test111.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://test121.nxtgauge.com/'; const canonical = 'https://test111.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/auth/check-email", { const response = await fetch("/api/gateway/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/auth/login", { const res = await fetch("/api/gateway/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/auth/check-email", { const checkRes = await fetch("/api/gateway/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/auth/switch-role", { const switchRes = await fetch("/api/gateway/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/auth/resend-otp", { const res = await fetch("/api/gateway/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/auth/verify-email", { const verifyRes = await fetch("/api/gateway/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://test121.nxtgauge.com/professionals'; const canonical = 'https://test111.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/auth/check-email", { const response = await fetch("/api/gateway/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/auth/register", { const res = await fetch("/api/gateway/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/auth/verify-email", { const verifyRes = await fetch("/api/gateway/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/auth/resend-otp", { const res = await fetch("/api/gateway/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,6 +2,10 @@
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: {
@ -14,7 +18,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": { "/api/kb": {
target: "http://localhost:9100", target: "http://localhost:9100",
changeOrigin: true, changeOrigin: true,
}, },