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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// Required for Docker image (Dokploy). Local Windows may skip via unset DOCKER_BUILD.
|
|
||||||
output: process.env.DOCKER_BUILD === "1" ? "standalone" : undefined,
|
output: process.env.DOCKER_BUILD === "1" ? "standalone" : undefined,
|
||||||
async rewrites() {
|
// API calls go through src/app/api/[...path] runtime proxy (not build-time rewrites)
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/api/:path*",
|
|
||||||
destination: `${apiUrl}/api/:path*`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
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 }),
|
body: JSON.stringify({ email, password }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const text = await res.text();
|
||||||
throw new Error(data.error?.message ?? "Login failed");
|
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");
|
router.push("/dashboard");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
HOSTNAME: 0.0.0.0
|
HOSTNAME: 0.0.0.0
|
||||||
PORT: "3000"
|
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
|
NEXT_PUBLIC_API_URL: http://master-api:3000
|
||||||
depends_on:
|
depends_on:
|
||||||
- master-api
|
- master-api
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue