94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
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");
|
||
}
|