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:
parent
4e21eb76ae
commit
89916d2c0b
4 changed files with 111 additions and 13 deletions
|
|
@ -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;
|
||||
|
|
|
|||
99
apps/admin-ui/src/app/api/[...path]/route.ts
Normal file
99
apps/admin-ui/src/app/api/[...path]/route.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue