Fix admin login: runtime API proxy to master-api instead of broken build-time rewrite.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-08-25 00:43:22 +02:00
parent 4e21eb76ae
commit 89916d2c0b
4 changed files with 111 additions and 13 deletions

View file

@ -1,16 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Required for Docker image (Dokploy). Local Windows may skip via unset DOCKER_BUILD.
output: process.env.DOCKER_BUILD === "1" ? "standalone" : undefined,
async rewrites() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
return [
{
source: "/api/:path*",
destination: `${apiUrl}/api/:path*`,
},
];
},
// API calls go through src/app/api/[...path] runtime proxy (not build-time rewrites)
};
module.exports = nextConfig;

View file

@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
function masterBaseUrl(): string {
return (
process.env.MASTER_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
"http://master-api:3000"
).replace(/\/$/, "");
}
async function proxy(request: NextRequest, pathSegments: string[]) {
const targetPath = pathSegments.join("/");
const url = new URL(request.url);
const target = `${masterBaseUrl()}/api/${targetPath}${url.search}`;
const headers = new Headers();
const contentType = request.headers.get("content-type");
if (contentType) headers.set("content-type", contentType);
const cookie = request.headers.get("cookie");
if (cookie) headers.set("cookie", cookie);
headers.set("accept", "application/json");
const init: RequestInit = {
method: request.method,
headers,
redirect: "manual",
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = await request.arrayBuffer();
}
let upstream: Response;
try {
upstream = await fetch(target, init);
} catch (err) {
const message = err instanceof Error ? err.message : "Upstream unreachable";
return NextResponse.json(
{ error: { code: "UPSTREAM_ERROR", message: `Master API unreachable: ${message}` } },
{ status: 502 }
);
}
const body = await upstream.arrayBuffer();
const response = new NextResponse(body, { status: upstream.status });
const upstreamType = upstream.headers.get("content-type");
if (upstreamType) {
response.headers.set("content-type", upstreamType);
} else {
response.headers.set("content-type", "application/json");
}
// Forward Set-Cookie so login session works on admin domain
const anyHeaders = upstream.headers as Headers & { getSetCookie?: () => string[] };
const setCookies =
typeof anyHeaders.getSetCookie === "function"
? anyHeaders.getSetCookie()
: upstream.headers.get("set-cookie")
? [upstream.headers.get("set-cookie") as string]
: [];
for (const c of setCookies) {
// Master may set Secure; admin is HTTPS via NPM so keep as-is
response.headers.append("set-cookie", c);
}
return response;
}
type Ctx = { params: Promise<{ path: string[] }> };
export async function GET(request: NextRequest, ctx: Ctx) {
const { path } = await ctx.params;
return proxy(request, path);
}
export async function POST(request: NextRequest, ctx: Ctx) {
const { path } = await ctx.params;
return proxy(request, path);
}
export async function PUT(request: NextRequest, ctx: Ctx) {
const { path } = await ctx.params;
return proxy(request, path);
}
export async function PATCH(request: NextRequest, ctx: Ctx) {
const { path } = await ctx.params;
return proxy(request, path);
}
export async function DELETE(request: NextRequest, ctx: Ctx) {
const { path } = await ctx.params;
return proxy(request, path);
}

View file

@ -28,8 +28,15 @@ export default function LoginPage() {
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error?.message ?? "Login failed");
const text = await res.text();
let message = "Login failed";
try {
const data = JSON.parse(text);
message = data.error?.message ?? message;
} catch {
message = text.slice(0, 120) || `Login failed (HTTP ${res.status})`;
}
throw new Error(message);
}
router.push("/dashboard");
} catch (err) {

View file

@ -73,7 +73,8 @@ services:
environment:
HOSTNAME: 0.0.0.0
PORT: "3000"
# Server-side rewrite target (Docker network)
# Runtime proxy target (server-side, Docker network)
MASTER_API_URL: http://master-api:3000
NEXT_PUBLIC_API_URL: http://master-api:3000
depends_on:
- master-api