优化构架
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { CurrentUser } from '@chongchong/shared'
|
||||
import { loginMerchant, logoutMerchant } from '@/api/auth'
|
||||
import { ApiError } from '@/api/http'
|
||||
import { getMerchantPermissionResources, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, type MerchantPermissionResource } from '@/api/merchantIam'
|
||||
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair } from '@/api/session'
|
||||
import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, TENANT_PERMISSION_GROUPS } from '@/stores/tenantAdministration'
|
||||
import { decodeMerchantToken, type MerchantJwtPayload } from '@/utils/jwt'
|
||||
|
||||
const frontendPermissionCodes = new Set(TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)))
|
||||
const backendPermissionMapping = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
.reduce<Record<string, string[]>>((mapping, [frontendCode, backendCodes]) => {
|
||||
for (const backendCode of backendCodes) {
|
||||
if (!mapping[backendCode]) mapping[backendCode] = []
|
||||
mapping[backendCode].push(frontendCode)
|
||||
}
|
||||
return mapping
|
||||
}, {})
|
||||
|
||||
export interface ClientAccountProfile {
|
||||
username: string
|
||||
realName: string
|
||||
phone: string
|
||||
email: string
|
||||
tenantName: string
|
||||
}
|
||||
|
||||
const emptyProfile: ClientAccountProfile = {
|
||||
username: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
tenantName: '',
|
||||
}
|
||||
|
||||
function effectiveBackendPermissions(payload: MerchantJwtPayload, resources?: MerchantPermissionResource[]) {
|
||||
const jwtPermissions = new Set(payload.permissions ?? [])
|
||||
if (!resources) return jwtPermissions
|
||||
const activeResources = new Set(resources.filter((item) => item.status === 'ACTIVE').map((item) => item.permissionCode))
|
||||
return new Set([...jwtPermissions].filter((permission) => activeResources.has(permission)))
|
||||
}
|
||||
|
||||
function mapPermissions(payload: MerchantJwtPayload, resources?: MerchantPermissionResource[]) {
|
||||
const backendPermissions = effectiveBackendPermissions(payload, resources)
|
||||
const result = new Set<string>(['dashboard:view', 'account:profile:view'])
|
||||
for (const permission of backendPermissions) {
|
||||
if (frontendPermissionCodes.has(permission)) result.add(permission)
|
||||
for (const mapped of backendPermissionMapping[permission] ?? []) result.add(mapped)
|
||||
}
|
||||
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: payload.roles?.length ? [...payload.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<MerchantJwtPayload | null>(null)
|
||||
const user = ref<CurrentUser | null>(null)
|
||||
const profile = ref<ClientAccountProfile>({ ...emptyProfile })
|
||||
const permissionCodes = ref<string[]>([])
|
||||
const sessionRestored = ref(false)
|
||||
|
||||
function applyToken(accessToken: string, resources?: MerchantPermissionResource[]) {
|
||||
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(decoded, resources)
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
clearSessionTokens()
|
||||
token.value = ''
|
||||
payload.value = null
|
||||
user.value = null
|
||||
permissionCodes.value = []
|
||||
profile.value = { ...emptyProfile }
|
||||
sessionRestored.value = true
|
||||
}
|
||||
|
||||
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 [resources, tenant, personalProfile] = await Promise.all([
|
||||
getMerchantPermissionResources(),
|
||||
getTenantProfile(),
|
||||
getMerchantPersonalProfile(),
|
||||
])
|
||||
permissionCodes.value = mapPermissions(payload.value, resources)
|
||||
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(username: string, password: string, remember = false) {
|
||||
const tokens = await loginMerchant(username, 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(decoded)
|
||||
sessionRestored.value = false
|
||||
try {
|
||||
await loadSessionContext()
|
||||
sessionRestored.value = true
|
||||
} 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<ClientAccountProfile, 'realName' | 'phone' | 'email'>) {
|
||||
profile.value = { ...profile.value, ...structuredClone(next) }
|
||||
if (user.value) user.value = { ...user.value, name: next.realName }
|
||||
}
|
||||
|
||||
async function savePersonalProfile(next: Pick<ClientAccountProfile, 'realName' | 'phone' | 'email'>) {
|
||||
const updated = await updateMerchantPersonalProfile({
|
||||
displayName: next.realName.trim(),
|
||||
phone: next.phone.trim(),
|
||||
email: next.email.trim(),
|
||||
})
|
||||
updateProfile({
|
||||
realName: updated.displayName,
|
||||
phone: updated.phone || '',
|
||||
email: updated.email || '',
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('cc-client-session-expired', clearSession)
|
||||
|
||||
return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, restoreSession, updateProfile, savePersonalProfile }
|
||||
})
|
||||
Reference in New Issue
Block a user