Files
client-web/src/modules/auth/store/auth.ts
T

245 lines
7.7 KiB
TypeScript

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, refreshAuthenticatedSession } from '@/api/http'
import {
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
hasMappedMerchantPermission,
getCurrentMerchantPermissionCodes,
getMerchantPersonalProfile,
getTenantProfile,
updateMerchantPersonalProfile,
} from '@/modules/tenant-management/public'
import { clearSessionTokens, getAccessToken, getRefreshToken, persistRememberedLoginId, persistTemporaryAccessToken, persistTokenPair } from '@/api/session'
import { decodeMerchantToken, MerchantTokenExpiredError, 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<string>(['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<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) {
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 (error) {
if (!(error instanceof MerchantTokenExpiredError && getRefreshToken())) 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) {
if (!getRefreshToken()) {
sessionRestored.value = true
return
}
try {
applyToken(await refreshAuthenticatedSession())
} catch (error) {
if (isExpiredSessionError(error)) {
clearSession()
throw error
}
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)
persistRememberedLoginId(loginId, 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<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' | 'email'>) {
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 }
})