Files
NianAIGC/lib/client/browser-auth.ts

94 lines
3.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { AuthMode, AuthUser } from "@/lib/auth/session";
export type BrowserCurrentSession = {
authenticated: boolean;
authRequired: boolean;
authConfigured: boolean;
authMode: AuthMode | null;
user: AuthUser | null;
};
export async function fetchBrowserAuthState(signal?: AbortSignal): Promise<BrowserCurrentSession> {
const response = await fetch("/api/auth/me", {
method: "GET",
cache: "no-store",
credentials: "same-origin",
headers: { Accept: "application/json" },
signal
});
if (!response.ok) throw new Error(`认证状态请求失败(${response.status}`);
return parseCurrentSession(await response.json());
}
export function safeBrowserNext(value: string | null | undefined, fallback = "/create"): string {
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return fallback;
try {
const base = "http://zhinian.local";
const parsed = new URL(value, base);
if (parsed.origin !== base) return fallback;
const path = `${parsed.pathname}${parsed.search}${parsed.hash}`;
if (parsed.pathname.startsWith("/api/auth") || parsed.pathname.startsWith("/auth/login") || parsed.pathname.startsWith("/auth/admin-login")) {
return fallback;
}
return path;
} catch {
return fallback;
}
}
export function safeBrowserLocationNext(
location: Pick<Location, "pathname" | "search" | "hash">,
fallback = "/create"
): string {
return safeBrowserNext(`${location.pathname}${location.search}${location.hash}`, fallback);
}
function parseCurrentSession(value: unknown): BrowserCurrentSession {
if (!isRecord(value) || typeof value.authenticated !== "boolean" || typeof value.authRequired !== "boolean" || typeof value.authConfigured !== "boolean") {
throw new Error("认证状态响应格式无效");
}
if (!value.authenticated) {
if (value.authMode !== null || value.user !== null) throw new Error("认证状态响应格式无效");
return {
authenticated: false,
authRequired: value.authRequired,
authConfigured: value.authConfigured,
authMode: null,
user: null
};
}
if (!value.authConfigured || (value.authMode !== "user" && value.authMode !== "admin") || !isAuthUser(value.user)) {
throw new Error("认证用户响应格式无效");
}
const expectedMode: AuthMode = value.user.role === "user" ? "user" : "admin";
if (value.authMode !== expectedMode) throw new Error("认证用户响应格式无效");
return {
authenticated: true,
authRequired: value.authRequired,
authConfigured: value.authConfigured,
authMode: value.authMode,
user: value.user
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isAuthUser(value: unknown): value is AuthUser {
return isRecord(value) &&
typeof value.id === "string" &&
value.id.length > 0 &&
typeof value.subject === "string" &&
value.subject === value.id &&
typeof value.displayName === "string" &&
typeof value.clientId === "string" &&
(value.role === "user" || value.role === "organization_admin" || value.role === "super_admin") &&
value.status === "active" &&
(value.role === "super_admin" || (typeof value.organizationId === "string" && value.organizationId.length > 0)) &&
Array.isArray(value.authorities) &&
value.authorities.every((item) => typeof item === "string") &&
Array.isArray(value.scope) &&
value.scope.every((item) => typeof item === "string");
}