fix: route authenticated SSR through Go

This commit is contained in:
2026-08-16 17:53:45 +08:00
parent b26f9679ab
commit 498c2fa242
6 changed files with 445 additions and 1 deletions

View File

@@ -1,7 +1,13 @@
import { cookies } from "next/headers";
import { SESSION_COOKIE_NAME, getAuthRuntimeConfig } from "@/lib/auth/config";
import { hasAdminSessionAccess, hasSuperAdminAccess } from "@/lib/auth/permissions";
import { parseSessionCookieValue, readChunkedCookieValue, type AuthSession, type AuthUser } from "@/lib/auth/session";
import {
chunkedCookieNames,
parseSessionCookieValue,
readChunkedCookieValue,
type AuthSession,
type AuthUser
} from "@/lib/auth/session";
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
import { loadPlatformAuthorizationSnapshot } from "@/lib/server/auth/platform-authorization-store";
import { authorizePlatformSession } from "@/lib/server/auth/platform-session";
@@ -49,6 +55,127 @@ function localSession(): AuthSession {
};
}
type PlatformAuthUser = AuthUser & {
role: NonNullable<AuthUser["role"]>;
status: "active";
};
type GoCurrentSessionResponse = {
authRequired: boolean;
authConfigured: boolean;
} & (
| { authenticated: false; authMode: null; user: null }
| { authenticated: true; authMode: AuthSession["authMode"]; user: PlatformAuthUser }
);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
function parsePlatformAuthUser(value: unknown): PlatformAuthUser | null {
if (!isRecord(value)) return null;
const role = value.role;
if (
(role !== "user" && role !== "organization_admin" && role !== "super_admin") ||
value.status !== "active" ||
typeof value.id !== "string" ||
!value.id ||
value.subject !== value.id ||
typeof value.displayName !== "string" ||
typeof value.clientId !== "string" ||
!isStringArray(value.authorities) ||
!isStringArray(value.scope)
) {
return null;
}
const organizationId = typeof value.organizationId === "string" && value.organizationId.trim()
? value.organizationId
: undefined;
if (role !== "super_admin" && !organizationId) return null;
const user: PlatformAuthUser = {
id: value.id,
subject: value.id,
displayName: value.displayName,
clientId: value.clientId,
role,
status: "active",
authorities: [...value.authorities],
scope: [...value.scope]
};
if (typeof value.username === "string") user.username = value.username;
if (typeof value.phone === "string") user.phone = value.phone;
if (typeof value.tenantId === "string") user.tenantId = value.tenantId;
if (organizationId) user.organizationId = organizationId;
if (typeof value.organizationName === "string") user.organizationName = value.organizationName;
return user;
}
function parseGoCurrentSessionResponse(value: unknown): GoCurrentSessionResponse {
if (
!isRecord(value) ||
typeof value.authenticated !== "boolean" ||
typeof value.authRequired !== "boolean" ||
typeof value.authConfigured !== "boolean"
) {
throw new Error("Go auth/me returned an invalid response.");
}
if (!value.authenticated) {
if (value.authMode !== null || value.user !== null) {
throw new Error("Go auth/me returned an invalid anonymous response.");
}
return {
authenticated: false,
authRequired: value.authRequired,
authConfigured: value.authConfigured,
authMode: null,
user: null
};
}
const user = parsePlatformAuthUser(value.user);
if (
!value.authConfigured ||
(value.authMode !== "user" && value.authMode !== "admin") ||
!user ||
value.authMode !== (user.role === "user" ? "user" : "admin")
) {
throw new Error("Go auth/me returned an invalid authenticated response.");
}
return {
authenticated: true,
authRequired: value.authRequired,
authConfigured: value.authConfigured,
authMode: value.authMode,
user
};
}
async function authorizeSessionThroughGo(session: AuthSession, cookieHeader: string, baseUrl: string) {
const endpoint = new URL(`${baseUrl.replace(/\/+$/, "")}/api/auth/me`);
if ((endpoint.protocol !== "http:" && endpoint.protocol !== "https:") || endpoint.username || endpoint.password) {
throw new AuthConfigurationError("ZHINIAN_GO_INTERNAL_BASE_URL 必须是无凭据的 HTTP(S) 地址。");
}
const response = await fetch(endpoint.toString(), {
cache: "no-store",
headers: { cookie: cookieHeader }
});
if (!response.ok) throw new Error(`Go auth/me request failed with status ${response.status}.`);
const currentSession = parseGoCurrentSessionResponse(await response.json());
if (!currentSession.authenticated) return null;
if (currentSession.user.id !== session.user.id || currentSession.user.clientId !== "platform") {
throw new Error("Go auth/me returned a mismatched authenticated user.");
}
return {
...session,
authMode: currentSession.authMode,
user: currentSession.user
} satisfies AuthSession;
}
export async function getOptionalAuthSession(): Promise<AuthSession | null> {
const config = getAuthRuntimeConfig();
if (!config.sessionSecret) return null;
@@ -58,6 +185,18 @@ export async function getOptionalAuthSession(): Promise<AuthSession | null> {
config.sessionSecret
);
if (!session) return null;
if (session.user.clientId !== "platform") return null;
const internalGoBaseUrl = process.env.ZHINIAN_GO_INTERNAL_BASE_URL?.trim();
if (internalGoBaseUrl) {
const cookieHeader = chunkedCookieNames(SESSION_COOKIE_NAME)
.map((name) => {
const value = cookieStore.get(name)?.value;
return value === undefined ? null : `${name}=${value}`;
})
.filter((value): value is string => value !== null)
.join("; ");
return authorizeSessionThroughGo(session, cookieHeader, internalGoBaseUrl);
}
const authorization = await authorizePlatformSession(session, loadPlatformAuthorizationSnapshot);
return authorization.outcome === "authenticated" ? authorization.session : null;
}