Fix admin session cookie by setting it on the admin domain after login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
29595dd98f
commit
39ecf1af56
3 changed files with 106 additions and 12 deletions
|
|
@ -3,6 +3,9 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const SESSION_COOKIE = "mc_admin_session";
|
||||
const SESSION_MAX_AGE = 7 * 24 * 60 * 60;
|
||||
|
||||
function candidateBases(): string[] {
|
||||
const configured = [
|
||||
process.env.MASTER_API_URL,
|
||||
|
|
@ -14,10 +17,50 @@ function candidateBases(): string[] {
|
|||
return [...new Set(configured.map((v) => v.replace(/\/$/, "")))];
|
||||
}
|
||||
|
||||
function isHttps(request: NextRequest): boolean {
|
||||
const proto = request.headers.get("x-forwarded-proto");
|
||||
if (proto) return proto.split(",")[0].trim() === "https";
|
||||
return request.nextUrl.protocol === "https:";
|
||||
}
|
||||
|
||||
function applySessionCookie(
|
||||
response: NextResponse,
|
||||
token: string,
|
||||
request: NextRequest,
|
||||
expiresAt?: string
|
||||
) {
|
||||
const secure = isHttps(request) || process.env.NODE_ENV === "production";
|
||||
response.cookies.set({
|
||||
name: SESSION_COOKIE,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: SESSION_MAX_AGE,
|
||||
...(expiresAt ? { expires: new Date(expiresAt) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function clearSessionCookie(response: NextResponse, request: NextRequest) {
|
||||
const secure = isHttps(request) || process.env.NODE_ENV === "production";
|
||||
response.cookies.set({
|
||||
name: SESSION_COOKIE,
|
||||
value: "",
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function proxy(request: NextRequest, pathSegments: string[]) {
|
||||
const targetPath = pathSegments.join("/");
|
||||
const url = new URL(request.url);
|
||||
const search = url.search;
|
||||
const isLogin = request.method === "POST" && targetPath === "v1/auth/login";
|
||||
const isLogout = request.method === "POST" && targetPath === "v1/auth/logout";
|
||||
|
||||
const headers = new Headers();
|
||||
const contentType = request.headers.get("content-type");
|
||||
|
|
@ -43,27 +86,71 @@ async function proxy(request: NextRequest, pathSegments: string[]) {
|
|||
redirect: "manual",
|
||||
});
|
||||
|
||||
const responseBody = await upstream.arrayBuffer();
|
||||
const response = new NextResponse(responseBody, { status: upstream.status });
|
||||
// Special-case auth so the session cookie is owned by admin.vonas.nl
|
||||
if (isLogin) {
|
||||
const text = await upstream.text();
|
||||
let data: {
|
||||
user?: unknown;
|
||||
sessionToken?: string;
|
||||
expiresAt?: string;
|
||||
error?: unknown;
|
||||
} = {};
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
return new NextResponse(text, {
|
||||
status: upstream.status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const upstreamType = upstream.headers.get("content-type");
|
||||
response.headers.set(
|
||||
"content-type",
|
||||
upstreamType ?? "application/json"
|
||||
const response = NextResponse.json(
|
||||
{ user: data.user, error: data.error },
|
||||
{ status: upstream.status }
|
||||
);
|
||||
|
||||
if (upstream.ok && data.sessionToken) {
|
||||
applySessionCookie(response, data.sessionToken, request, data.expiresAt);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
if (isLogout) {
|
||||
const responseBody = await upstream.arrayBuffer();
|
||||
const response = new NextResponse(responseBody, {
|
||||
status: upstream.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
upstream.headers.get("content-type") ?? "application/json",
|
||||
},
|
||||
});
|
||||
clearSessionCookie(response, request);
|
||||
return response;
|
||||
}
|
||||
|
||||
const responseBody = await upstream.arrayBuffer();
|
||||
const response = new NextResponse(responseBody, { status: upstream.status });
|
||||
response.headers.set(
|
||||
"content-type",
|
||||
upstream.headers.get("content-type") ?? "application/json"
|
||||
);
|
||||
|
||||
// Forward any upstream cookies as fallback (rewritten without 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) {
|
||||
response.headers.append("set-cookie", c);
|
||||
for (const raw of setCookies) {
|
||||
const cleaned = raw
|
||||
.split(";")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => !/^domain=/i.test(p))
|
||||
.join("; ");
|
||||
response.headers.append("set-cookie", cleaned);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ export default function LoginPage() {
|
|||
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/auth/me", { credentials: "include" })
|
||||
.then((r) => r.ok && router.push("/dashboard"))
|
||||
.then((r) => {
|
||||
if (r.ok) router.push("/dashboard");
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [router]);
|
||||
|
||||
|
|
@ -38,10 +40,10 @@ export default function LoginPage() {
|
|||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
router.push("/dashboard");
|
||||
// Full navigation so the session cookie is definitely included
|
||||
window.location.assign("/dashboard");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Inloggen mislukt");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,12 @@ export async function registerAuthRoutes(
|
|||
expires: expiresAt,
|
||||
});
|
||||
|
||||
return { user: { id: user.id, email: user.email, name: user.name } };
|
||||
// sessionToken also in body so Admin UI proxy can set cookie on its own domain
|
||||
return {
|
||||
user: { id: user.id, email: user.email, name: user.name },
|
||||
sessionToken: token,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/api/v1/auth/logout", async (request, reply) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue