import Keycloak, { type KeycloakConfig, type KeycloakInitOptions, type KeycloakLoginOptions, type KeycloakLogoutOptions, type KeycloakTokenParsed } from "keycloak-js"; import type { RuntimeConfig } from "@/shared/config/env"; const TOKEN_MIN_VALIDITY_SECONDS = 30; export type AuthenticatedUser = { name: string; role: string; }; export type AccessTokenProvider = () => Promise; export type Authentication = { enabled: boolean; user: AuthenticatedUser | null; getAccessToken: AccessTokenProvider; logout?: () => Promise; }; type KeycloakClient = { token?: string; tokenParsed?: KeycloakTokenParsed; idTokenParsed?: KeycloakTokenParsed; onTokenExpired?: () => void; init: (options: KeycloakInitOptions) => Promise; login: (options?: KeycloakLoginOptions) => Promise; logout: (options?: KeycloakLogoutOptions) => Promise; updateToken: (minValidity: number) => Promise; }; type KeycloakClientFactory = (config: KeycloakConfig) => KeycloakClient; export async function initializeAuthentication( config: RuntimeConfig, createClient: KeycloakClientFactory = (keycloakConfig) => new Keycloak(keycloakConfig) ): Promise { if (config.TJWATER_AUTH_MODE === "disabled") { return { enabled: false, user: null, getAccessToken: async () => null }; } const keycloak = createClient( toKeycloakConfig( config.TJWATER_KEYCLOAK_ISSUER, config.TJWATER_KEYCLOAK_CLIENT_ID ) ); const redirectUri = `${window.location.origin}/`; const login = () => keycloak.login({ redirectUri }); const authenticated = await keycloak.init({ onLoad: "login-required", flow: "standard", pkceMethod: "S256" }); if (!authenticated) { await login(); throw new Error("Keycloak authentication was not completed"); } const refreshAccessToken = async () => { try { await keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS); } catch (error) { await login(); throw error; } }; keycloak.onTokenExpired = () => { void refreshAccessToken().catch(() => undefined); }; return { enabled: true, user: toAuthenticatedUser(keycloak.idTokenParsed ?? keycloak.tokenParsed), getAccessToken: async () => { await refreshAccessToken(); if (!keycloak.token) { throw new Error("Keycloak access token is unavailable"); } return keycloak.token; }, logout: () => keycloak.logout({ redirectUri }) }; } export function toKeycloakConfig(issuer: string, clientId: string): KeycloakConfig { const url = new URL(issuer); const realmMarker = "/realms/"; const markerIndex = url.pathname.lastIndexOf(realmMarker); const realm = decodeURIComponent(url.pathname.slice(markerIndex + realmMarker.length)); const serverPath = url.pathname.slice(0, markerIndex).replace(/\/+$/, ""); return { url: `${url.origin}${serverPath}`, realm, clientId }; } export function toAuthenticatedUser(token: KeycloakTokenParsed | undefined): AuthenticatedUser { const name = readClaim(token, "name") ?? readClaim(token, "preferred_username") ?? readClaim(token, "sub") ?? "已认证用户"; return { name, role: readClaim(token, "email") ?? "统一认证用户" }; } function readClaim(token: KeycloakTokenParsed | undefined, key: string) { const value = token?.[key]; return typeof value === "string" && value.trim() ? value.trim() : undefined; }