@ithinkdt/core
Version:
iThinkDT Core
336 lines (295 loc) • 10.8 kB
JavaScript
import { toRef, ref, watch, computed } from 'vue'
import { promiseTimeout, until, syncRef } from '@vueuse/core'
import { walkTree } from '@ithinkdt/common'
import { STORE_PREFIX } from '../store'
import { $dialog } from '../feedback'
import { jwt as _jwt } from './jwt.interceptor'
import { initRouter, Route } from './router'
import { getFirstPageModule, initAuthInfo } from './service'
export let useAuth = () => {
throw new Error('未初始化 Auth 模块,不能调用 useAuth')
}
export let msgHandler = {
loadMessages: () =>
Promise.resolve({
total: 0,
records: [],
}),
}
export let auth
/**
* @type {import('vue-router').Router}
*/
export let router
export let $can
export function initAuth({
jwt: jwtInit,
domain,
router: routerInit,
loadMessages,
readMessages,
deleteMessages,
locale,
loadAppInfo,
sso,
logout,
...authInfoInit
}) {
msgHandler = {
loadMessages,
readMessages,
deleteMessages,
}
const logged = ref(false)
function setToken(token) {
if (auth.token === token) return
if (token) {
auth.token = token
} else {
const href = router.resolve({
name: Route.LOGOUT,
query: auth.user ? { username: auth.user.username, appCode: auth.app.appCode } : {},
}).href
auth.$reset()
setTimeout(() => {
location.replace(href)
}, 300)
}
return auth.load(true)
}
const getToken = () => {
return auth?.token
}
router = initRouter({
...routerInit,
index:
routerInit.index ??
(() => {
return getFirstPageModule(auth.modules ?? [])?.path ?? '/index'
}),
logged: logged,
getToken,
setToken,
logout: () => auth.logout(),
sso,
getAuthErrorType: () => auth.error,
})
const lastRoutes = []
let $init
useAuth = initAuthInfo({
...authInfoInit,
loadAuthInfo: (...params) => {
return ($init = authInfoInit.loadAuthInfo(...params).then((result) => {
for (const r of lastRoutes) {
router.removeRoute(r)
}
lastRoutes.length = 0
const remoteUrls = new Set()
walkTree(result.modules, (m) => {
if (m.type === 'federate') {
remoteUrls.add(m.remote)
} else if (m.type === 'lowcode') {
lastRoutes.push('/lowcode' + m.path)
router.addRoute({
name: '/lowcode' + m.path,
path: m.path,
component: routerInit.Lowcode,
props: (route) => ({
...route.params,
ithinkdtLcPageKey: m.lcKey,
ithinkdtLcAppCode: m.remote,
}),
})
}
})
return Promise.allSettled(
[...remoteUrls].map(async (url) => {
try {
const m = await import(/* @vite-ignore */ url)
for (const r of m.default) router.addRoute(r)
} catch (error) {
console.error(`[auth] load federate remote app routes error!`, error)
}
}),
).then(() => result)
}))
},
logout: sso
? undefined
: async (...params) => {
const ret = await logout?.(...params)
Promise.resolve(loadAppInfo(locale.value)).then((app) => {
auth.app = app
})
return ret
},
router,
locale,
})
const _can = (action) => {
return auth.permissions[action] || false
}
function can(...actions) {
if (actions?.length > 1) {
return actions.some((act) => _can(act))
}
return _can(actions[0])
}
function checkLogin(to) {
if (auth.logged) {
return
}
console.debug('[router] requires user login, redirect to login page.')
const redirect = to.matched[0]?.name === Route.INDEX ? undefined : to.href
return {
name: Route.LOGIN,
query: redirect
? {
redirect: encodeURIComponent(redirect),
}
: undefined,
}
}
let fromModule
router.beforeEach((to) => {
if ($init) {
return $init.then(() => {
$init = undefined
return to.fullPath
})
}
console.debug(`[router] route to '${to.fullPath}'...`, to)
fromModule = auth.currentModule || auth.fromModule
if (to.meta?.requiresLogin === false) {
console.debug('[router] not requires user login.')
return auth.logged ? auth.load() : true
}
const check = checkLogin(to)
if (check) return check
return auth.load().catch((error) => {
console.error('[router] error when loading user info!', error)
const check = checkLogin(to)
if (check) return check
// TODO 调整方式
return {
path: '/500',
query: {
msg: error?.message ?? error,
},
}
})
})
const findAuth = (auths) => {
return auths.find((s) => auth.permissions[s.trim()] || auth.moduleMap[s.trim()])
}
router.afterEach(async (to, _from, failure) => {
auth.fromModule = fromModule
if (failure) return
if (to.meta?.requiresLogin === false || to.meta?.requiresAuth === false) {
console.debug(`[auth] not requires auth access, path: ${to.fullPath}`)
} else {
let requires = to.meta?.requiresAuth
if (requires && typeof requires !== 'boolean')
requires = typeof requires === 'string' ? requires.split(',') : requires
if (
to.path !== '/' &&
(requires?.length
? !findAuth(requires)
: !auth.moduleMap[to.path] && !auth.moduleMap[to.matched.at(-1)?.path ?? ''])
) {
console.debug(`[auth] not found module '${to.fullPath}' in user auth access.`)
to.__DT_ROUTE_ERROR = '403'
} else if (to.matched?.find((route) => route?.meta.__DT_ROUTE_LOAD_FAILURED)) {
to.__DT_ROUTE_ERROR = 'LOAD_FAILURED'
}
}
})
const jwt = jwtInit ? _jwt(router, getToken, setToken, jwtInit) : undefined
function _init() {
auth = useAuth()
jwt?._init()
if (domain) {
let timer
watch(
() => auth.user?.username,
(name) => {
if (name) {
// eslint-disable-next-line unicorn/no-document-cookie
document.cookie = `${STORE_PREFIX}auth=${encodeURIComponent(btoa(name))}; expires=${
Date.now() + 86_400_000
}; domain=${domain}; path=/; Strict`
} else {
timer !== undefined && clearInterval(timer)
// eslint-disable-next-line unicorn/no-document-cookie
document.cookie = `${STORE_PREFIX}auth=; expires=${
Date.now() - 60_000
}; domain=${domain}; path=/; Strict`
}
},
{ immediate: true, flush: 'sync' },
)
const reg = new RegExp(`(^| )${STORE_PREFIX}auth=([^;]*)(;|$)`)
const checkUser = () => {
const cookieUser = reg.exec(document.cookie)?.[2]
if (!cookieUser || window.atob(decodeURIComponent(cookieUser)) !== auth.user.username) {
console.debug(
`user is expired, user: ${auth.user.username}, cookie user: ${cookieUser}, origin: ${location.origin}, referrer: ${document.referrer}`,
)
return false
}
}
until(computed(() => auth.user?.username))
.toBeTruthy()
.then(() => {
if (auth.user?.username && checkUser() === false) {
auth.$reset()
promiseTimeout(30).then(() => {
location.reload()
})
} else {
timer = setInterval(() => {
if (auth.user?.username && checkUser() !== false) return
clearInterval(timer)
timer = undefined
$dialog({
type: 'error',
title: '账号冲突',
content: '您的账号可能被其他窗口登出,即将刷新此页面。。',
cancelText: false,
okText: '刷 新',
onOk() {
auth.$reset()
return promiseTimeout(30).then(() => {
location.reload()
})
},
})
}, 300)
}
})
}
syncRef(toRef(auth, 'logged'), logged, { direction: 'ltr' })
watch(
locale,
async () => {
console.debug('[auth] locale changed, reload user & app info.')
auth.token &&
auth.load(true).catch(() => {
// no empty
})
auth.app = (await loadAppInfo(locale.value)) ?? auth.app
},
{ immediate: true },
)
}
return {
router,
jwt,
install(app) {
if (!auth) _init()
app.config.globalProperties.$can = $can = can
app.provide('__dt_access__', can)
app.use(router)
},
}
}