Files
xh-medical-sign-web/clinical-web/src/stores/login.ts
T

233 lines
6.7 KiB
TypeScript

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import {
changePassword as changePasswordRequest,
getCurrentAuthorization,
getCurrentUser,
login as loginRequest,
logout as logoutRequest,
} from '@/api/auth'
import type {
AuthenticatedUser,
LoginRequest,
LoginResponse,
CurrentAuthorization,
PermissionResourceNode,
PasswordChangeRequest,
} from '@/types/auth'
import { clearAuthStorage, readStoredAuthSession, saveAuthSession } from '@/utils/auth-storage'
export const useLoginStore = defineStore('login', () => {
const storedSession = readStoredAuthSession()
const token = ref(storedSession?.token ?? '')
const expiresAt = ref(storedSession?.expiresAt ?? '')
const user = ref<AuthenticatedUser | null>(storedSession?.user ?? null)
const authorization = ref<CurrentAuthorization | null>(null)
const currentUserReady = ref(false)
let currentUserRequest: Promise<AuthenticatedUser> | null = null
let authorizationSyncedAt = 0
const isLoggedIn = computed(() => Boolean(token.value))
// Data scope belongs to roles now; it no longer determines whether a user can write.
const isReadOnly = computed(() => false)
const userName = computed(() => user.value?.displayName || user.value?.username || '医护用户')
const department = computed(() => user.value?.departmentName || '医签通用户')
function hasPermission(required: string | string[]) {
const requiredPermissions = Array.isArray(required) ? required : [required]
if (!requiredPermissions.length) {
return true
}
const permissions = authorization.value?.permissions ?? user.value?.permissions ?? []
return requiredPermissions.every(
(permission) => permissions.includes(permission) || permissions.includes('*'),
)
}
/**
* 已下线菜单:后端菜单资源里仍然下发、但前端已经不再提供页面的菜单。
*
* 「系统管理 → 报表分析」(`/report/index`)已整体下线,后端菜单资源暂时无法同步下架,
* 所以在前端统一屏蔽:侧边栏不展示,路由守卫也会把它当成无权限菜单重定向走,
* 避免出现点进去只有「前端暂未接入组件」占位页的死链。
* 后端下架该菜单资源后,这个集合可以删掉。
*/
const RETIRED_MENU_PATHS = new Set(['/report/index'])
function isRetiredMenu(menu: PermissionResourceNode) {
return RETIRED_MENU_PATHS.has(menu.routePath?.replace(/\/$/, '') ?? '')
}
const accessibleMenus = computed(() =>
(authorization.value?.navigation ?? [])
.filter((directory) => directory.resourceType === 'DIR' && directory.status === 'ENABLED')
.flatMap((directory) => directory.children ?? [])
.filter((menu) => !isRetiredMenu(menu)),
)
/** 侧边栏用的导航树:结构与后端一致,只剔除已下线菜单(空目录由使用方过滤) */
const visibleNavigation = computed(() =>
(authorization.value?.navigation ?? [])
.filter((directory) => directory.resourceType === 'DIR' && directory.status === 'ENABLED')
.map((directory) => ({
...directory,
children: (directory.children ?? []).filter((menu) => !isRetiredMenu(menu)),
})),
)
function menuForPath(path: string): PermissionResourceNode | null {
const normalizedPath = path.replace(/\/$/, '') || '/'
return (
accessibleMenus.value.find((menu) => menu.routePath?.replace(/\/$/, '') === normalizedPath) ??
null
)
}
const firstAuthorizedPath = computed(
() =>
accessibleMenus.value.find(
(menu) =>
menu.visible && menu.status === 'ENABLED' && menu.code && hasPermission(menu.code),
)?.routePath ?? null,
)
function hasWritePermission(required: string | string[]) {
return !isReadOnly.value && hasPermission(required)
}
function setSession(session: LoginResponse) {
saveAuthSession(session)
token.value = session.token
expiresAt.value = session.expiresAt
user.value = session.user
authorization.value = null
authorizationSyncedAt = 0
currentUserReady.value = false
}
function setCurrentUser(nextUser: AuthenticatedUser) {
user.value = nextUser
if (token.value) {
saveAuthSession({
token: token.value,
expiresAt: expiresAt.value,
user: nextUser,
})
}
}
function clearSession() {
clearAuthStorage()
token.value = ''
expiresAt.value = ''
user.value = null
authorization.value = null
authorizationSyncedAt = 0
currentUserReady.value = false
currentUserRequest = null
}
async function login(payload: LoginRequest) {
const session = await loginRequest(payload)
setSession(session)
return session
}
async function refreshCurrentUser() {
const [currentUser, currentAuthorization] = await Promise.all([
getCurrentUser(),
getCurrentAuthorization(),
])
const effectiveUser = { ...currentUser, permissions: currentAuthorization.permissions }
authorization.value = currentAuthorization
authorizationSyncedAt = Date.now()
setCurrentUser(effectiveUser)
return effectiveUser
}
async function refreshAuthorization() {
const currentAuthorization = await getCurrentAuthorization()
authorization.value = currentAuthorization
authorizationSyncedAt = Date.now()
if (user.value) {
setCurrentUser({ ...user.value, permissions: currentAuthorization.permissions })
}
return currentAuthorization
}
async function ensureCurrentUser() {
if (!token.value) {
return null
}
if (
currentUserReady.value &&
user.value &&
authorization.value &&
Date.now() - authorizationSyncedAt < 10_000
) {
return user.value
}
if (!currentUserRequest) {
currentUserRequest = refreshCurrentUser()
.then((currentUser) => {
currentUserReady.value = true
return currentUser
})
.catch((error: unknown) => {
currentUserReady.value = false
throw error
})
.finally(() => {
currentUserRequest = null
})
}
return currentUserRequest
}
async function logout() {
try {
await logoutRequest()
} finally {
clearSession()
}
}
async function changePassword(payload: PasswordChangeRequest) {
await changePasswordRequest(payload)
// 服务端会递增 authEpoch;当前令牌随即失效,必须使用新密码重新登录。
clearSession()
}
return {
token,
expiresAt,
user,
authorization,
accessibleMenus,
visibleNavigation,
isLoggedIn,
isReadOnly,
userName,
department,
login,
refreshCurrentUser,
ensureCurrentUser,
refreshAuthorization,
menuForPath,
firstAuthorizedPath,
hasPermission,
hasWritePermission,
changePassword,
logout,
clearSession,
}
})