a686957b09
CI — Build, Lint & Security Scan / backend (push) Failing after 1m4s
CI — Build, Lint & Security Scan / frontend (push) Failing after 1m24s
CI — Build, Lint & Security Scan / image-scan (push) Has been skipped
CI — Build, Lint & Security Scan / secrets-scan (push) Failing after 21s
Deploy to TrueNAS / deploy (push) Failing after 4m0s
Auth fix (the real unblocker): - Add server-side proxy Route Handler app/api/backend/[...path]/route.ts that reads the NextAuth session via auth() and injects Authorization: Bearer on every API call. Method-agnostic; streams raw request body (multipart uploads) and upstream response body (binary PDF/CSV downloads). Replaces the static next.config.mjs rewrite, which could not inject a header — the root cause of every authenticated browser fetch hitting the backend unauthenticated. - Expose session.accessToken in the auth.ts session() callback (+ type aug). Uses auth() not getToken() so cookie handling is correct across the public HTTPS (Apache) -> internal HTTP (container) proxy boundary. - No service files changed; all 24 services already call /api/backend/*. Verified live: NextAuth login -> GET /api/backend/members -> HTTP 200. Public hosting (same proven chain as Gitea/InspectFlow): - docker-compose.truenas.yml: NEXTAUTH_URL/AUTH_URL -> https public origin, rotate AUTH_SECRET + JWT_SECRET + DB_PASSWORD off the committed dev defaults. - deploy.yml: inject AUTH_SECRET/JWT_SECRET/DB_PASSWORD from Gitea secrets; reconcile the live Postgres role password (volume keeps old pw on re-deploy). - frpc on TrueNAS tunnels frontend :3000 -> VPS frps :30010; IONOS Apache terminates TLS for cannamanage.plate-software.de and proxies through frp.
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
/**
|
|
* Server-side API proxy for the CannaManage backend.
|
|
*
|
|
* Replaces the old static `rewrites()` proxy in next.config.mjs. A static
|
|
* rewrite forwards requests as-is and CANNOT inject an Authorization header,
|
|
* which was the root cause of the systemic "no token reaches the backend" bug:
|
|
* every browser fetch hit the backend unauthenticated → 401/500 → pages only
|
|
* survived via mock fallbacks.
|
|
*
|
|
* This Route Handler runs on the server, reads the NextAuth session via
|
|
* `auth()` (so the JWT never leaves the server), and forwards the request to
|
|
* `${BACKEND_URL}/api/v1/<path>` with `Authorization: Bearer <accessToken>`.
|
|
*
|
|
* It is method-agnostic and content-agnostic:
|
|
* - Query string is preserved.
|
|
* - The raw request body is streamed through unparsed, so JSON,
|
|
* multipart/form-data (file uploads) and any other content type work.
|
|
* - The upstream response body is streamed back verbatim, so binary
|
|
* downloads (PDF/CSV reports, attachments) are byte-exact.
|
|
*/
|
|
import { NextResponse } from "next/server"
|
|
|
|
import type { NextRequest } from "next/server"
|
|
|
|
import { auth } from "@/lib/auth"
|
|
|
|
// Always run dynamically — this proxy depends on per-request auth + body.
|
|
export const dynamic = "force-dynamic"
|
|
|
|
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8080"
|
|
|
|
// Hop-by-hop and host-specific headers that must not be forwarded upstream.
|
|
const STRIPPED_REQUEST_HEADERS = new Set([
|
|
"host",
|
|
"connection",
|
|
"content-length",
|
|
"transfer-encoding",
|
|
"accept-encoding",
|
|
])
|
|
|
|
// Headers that must not be copied from the upstream response back to the client.
|
|
const STRIPPED_RESPONSE_HEADERS = new Set([
|
|
"connection",
|
|
"transfer-encoding",
|
|
"content-encoding",
|
|
"content-length",
|
|
])
|
|
|
|
async function proxy(req: NextRequest, path: string[]): Promise<NextResponse> {
|
|
const session = await auth()
|
|
const accessToken = session?.accessToken
|
|
|
|
// Build the upstream URL: /api/backend/<path> → BACKEND_URL/api/v1/<path>
|
|
const search = req.nextUrl.search // includes leading "?" or ""
|
|
const upstreamUrl = `${BACKEND_URL}/api/v1/${path.join("/")}${search}`
|
|
|
|
// Clone the incoming headers, stripping hop-by-hop/host ones, then inject auth.
|
|
const headers = new Headers()
|
|
req.headers.forEach((value, key) => {
|
|
if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) {
|
|
headers.set(key, value)
|
|
}
|
|
})
|
|
if (accessToken) {
|
|
headers.set("Authorization", `Bearer ${accessToken}`)
|
|
}
|
|
|
|
const method = req.method.toUpperCase()
|
|
const hasBody = method !== "GET" && method !== "HEAD"
|
|
|
|
try {
|
|
const upstream = await fetch(upstreamUrl, {
|
|
method,
|
|
headers,
|
|
// Stream the raw body through unparsed (works for JSON + multipart + binary).
|
|
body: hasBody ? req.body : undefined,
|
|
// Required by undici/Node when sending a streaming request body.
|
|
...(hasBody ? { duplex: "half" } : {}),
|
|
redirect: "manual",
|
|
cache: "no-store",
|
|
} as RequestInit)
|
|
|
|
// Copy upstream response headers, dropping ones that break a re-emitted body.
|
|
const responseHeaders = new Headers()
|
|
upstream.headers.forEach((value, key) => {
|
|
if (!STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase())) {
|
|
responseHeaders.set(key, value)
|
|
}
|
|
})
|
|
|
|
// Stream the body straight back — byte-exact for downloads.
|
|
return new NextResponse(upstream.body, {
|
|
status: upstream.status,
|
|
statusText: upstream.statusText,
|
|
headers: responseHeaders,
|
|
})
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ code: "BACKEND_UNREACHABLE", message: "Unable to reach the API." },
|
|
{ status: 502 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Next.js 15: the second arg's `params` is a Promise.
|
|
type Ctx = { params: Promise<{ path: string[] }> }
|
|
|
|
export async function GET(req: NextRequest, ctx: Ctx) {
|
|
return proxy(req, (await ctx.params).path)
|
|
}
|
|
export async function POST(req: NextRequest, ctx: Ctx) {
|
|
return proxy(req, (await ctx.params).path)
|
|
}
|
|
export async function PUT(req: NextRequest, ctx: Ctx) {
|
|
return proxy(req, (await ctx.params).path)
|
|
}
|
|
export async function PATCH(req: NextRequest, ctx: Ctx) {
|
|
return proxy(req, (await ctx.params).path)
|
|
}
|
|
export async function DELETE(req: NextRequest, ctx: Ctx) {
|
|
return proxy(req, (await ctx.params).path)
|
|
}
|