Fix admin→master connectivity via host fallback; use generic login branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
89916d2c0b
commit
0a007aae83
4 changed files with 75 additions and 56 deletions
|
|
@ -3,18 +3,21 @@ 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(/\/$/, "");
|
||||
function candidateBases(): string[] {
|
||||
const configured = [
|
||||
process.env.MASTER_API_URL,
|
||||
process.env.NEXT_PUBLIC_API_URL,
|
||||
"http://master-api:3000",
|
||||
"http://host.docker.internal:3100",
|
||||
].filter((v): v is string => !!v && v.trim().length > 0);
|
||||
|
||||
return [...new Set(configured.map((v) => v.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 search = url.search;
|
||||
|
||||
const headers = new Headers();
|
||||
const contentType = request.headers.get("content-type");
|
||||
|
|
@ -23,39 +26,35 @@ async function proxy(request: NextRequest, pathSegments: string[]) {
|
|||
if (cookie) headers.set("cookie", cookie);
|
||||
headers.set("accept", "application/json");
|
||||
|
||||
const init: RequestInit = {
|
||||
const body =
|
||||
request.method !== "GET" && request.method !== "HEAD"
|
||||
? await request.arrayBuffer()
|
||||
: undefined;
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const base of candidateBases()) {
|
||||
const target = `${base}/api/${targetPath}${search}`;
|
||||
try {
|
||||
const upstream = await fetch(target, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body,
|
||||
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 responseBody = await upstream.arrayBuffer();
|
||||
const response = new NextResponse(responseBody, { 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");
|
||||
}
|
||||
response.headers.set(
|
||||
"content-type",
|
||||
upstreamType ?? "application/json"
|
||||
);
|
||||
|
||||
// Forward Set-Cookie so login session works on admin domain
|
||||
const anyHeaders = upstream.headers as Headers & { getSetCookie?: () => string[] };
|
||||
const anyHeaders = upstream.headers as Headers & {
|
||||
getSetCookie?: () => string[];
|
||||
};
|
||||
const setCookies =
|
||||
typeof anyHeaders.getSetCookie === "function"
|
||||
? anyHeaders.getSetCookie()
|
||||
|
|
@ -64,11 +63,25 @@ async function proxy(request: NextRequest, pathSegments: 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;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "fetch failed";
|
||||
errors.push(`${base}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: "UPSTREAM_ERROR",
|
||||
message: `Master API unreachable: ${errors.join(" | ")}`,
|
||||
},
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
type Ctx = { params: Promise<{ path: string[] }> };
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import type { Metadata } from "next";
|
|||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "STH Media Cluster",
|
||||
description: "Admin dashboard for distributed Stremio media cluster",
|
||||
title: "Aanmelden",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function Nav() {
|
|||
|
||||
return (
|
||||
<nav>
|
||||
<span className="brand">STH Media</span>
|
||||
<span className="brand">Beheer</span>
|
||||
{links.map((l) => (
|
||||
<Link key={l.href} href={l.href} className={pathname === l.href ? "active" : ""}>
|
||||
{l.label}
|
||||
|
|
|
|||
|
|
@ -62,22 +62,28 @@ services:
|
|||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- media-cluster
|
||||
media-cluster:
|
||||
aliases:
|
||||
- master-api
|
||||
|
||||
admin-ui:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile.admin-ui
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
HOSTNAME: 0.0.0.0
|
||||
PORT: "3000"
|
||||
# Runtime proxy target (server-side, Docker network)
|
||||
# Prefer compose DNS; fallback via published host port
|
||||
MASTER_API_URL: http://master-api:3000
|
||||
NEXT_PUBLIC_API_URL: http://master-api:3000
|
||||
depends_on:
|
||||
- master-api
|
||||
master-api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3101:3000"
|
||||
networks:
|
||||
|
|
|
|||
Loading…
Reference in a new issue