export interface LoginByUsernameResponse { access_token: string; token_type: string; expires_in: number; user_id: string; email: string; system_role: string; needs_setup: boolean; created: boolean; /** * 本次登录解析出的原始登录用户名(token 登录=getTokenInfo 解析值;用户名登录=所传用户名)。 * 用于跳转外链时拼 ``?username=``——后端落库的邮箱前缀是归一化(小写/替换)后的,有损,故以此为准。 * 旧后端可能不返回。 */ username?: string; } import { authStorageGet, authStorageRemove, authStorageSet, } from "@/core/auth/scoped-storage"; const AUTH_STORAGE_KEY = "deerflow.auth"; /** Fired in the current tab because the browser `storage` event only reaches other tabs. */ export const AUTH_STATE_CHANGED_EVENT = "deerflow:auth-state-changed"; function isBrowser(): boolean { return typeof window !== "undefined"; } export function getStoredAuth(): LoginByUsernameResponse | null { if (!isBrowser()) { return null; } // 嵌入(iframe)态走 sessionStorage、正常态走 localStorage —— 隔离 iframe 登录用户与正常登录用户。 const raw = authStorageGet(AUTH_STORAGE_KEY); if (!raw) { return null; } try { return JSON.parse(raw) as LoginByUsernameResponse; } catch { return null; } } function normalizeIdentityPart(value: unknown): string { return typeof value === "string" ? value.trim().toLowerCase() : ""; } function identityFromLoginName(value: unknown): string | null { const normalized = normalizeIdentityPart(value); if (!normalized) return null; // Explicit email login is domain-specific. Bare username login is the // stable business principal even if the backend's synthetic email domain or // internal UUID changes. return normalized.includes("@") ? `email:${normalized}` : `account:${normalized}`; } function readUserInfoIdentity(): string | null { let info: Record | null = null; try { const raw = authStorageGet("userInfo"); if (raw) { const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { info = parsed as Record; } } } catch { info = null; } if (!info) return null; const candidates = [ ["yUserId", info.yUserId], ["userName", info.userName], ["userId", info.userId], ] as const; for (const [prefix, value] of candidates) { const normalized = normalizeIdentityPart(value); if (normalized) return `${prefix}:${normalized}`; } return null; } /** * A stable cross-tab identity key for "did the account really change?" checks. * * Some external login paths can re-issue a backend ``user_id`` or use a * different synthetic email domain while resolving to the same business * account. Prefer the raw login username returned by the backend, then the * email local-part, and keep ``user_id`` only as the final fallback. */ export function getAccountIdentityKey( auth: LoginByUsernameResponse | null = getStoredAuth(), ): string | null { if (!auth) return null; const loginIdentity = identityFromLoginName(auth.username); if (loginIdentity) return loginIdentity; const email = normalizeIdentityPart(auth.email); if (email) { const localPart = email.split("@", 1)[0]; return localPart ? `account:${localPart}` : `email:${email}`; } const userInfoIdentity = readUserInfoIdentity(); if (userInfoIdentity) return `userInfo:${userInfoIdentity}`; const userId = normalizeIdentityPart(auth.user_id); return userId ? `user_id:${userId}` : null; } export function setStoredAuth(auth: LoginByUsernameResponse) { if (!isBrowser()) { return; } authStorageSet(AUTH_STORAGE_KEY, JSON.stringify(auth)); window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT)); } export function clearStoredAuth() { if (!isBrowser()) { return; } authStorageRemove(AUTH_STORAGE_KEY); window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT)); } /** 与设置 → 个人信息页「用户名」一致:邮箱 @ 前缀,否则 user_id。 */ export function getAccountDisplayName( auth: LoginByUsernameResponse | null = getStoredAuth(), ): string { if (!auth) return "用户"; const usernameFromEmail = (auth.email ?? "").split("@")[0] ?? ""; return usernameFromEmail || auth.user_id || "用户"; } /** * 把登录用户名写入 localStorage.userInfo.userName(轻应用登录参数等场景使用)。 * 仅在尚未设置时写入,避免覆盖外部登录(yUserId)从 consumer 拿到的用户名。 */ export function setStoredUserName(name: string | null | undefined) { if (!isBrowser()) { return; } const trimmed = name?.trim(); if (!trimmed) { return; } let info: Record = {}; try { const raw = authStorageGet("userInfo"); if (raw) { const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object") { info = parsed as Record; } } } catch { info = {}; } if (typeof info.userName === "string" && info.userName.trim()) { return; } info.userName = trimmed; authStorageSet("userInfo", JSON.stringify(info)); } export function getAccessToken(): string | undefined { const auth = getStoredAuth(); const token = auth?.access_token?.trim(); return token ? token : undefined; } export function getAuthorizationHeaderValue(): string | undefined { const auth = getStoredAuth(); const token = auth?.access_token?.trim(); if (!token) { return undefined; } const rawType = auth?.token_type?.trim(); const tokenType = rawType ? rawType : "Bearer"; return `${tokenType} ${token}`; }