Files
NianAIGC/lib/server/auth/current-user.ts

275 lines
8.7 KiB
TypeScript

import { cookies } from "next/headers";
import { SESSION_COOKIE_NAME, getAuthRuntimeConfig } from "@/lib/auth/config";
import { hasAdminSessionAccess, hasSuperAdminAccess } from "@/lib/auth/permissions";
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";
export class AuthRequiredError extends Error {
status = 401;
constructor(message = "请先登录。") {
super(message);
this.name = "AuthRequiredError";
}
}
export class AuthConfigurationError extends Error {
status = 503;
constructor(message: string) {
super(message);
this.name = "AuthConfigurationError";
}
}
const localUser: AuthUser = {
id: DEFAULT_OWNER_ID,
subject: DEFAULT_OWNER_ID,
username: "13800000000",
phone: "13800000000",
displayName: "智念用户",
clientId: "local-dev",
role: "super_admin",
organizationId: "org-demo",
organizationName: "演示组织",
authorities: ["zhinian_admin"],
scope: []
};
function localSession(): AuthSession {
const now = Math.floor(Date.now() / 1000);
return {
version: 1,
authMode: "admin",
user: localUser,
issuedAt: now,
expiresAt: now + 24 * 60 * 60
};
}
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;
const cookieStore = await cookies();
const session = await parseSessionCookieValue(
readChunkedCookieValue(SESSION_COOKIE_NAME, (name) => cookieStore.get(name)?.value),
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;
}
export async function requireAppSession(): Promise<AuthSession> {
const session = await getOptionalAuthSession();
if (session) return session;
const config = getAuthRuntimeConfig();
if (!config.required) return localSession();
if (!config.configured) {
throw new AuthConfigurationError(`认证配置不完整:${config.missing.join(", ") || "未知配置"}`);
}
throw new AuthRequiredError();
}
export async function requireAppUser(): Promise<AuthUser> {
return (await requireAppSession()).user;
}
export class AdminRequiredError extends Error {
status = 403;
constructor(message = "需要管理员权限。") {
super(message);
this.name = "AdminRequiredError";
}
}
export async function requireAdminUser(): Promise<AuthUser> {
return (await requireAdminSession()).user;
}
export async function requireAdminSession(): Promise<AuthSession> {
const session = await requireAppSession();
if (!hasAdminSessionAccess(session)) throw new AdminRequiredError();
return session;
}
export class SuperAdminRequiredError extends Error {
status = 403;
constructor(message = "需要超级管理员权限。") {
super(message);
this.name = "SuperAdminRequiredError";
}
}
export async function requireSuperAdminSession(): Promise<AuthSession> {
const session = await requireAppSession();
if (!hasSuperAdminAccess(session.user)) throw new SuperAdminRequiredError();
return session;
}
export async function requireSuperAdminUser(): Promise<AuthUser> {
return (await requireSuperAdminSession()).user;
}
export async function getShellAuthState(): Promise<{
user: AuthUser | null;
authRequired: boolean;
authConfigured: boolean;
isAdmin: boolean;
isSuperAdmin: boolean;
}> {
const config = getAuthRuntimeConfig();
const session = await getOptionalAuthSession();
const shellSession = session || (!config.required ? localSession() : null);
return {
user: shellSession?.user || null,
authRequired: config.required,
authConfigured: config.configured,
isAdmin: hasAdminSessionAccess(shellSession),
isSuperAdmin: hasSuperAdminAccess(shellSession?.user)
};
}