import { computed, ref } from 'vue' import { defineStore } from 'pinia' import type { CurrentUser } from '@chongchong/shared' import { exchangeImpersonationCode, loginMerchant, logoutMerchant } from '@/modules/auth/api/auth' import { ApiError } from '@/api/http' import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, hasMappedMerchantPermission, getCurrentMerchantPermissionCodes, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, } from '@/modules/tenant-management/public' import { clearSessionTokens, getAccessToken, getRefreshToken, persistTemporaryAccessToken, persistTokenPair } from '@/api/session' import { decodeMerchantToken, type MerchantJwtPayload } from '@/modules/auth/utils/jwt' export interface ClientAccountProfile { username: string realName: string phone: string email: string tenantName: string } const emptyProfile: ClientAccountProfile = { username: '', realName: '', phone: '', email: '', tenantName: '', } function mapPermissions(backendPermissionCodes: string[] = []) { const backendPermissions = new Set(backendPermissionCodes) const result = new Set(['dashboard:view', 'account:profile:view']) for (const [frontendCode] of Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)) { if (hasMappedMerchantPermission(frontendCode, backendPermissions)) result.add(frontendCode) } return [...result] } function currentUser(payload: MerchantJwtPayload, tenantName?: string): CurrentUser { return { id: String(payload.user_id), name: payload.username, account: payload.username, tenantName: tenantName || `企业租户 ${payload.tenant_id}`, roles: [], } } function isExpiredSessionError(error: unknown) { return error instanceof ApiError && (error.status === 401 || error.code === 'A0401') } export const useAuthStore = defineStore('client-auth', () => { const token = ref(getAccessToken()) const payload = ref(null) const user = ref(null) const profile = ref({ ...emptyProfile }) const permissionCodes = ref([]) const sessionRestored = ref(false) function applyToken(accessToken: string) { const decoded = decodeMerchantToken(accessToken) payload.value = decoded token.value = accessToken user.value = currentUser(decoded, user.value?.tenantName) profile.value.username = decoded.username profile.value.realName = decoded.username permissionCodes.value = mapPermissions() } function clearSession() { clearSessionTokens() token.value = '' payload.value = null user.value = null permissionCodes.value = [] profile.value = { ...emptyProfile } sessionRestored.value = true } function applyStoredSession() { const storedToken = getAccessToken() if (!storedToken) { token.value = '' payload.value = null user.value = null permissionCodes.value = [] profile.value = { ...emptyProfile } sessionRestored.value = true return } try { applyToken(storedToken) sessionRestored.value = false } catch { clearSession() } } if (token.value) { try { applyToken(token.value) } catch { clearSession() } } const isAuthenticated = computed(() => Boolean(token.value && payload.value)) async function loadSessionContext() { if (!payload.value) return const currentPermissionCodes = await getCurrentMerchantPermissionCodes() const currentAccessToken = getAccessToken() if (currentAccessToken && currentAccessToken !== token.value) applyToken(currentAccessToken) permissionCodes.value = mapPermissions(currentPermissionCodes) const [tenant, personalProfile] = await Promise.all([ getTenantProfile(), getMerchantPersonalProfile(), ]) user.value = currentUser(payload.value, tenant.tenantName) profile.value = { ...profile.value, realName: personalProfile.displayName || payload.value.username, phone: personalProfile.phone || '', email: personalProfile.email || '', tenantName: tenant.tenantName, } } async function restoreSession() { if (sessionRestored.value) return if (!isAuthenticated.value) { sessionRestored.value = true return } try { await loadSessionContext() } catch (error) { if (isExpiredSessionError(error)) { clearSession() throw error } sessionRestored.value = true return } sessionRestored.value = true } async function login(loginId: string, password: string, remember = false) { const tokens = await loginMerchant(loginId, password) const decoded = decodeMerchantToken(tokens.accessToken) persistTokenPair(tokens, remember) token.value = tokens.accessToken payload.value = decoded user.value = currentUser(decoded) profile.value = { ...emptyProfile, username: decoded.username, realName: decoded.username } permissionCodes.value = mapPermissions() sessionRestored.value = false try { await loadSessionContext() sessionRestored.value = true } catch (error) { if (isExpiredSessionError(error)) { clearSession() throw error } sessionRestored.value = true } } async function acceptImpersonationCode(code: string) { const exchanged = await exchangeImpersonationCode(code) const decoded = decodeMerchantToken(exchanged.accessToken) persistTemporaryAccessToken(exchanged.accessToken) token.value = exchanged.accessToken payload.value = decoded user.value = currentUser(decoded) profile.value = { ...emptyProfile, username: decoded.username, realName: decoded.username } permissionCodes.value = mapPermissions() sessionRestored.value = false try { await loadSessionContext() } catch (error) { if (isExpiredSessionError(error)) { clearSession() throw error } } sessionRestored.value = true } async function logout() { const refreshToken = getRefreshToken() try { if (refreshToken) await logoutMerchant(refreshToken) } finally { clearSession() } } function hasPermission(permission?: string) { return !permission || permissionCodes.value.includes(permission) } function updateProfile(next: Pick) { profile.value = { ...profile.value, ...structuredClone(next) } if (user.value) user.value = { ...user.value, name: next.realName } } async function savePersonalProfile(next: Pick) { const updated = await updateMerchantPersonalProfile({ displayName: next.realName.trim(), email: next.email.trim(), }) updateProfile({ realName: updated.displayName, phone: updated.phone || '', email: updated.email || '', }) } window.addEventListener('cc-client-session-expired', clearSession) window.addEventListener('storage', (event) => { if (event.key === 'cc-client-access-token' || event.key === 'cc-client-refresh-token') applyStoredSession() }) return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, clearSession, restoreSession, acceptImpersonationCode, updateProfile, savePersonalProfile } })