UNPKG

@ithinkdt/core

Version:

iThinkDT Core

336 lines (295 loc) 10.8 kB
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) }, } }