202 lines
7.3 KiB
TypeScript
202 lines
7.3 KiB
TypeScript
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 }
|
||
|
|
})
|