fix: route authenticated SSR through Go
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user