refactor: serve static frontend with Go APIs
This commit is contained in:
93
lib/client/browser-auth.ts
Normal file
93
lib/client/browser-auth.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user