From 37d20b8e0d0e7f29bdec27d081ceca3d8df20d55 Mon Sep 17 00:00:00 2001 From: Soybean Date: Mon, 25 Mar 2024 02:42:50 +0800 Subject: [PATCH] refactor(projects): new route guard --- .env | 2 +- src/router/elegant/routes.ts | 12 +- src/router/guard/index.ts | 6 +- src/router/guard/permission.ts | 142 --------------------- src/router/guard/route.ts | 196 +++++++++++++++++++++++++++++ src/router/index.ts | 6 +- src/router/routes/builtin.ts | 31 +++++ src/router/routes/index.ts | 35 ++---- src/service/api/route.ts | 5 + src/store/modules/route/index.ts | 81 ++++++++---- src/store/modules/route/shared.ts | 8 -- src/views/_builtin/login/index.vue | 6 +- 12 files changed, 316 insertions(+), 214 deletions(-) delete mode 100644 src/router/guard/permission.ts create mode 100644 src/router/guard/route.ts create mode 100644 src/router/routes/builtin.ts diff --git a/.env b/.env index 3f7b9be8..a9f85cf7 100644 --- a/.env +++ b/.env @@ -12,7 +12,7 @@ VITE_ICON_PREFIX=icon VITE_ICON_LOCAL_PREFIX=icon-local # auth route mode: static | dynamic -VITE_AUTH_ROUTE_MODE=dynamic +VITE_AUTH_ROUTE_MODE=static # static auth route home VITE_ROUTE_HOME=home diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index c96de374..cf451171 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -13,7 +13,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: '403', i18nKey: 'route.403', - constant: true + constant: true, +hideInMenu: true } }, { @@ -23,7 +24,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: '404', i18nKey: 'route.404', - constant: true + constant: true, +hideInMenu: true } }, { @@ -33,7 +35,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: '500', i18nKey: 'route.500', - constant: true + constant: true, +hideInMenu: true } }, { @@ -184,7 +187,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: 'login', i18nKey: 'route.login', - constant: true + constant: true, +hideInMenu: true } }, { diff --git a/src/router/guard/index.ts b/src/router/guard/index.ts index 8b191736..0fea15a8 100644 --- a/src/router/guard/index.ts +++ b/src/router/guard/index.ts @@ -1,7 +1,8 @@ import type { Router } from 'vue-router'; +import { createRouteGuard } from './route'; import { createProgressGuard } from './progress'; import { createDocumentTitleGuard } from './title'; -import { createPermissionGuard } from './permission'; +// import { createPermissionGuard } from './permission'; /** * Router guard @@ -10,6 +11,7 @@ import { createPermissionGuard } from './permission'; */ export function createRouterGuard(router: Router) { createProgressGuard(router); - createPermissionGuard(router); + createRouteGuard(router); + // createPermissionGuard(router); createDocumentTitleGuard(router); } diff --git a/src/router/guard/permission.ts b/src/router/guard/permission.ts deleted file mode 100644 index 360ff2cb..00000000 --- a/src/router/guard/permission.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { NavigationGuardNext, RouteLocationNormalized, Router } from 'vue-router'; -import type { RouteKey, RoutePath } from '@elegant-router/types'; -import { useAuthStore } from '@/store/modules/auth'; -import { useRouteStore } from '@/store/modules/route'; -import { localStg } from '@/utils/storage'; - -export function createPermissionGuard(router: Router) { - router.beforeEach(async (to, from, next) => { - const pass = await createAuthRouteGuard(to, from, next); - - if (!pass) return; - - // 1. route with href - if (to.meta.href) { - window.open(to.meta.href, '_blank'); - next({ path: from.fullPath, replace: true, query: from.query, hash: to.hash }); - } - - const authStore = useAuthStore(); - - const isLogin = Boolean(localStg.get('token')); - const needLogin = !to.meta.constant; - const routeRoles = to.meta.roles || []; - const rootRoute: RouteKey = 'root'; - const loginRoute: RouteKey = 'login'; - const noPermissionRoute: RouteKey = '403'; - - // check whether the user has permission to access the route - // 1. if the route's "roles" is empty, then it is allowed to access - // 2. if the user is super admin in static route, then it is allowed to access - // 3. if the user's role is included in the route's "roles", then it is allowed to access - const hasPermission = - !routeRoles.length || authStore.isStaticSuper || authStore.userInfo.roles.some(role => routeRoles.includes(role)); - - const strategicPatterns: CommonType.StrategicPattern[] = [ - // 1. if it is login route when logged in, change to the root page - { - condition: isLogin && to.name === loginRoute, - callback: () => { - next({ name: rootRoute }); - } - }, - // 2. if is is constant route, then it is allowed to access directly - { - condition: !needLogin, - callback: () => { - next(); - } - }, - // 3. if the route need login but the user is not logged in, then switch to the login page - { - condition: !isLogin && needLogin, - callback: () => { - next({ name: loginRoute, query: { redirect: to.fullPath } }); - } - }, - // 4. if the user is logged in and has permission, then it is allowed to access - { - condition: isLogin && needLogin && hasPermission, - callback: () => { - next(); - } - }, - // 5. if the user is logged in but does not have permission, then switch to the 403 page - { - condition: isLogin && needLogin && !hasPermission, - callback: () => { - next({ name: noPermissionRoute }); - } - } - ]; - - strategicPatterns.some(({ condition, callback }) => { - if (condition) { - callback(); - } - - return condition; - }); - }); -} - -async function createAuthRouteGuard( - to: RouteLocationNormalized, - _from: RouteLocationNormalized, - next: NavigationGuardNext -) { - const notFoundRoute: RouteKey = 'not-found'; - const isNotFoundRoute = to.name === notFoundRoute; - - // 1. If the route is the constant route but is not the "not-found" route, then it is allowed to access. - if (to.meta.constant && !isNotFoundRoute) { - return true; - } - - // 2. If the auth route is initialized but is not the "not-found" route, then it is allowed to access. - const routeStore = useRouteStore(); - if (routeStore.isInitAuthRoute && !isNotFoundRoute) { - return true; - } - - // 3. If the route is initialized, check whether the route exists. - if (routeStore.isInitAuthRoute && isNotFoundRoute) { - const exist = await routeStore.getIsAuthRouteExist(to.path as RoutePath); - - if (exist) { - const noPermissionRoute: RouteKey = '403'; - - next({ name: noPermissionRoute }); - - return false; - } - - return true; - } - - // 4. If the user is not logged in, then redirect to the login page. - const isLogin = Boolean(localStg.get('token')); - if (!isLogin) { - const loginRoute: RouteKey = 'login'; - const redirect = to.fullPath; - - next({ name: loginRoute, query: { redirect } }); - - return false; - } - - // 5. init auth route - await routeStore.initAuthRoute(); - - // 6. the route is caught by the "not-found" route because the auto route is not initialized. after the auto route is initialized, redirect to the original route. - if (isNotFoundRoute) { - const rootRoute: RouteKey = 'root'; - const path = to.redirectedFrom?.name === rootRoute ? '/' : to.fullPath; - - next({ path, replace: true, query: to.query, hash: to.hash }); - - return false; - } - - return true; -} diff --git a/src/router/guard/route.ts b/src/router/guard/route.ts new file mode 100644 index 00000000..fbeaaa3b --- /dev/null +++ b/src/router/guard/route.ts @@ -0,0 +1,196 @@ +import type { + LocationQueryRaw, + NavigationGuardNext, + RouteLocationNormalized, + RouteLocationRaw, + Router +} from 'vue-router'; +import type { RouteKey, RoutePath } from '@elegant-router/types'; +import { useAuthStore } from '@/store/modules/auth'; +import { useRouteStore } from '@/store/modules/route'; +import { localStg } from '@/utils/storage'; + +/** + * create route guard + * + * @param router router instance + */ +export function createRouteGuard(router: Router) { + router.beforeEach(async (to, from, next) => { + const location = await initRoute(to); + + if (location) { + next(location); + return; + } + + const authStore = useAuthStore(); + + const rootRoute: RouteKey = 'root'; + const loginRoute: RouteKey = 'login'; + const noAuthorizationRoute: RouteKey = '403'; + + const isLogin = Boolean(localStg.get('token')); + const needLogin = !to.meta.constant; + const routeRoles = to.meta.roles || []; + + const hasRole = authStore.userInfo.roles.some(role => routeRoles.includes(role)); + + const hasAuth = authStore.isStaticSuper || !routeRoles.length || hasRole; + + const routeSwitches: CommonType.StrategicPattern[] = [ + // if it is login route when logged in, then switch to the root page + { + condition: isLogin && to.name === loginRoute, + callback: () => { + next({ name: rootRoute }); + } + }, + // if is is constant route, then it is allowed to access directly + { + condition: !needLogin, + callback: () => { + handleRouteSwitch(to, from, next); + } + }, + // if the route need login but the user is not logged in, then switch to the login page + { + condition: !isLogin && needLogin, + callback: () => { + next({ name: loginRoute, query: { redirect: to.fullPath } }); + } + }, + // if the user is logged in and has authorization, then it is allowed to access + { + condition: isLogin && needLogin && hasAuth, + callback: () => { + handleRouteSwitch(to, from, next); + } + }, + // if the user is logged in but does not have authorization, then switch to the 403 page + { + condition: isLogin && needLogin && !hasAuth, + callback: () => { + next({ name: noAuthorizationRoute }); + } + } + ]; + + routeSwitches.some(({ condition, callback }) => { + if (condition) { + callback(); + } + + return condition; + }); + }); +} + +/** + * initialize route + * + * @param to to route + */ +async function initRoute(to: RouteLocationNormalized): Promise { + const routeStore = useRouteStore(); + + const notFoundRoute: RouteKey = 'not-found'; + const isNotFoundRoute = to.name === notFoundRoute; + + // if the constant route is not initialized, then initialize the constant route + if (!routeStore.isInitConstantRoute) { + await routeStore.initConstantRoute(); + + // the route is captured by the "not-found" route because the constant route is not initialized + // after the constant route is initialized, redirect to the original route + if (isNotFoundRoute) { + const path = to.fullPath; + + const location: RouteLocationRaw = { + path, + replace: true, + query: to.query, + hash: to.hash + }; + + return location; + } + } + + // if the route is the constant route but is not the "not-found" route, then it is allowed to access. + if (to.meta.constant && !isNotFoundRoute) { + return null; + } + + // the auth route is initialized + // it is not the "not-found" route, then it is allowed to access + if (routeStore.isInitAuthRoute && !isNotFoundRoute) { + return null; + } + // it is captured by the "not-found" route, then check whether the route exists + if (routeStore.isInitAuthRoute && isNotFoundRoute) { + const exist = await routeStore.getIsAuthRouteExist(to.path as RoutePath); + const noPermissionRoute: RouteKey = '403'; + + if (exist) { + const location: RouteLocationRaw = { + name: noPermissionRoute + }; + + return location; + } + + return null; + } + + // if the auth route is not initialized, then initialize the auth route + const isLogin = Boolean(localStg.get('token')); + // initialize the auth route requires the user to be logged in, if not, redirect to the login page + if (!isLogin) { + const loginRoute: RouteKey = 'login'; + const redirect = to.fullPath; + + const query: LocationQueryRaw = to.name !== loginRoute ? { redirect } : {}; + + const location: RouteLocationRaw = { + name: loginRoute, + query + }; + + return location; + } + + // initialize the auth route + await routeStore.initAuthRoute(); + + // the route is captured by the "not-found" route because the auth route is not initialized + // after the auth route is initialized, redirect to the original route + if (isNotFoundRoute) { + const rootRoute: RouteKey = 'root'; + const path = to.redirectedFrom?.name === rootRoute ? '/' : to.fullPath; + + const location: RouteLocationRaw = { + path, + replace: true, + query: to.query, + hash: to.hash + }; + + return location; + } + + return null; +} + +function handleRouteSwitch(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { + // route with href + if (to.meta.href) { + window.open(to.meta.href, '_blank'); + + next({ path: from.fullPath, replace: true, query: from.query, hash: to.hash }); + + return; + } + + next(); +} diff --git a/src/router/index.ts b/src/router/index.ts index 557f57a3..ec1b925b 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,7 +6,7 @@ import { createWebHashHistory, createWebHistory } from 'vue-router'; -import { createRoutes } from './routes'; +import { createBuiltinVueRoutes } from './routes/builtin'; import { createRouterGuard } from './guard'; const { VITE_ROUTER_HISTORY_MODE = 'history', VITE_BASE_URL } = import.meta.env; @@ -17,11 +17,9 @@ const historyCreatorMap: Record Router memory: createMemoryHistory }; -const { constantVueRoutes } = createRoutes(); - export const router = createRouter({ history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL), - routes: constantVueRoutes + routes: createBuiltinVueRoutes() }); /** Setup Vue Router */ diff --git a/src/router/routes/builtin.ts b/src/router/routes/builtin.ts new file mode 100644 index 00000000..0a13e783 --- /dev/null +++ b/src/router/routes/builtin.ts @@ -0,0 +1,31 @@ +import type { CustomRoute } from '@elegant-router/types'; +import { layouts, views } from '../elegant/imports'; +import { getRoutePath, transformElegantRoutesToVueRoutes } from '../elegant/transform'; + +export const ROOT_ROUTE: CustomRoute = { + name: 'root', + path: '/', + redirect: getRoutePath(import.meta.env.VITE_ROUTE_HOME) || '/home', + meta: { + title: 'root', + constant: true + } +}; + +const NOT_FOUND_ROUTE: CustomRoute = { + name: 'not-found', + path: '/:pathMatch(.*)*', + component: 'layout.blank$view.404', + meta: { + title: 'not-found', + constant: true + } +}; + +/** builtin routes, it must be constant and setup in vue-router */ +const builtinRoutes: CustomRoute[] = [ROOT_ROUTE, NOT_FOUND_ROUTE]; + +/** create builtin vue routes */ +export function createBuiltinVueRoutes() { + return transformElegantRoutesToVueRoutes(builtinRoutes, layouts, views); +} diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index d9a0d0f1..89847852 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -1,29 +1,14 @@ import type { CustomRoute, ElegantConstRoute, ElegantRoute } from '@elegant-router/types'; import { generatedRoutes } from '../elegant/routes'; import { layouts, views } from '../elegant/imports'; -import { getRoutePath, transformElegantRoutesToVueRoutes } from '../elegant/transform'; - -export const ROOT_ROUTE: CustomRoute = { - name: 'root', - path: '/', - redirect: getRoutePath(import.meta.env.VITE_ROUTE_HOME) || '/home', - meta: { - title: 'root', - constant: true - } -}; +import { transformElegantRoutesToVueRoutes } from '../elegant/transform'; +/** + * custom routes + * + * @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route + */ const customRoutes: CustomRoute[] = [ - ROOT_ROUTE, - { - name: 'not-found', - path: '/:pathMatch(.*)*', - component: 'layout.blank$view.404', - meta: { - title: 'not-found', - constant: true - } - }, { name: 'exception', path: '/exception', @@ -69,8 +54,8 @@ const customRoutes: CustomRoute[] = [ } ]; -/** Create routes */ -export function createRoutes() { +/** create routes when the auth route mode is static */ +export function createStaticRoutes() { const constantRoutes: ElegantRoute[] = []; const authRoutes: ElegantRoute[] = []; @@ -83,10 +68,8 @@ export function createRoutes() { } }); - const constantVueRoutes = transformElegantRoutesToVueRoutes(constantRoutes, layouts, views); - return { - constantVueRoutes, + constantRoutes, authRoutes }; } diff --git a/src/service/api/route.ts b/src/service/api/route.ts index 8138470e..0956a7a8 100644 --- a/src/service/api/route.ts +++ b/src/service/api/route.ts @@ -1,5 +1,10 @@ import { request } from '../request'; +/** get constant routes */ +export function fetchGetConstantRoutes() { + return request({ url: '/route/getConstantRoutes' }); +} + /** get user routes */ export function fetchGetUserRoutes() { return request({ url: '/route/getUserRoutes' }); diff --git a/src/store/modules/route/index.ts b/src/store/modules/route/index.ts index 835d8884..7a845db1 100644 --- a/src/store/modules/route/index.ts +++ b/src/store/modules/route/index.ts @@ -1,13 +1,14 @@ -import { computed, ref } from 'vue'; +import { computed, ref, shallowRef } from 'vue'; import type { RouteRecordRaw } from 'vue-router'; import { defineStore } from 'pinia'; import { useBoolean } from '@sa/hooks'; import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types'; import { SetupStoreId } from '@/enum'; import { router } from '@/router'; -import { ROOT_ROUTE, createRoutes, getAuthVueRoutes } from '@/router/routes'; +import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes'; +import { ROOT_ROUTE } from '@/router/routes/builtin'; import { getRouteName, getRoutePath } from '@/router/elegant/transform'; -import { fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api'; +import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api'; import { useAppStore } from '../app'; import { useAuthStore } from '../auth'; import { useTabStore } from '../tab'; @@ -27,8 +28,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { const appStore = useAppStore(); const authStore = useAuthStore(); const tabStore = useTabStore(); + const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean(); const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean(); - const removeRouteFns: (() => void)[] = []; /** * Auth route mode @@ -51,6 +52,15 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { routeHome.value = routeKey; } + /** auth routes */ + const authRoutes = shallowRef([]); + + function addAuthRoutes(routes: ElegantConstRoute[]) { + authRoutes.value = [...authRoutes.value, ...routes]; + } + + const removeRouteFns: (() => void)[] = []; + /** Global menus */ const menus = ref([]); const searchMenus = computed(() => transformMenuToSearchMenus(menus.value)); @@ -74,9 +84,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { * @param routes Vue routes */ function getCacheRoutes(routes: RouteRecordRaw[]) { - const { constantVueRoutes } = createRoutes(); - - cacheRoutes.value = getCacheRouteNames([...constantVueRoutes, ...routes]); + cacheRoutes.value = getCacheRouteNames(routes); } /** @@ -145,6 +153,27 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { removeRouteFns.length = 0; } + /** init constant route */ + async function initConstantRoute() { + if (isInitConstantRoute.value) return; + + if (authRouteMode.value === 'static') { + const { constantRoutes } = createStaticRoutes(); + + addAuthRoutes(constantRoutes); + } else { + const { data, error } = await fetchGetConstantRoutes(); + + if (!error) { + addAuthRoutes(data); + } + } + + handleAuthRoutes(); + + setIsInitConstantRoute(true); + } + /** Init auth route */ async function initAuthRoute() { if (authRouteMode.value === 'static') { @@ -158,11 +187,17 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { /** Init static auth route */ async function initStaticAuthRoute() { - const { authRoutes } = createRoutes(); + const { authRoutes: staticAuthRoutes } = createStaticRoutes(); - const filteredAuthRoutes = filterAuthRoutesByRoles(authRoutes, authStore.userInfo.roles); + if (authStore.isStaticSuper) { + addAuthRoutes(staticAuthRoutes); + } else { + const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles); - handleAuthRoutes(filteredAuthRoutes); + addAuthRoutes(filteredAuthRoutes); + } + + handleAuthRoutes(); setIsInitAuthRoute(true); } @@ -174,7 +209,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { if (!error) { const { routes, home } = data; - handleAuthRoutes(routes); + addAuthRoutes(routes); + + handleAuthRoutes(); setRouteHome(home); @@ -184,18 +221,12 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { } } - /** - * Handle routes - * - * @param routes Auth routes - */ - function handleAuthRoutes(routes: ElegantConstRoute[]) { - const sortRoutes = sortRoutesByOrder(routes); + /** handle auth routes */ + function handleAuthRoutes() { + const sortRoutes = sortRoutesByOrder(authRoutes.value); const vueRoutes = getAuthVueRoutes(sortRoutes); - resetVueRoutes(); - addRoutesToVueRouter(vueRoutes); getGlobalMenus(sortRoutes); @@ -210,6 +241,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { */ function addRoutesToVueRouter(routes: RouteRecordRaw[]) { routes.forEach(route => { + if (route.name && router.hasRoute(route.name)) { + router.removeRoute(route.name); + } + const removeFn = router.addRoute(route); addRemoveRouteFn(removeFn); }); @@ -256,9 +291,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { } if (authRouteMode.value === 'static') { - const { authRoutes } = createRoutes(); - - return isRouteExistByRouteName(routeName, authRoutes); + return isRouteExistByRouteName(routeName, authRoutes.value); } const { data } = await fetchIsRouteExist(routeName); @@ -297,6 +330,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { reCacheRoutesByKey, reCacheRoutesByKeys, breadcrumbs, + initConstantRoute, + isInitConstantRoute, initAuthRoute, isInitAuthRoute, setIsInitAuthRoute, diff --git a/src/store/modules/route/shared.ts b/src/store/modules/route/shared.ts index 3b10f6fc..eccb1541 100644 --- a/src/store/modules/route/shared.ts +++ b/src/store/modules/route/shared.ts @@ -10,14 +10,6 @@ import { useSvgIcon } from '@/hooks/common/icon'; * @param roles Roles */ export function filterAuthRoutesByRoles(routes: ElegantConstRoute[], roles: string[]) { - // in static mode of auth route, the super admin role is defined in front-end - const SUPER_ROLE = 'R_SUPER'; - - // if the user is super admin, then it is allowed to access all routes - if (roles.includes(SUPER_ROLE)) { - return routes; - } - return routes.flatMap(route => filterAuthRouteByRoles(route, roles)); } diff --git a/src/views/_builtin/login/index.vue b/src/views/_builtin/login/index.vue index 61e5fdb8..78911fe7 100644 --- a/src/views/_builtin/login/index.vue +++ b/src/views/_builtin/login/index.vue @@ -17,9 +17,7 @@ interface Props { module?: UnionKey.LoginModule; } -const props = withDefaults(defineProps(), { - module: 'pwd-login' -}); +const props = defineProps(); const appStore = useAppStore(); const themeStore = useThemeStore(); @@ -37,7 +35,7 @@ const moduleMap: Record = { 'bind-wechat': { label: loginModuleRecord['bind-wechat'], component: BindWechat } }; -const activeModule = computed(() => moduleMap[props.module]); +const activeModule = computed(() => moduleMap[props.module || 'pwd-login']); const bgThemeColor = computed(() => themeStore.darkMode ? getColorPalette(themeStore.themeColor, 7) : themeStore.themeColor