Files
client-web/src/stores/auth.ts
T

202 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-07-30 17:59:42 +08:00
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { CurrentUser } from '@chongchong/shared'
import { loginMerchant, logoutMerchant } from '@/api/auth'
import { getMerchantPermissionResources, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, type MerchantPermissionResource } from '@/api/merchantIam'
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair } from '@/api/session'
import { 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: Readonly<Record<string, readonly string[]>> = {
'merchant:profile:view': ['company:profile:view'],
'merchant:profile:edit': ['company:profile:edit'],
'merchant:qualification:upload': ['company:qualification:upload'],
'merchant:member:list': ['company:member:list'],
'merchant:member:create': ['company:member:write'],
'merchant:member:edit': ['company:member:write'],
'merchant:member:grant-role': ['company:member:write'],
'merchant:member:status': ['company:member:write'],
'merchant:member:reset-password': ['company:member:reset-password'],
'merchant:role:list': ['company:role:list', 'company:permission:list'],
'merchant:role:create': ['company:role:write'],
'merchant:role:edit': ['company:role:write'],
'merchant:role:status': ['company:role:write'],
'merchant:role:delete': ['company:role:write'],
'merchant:role:grant': ['company:permission:list', 'company:permission:assign'],
'merchant:order:list': ['order:first-leg:list'],
'merchant:order:view': ['order:first-leg:list'],
'merchant:order:create': ['order:first-leg:create'],
'merchant:order:edit': ['order:first-leg:edit'],
'merchant:order:submit': ['order:first-leg:submit'],
}
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] : ['企业成员'],
}
}
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) {
clearSession()
throw error
}
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) {
clearSession()
throw error
}
}
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 }
})