From e6939e22b108a9a44fa2af7083dc3b3060ee4493 Mon Sep 17 00:00:00 2001 From: xingyuv Date: Sat, 16 Nov 2024 22:37:06 +0800 Subject: [PATCH] feat: request && login && router --- apps/web-antd/src/api/core/auth.ts | 66 ++++++++-- apps/web-antd/src/api/core/index.ts | 2 - apps/web-antd/src/api/core/menu.ts | 10 -- apps/web-antd/src/api/core/user.ts | 10 -- apps/web-antd/src/api/request.ts | 39 ++++-- apps/web-antd/src/preferences.ts | 3 + apps/web-antd/src/router/access.ts | 47 +++++++- apps/web-antd/src/router/helper.ts | 84 +++++++++++++ apps/web-antd/src/store/auth.ts | 61 +++++----- apps/web-antd/src/types/index.ts | 2 + apps/web-antd/src/types/menus.ts | 19 +++ apps/web-antd/src/types/user.ts | 22 ++++ apps/web-antd/src/utils/auth.ts | 45 +++++++ apps/web-antd/src/utils/index.ts | 1 + .../src/views/_core/authentication/login.vue | 113 ++++++++++-------- 15 files changed, 404 insertions(+), 120 deletions(-) delete mode 100644 apps/web-antd/src/api/core/menu.ts delete mode 100644 apps/web-antd/src/api/core/user.ts create mode 100644 apps/web-antd/src/router/helper.ts create mode 100644 apps/web-antd/src/types/index.ts create mode 100644 apps/web-antd/src/types/menus.ts create mode 100644 apps/web-antd/src/types/user.ts create mode 100644 apps/web-antd/src/utils/auth.ts create mode 100644 apps/web-antd/src/utils/index.ts diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index 71d9f994..b0f391ee 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -1,15 +1,22 @@ +import type { YudaoUserInfo } from '#/types'; + import { baseRequestClient, requestClient } from '#/api/request'; +import { getRefreshToken } from '#/utils'; export namespace AuthApi { /** 登录接口参数 */ export interface LoginParams { password?: string; username?: string; + captchaVerification?: string; } /** 登录接口返回值 */ export interface LoginResult { + userId: number | string; accessToken: string; + refreshToken: string; + expiresTime: number; } export interface RefreshTokenResult { @@ -22,30 +29,69 @@ export namespace AuthApi { * 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/auth/login', data); + return requestClient.post('/system/auth/login', data); } /** * 刷新accessToken */ export async function refreshTokenApi() { - return baseRequestClient.post('/auth/refresh', { - withCredentials: true, - }); + return baseRequestClient.post( + `/system/auth/refresh-token?refreshToken=${getRefreshToken()}`, + { + withCredentials: true, + }, + ); +} + +/** + * 使用租户名,获得租户编号 + * @param name 租户名 + * @returns 租户编号 + */ +export function getTenantIdByName(name: string) { + return requestClient.get( + `/system/tenant/get-id-by-name?name=${name}`, + ); +} + +/** + * 使用租户域名,获得租户信息 + * @param website 域名 + * @returns 租户信息 + */ +export function getTenantByWebsite(website: string) { + return requestClient.get(`/system/tenant/get-by-website?website=${website}`); } /** * 退出登录 */ export async function logoutApi() { - return baseRequestClient.post('/auth/logout', { + return baseRequestClient.post('/system/auth/logout', { withCredentials: true, }); } -/** - * 获取用户权限码 - */ -export async function getAccessCodesApi() { - return requestClient.get('/auth/codes'); +// 获取用户权限信息 +export function getUserInfo() { + return requestClient.get('/system/auth/get-permission-info'); +} + +/** + * 获取验证图片 以及token + */ +export function getCaptcha(data: any) { + return requestClient.post('/system/captcha/get', data, { + // isReturnNativeResponse: true, + }); +} + +/** + * 滑动或者点选验证 + */ +export function checkCaptcha(data: any) { + return requestClient.post('/system/captcha/check', data, { + // isReturnNativeResponse: true, + }); } diff --git a/apps/web-antd/src/api/core/index.ts b/apps/web-antd/src/api/core/index.ts index 28a5aef4..269586ee 100644 --- a/apps/web-antd/src/api/core/index.ts +++ b/apps/web-antd/src/api/core/index.ts @@ -1,3 +1 @@ export * from './auth'; -export * from './menu'; -export * from './user'; diff --git a/apps/web-antd/src/api/core/menu.ts b/apps/web-antd/src/api/core/menu.ts deleted file mode 100644 index 9ef60b11..00000000 --- a/apps/web-antd/src/api/core/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RouteRecordStringComponent } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户所有菜单 - */ -export async function getAllMenusApi() { - return requestClient.get('/menu/all'); -} diff --git a/apps/web-antd/src/api/core/user.ts b/apps/web-antd/src/api/core/user.ts deleted file mode 100644 index 7e28ea84..00000000 --- a/apps/web-antd/src/api/core/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { UserInfo } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户信息 - */ -export async function getUserInfoApi() { - return requestClient.get('/user/info'); -} diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 67ef35e4..6e1ebd98 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -15,10 +15,14 @@ import { useAccessStore } from '@vben/stores'; import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; +import { getTenantId } from '#/utils'; import { refreshTokenApi } from './core'; -const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); +const { apiURL, tenantEnable } = useAppConfig( + import.meta.env, + import.meta.env.PROD, +); function createRequestClient(baseURL: string) { const client = new RequestClient({ @@ -49,7 +53,7 @@ function createRequestClient(baseURL: string) { async function doRefreshToken() { const accessStore = useAccessStore(); const resp = await refreshTokenApi(); - const newToken = resp.data; + const newToken = resp.refreshToken; accessStore.setAccessToken(newToken); return newToken; } @@ -62,9 +66,11 @@ function createRequestClient(baseURL: string) { client.addRequestInterceptor({ fulfilled: async (config) => { const accessStore = useAccessStore(); - + const tenantId = getTenantId(); config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; + config.headers['tenant-id'] = + tenantEnable && tenantId ? tenantId : undefined; return config; }, }); @@ -72,11 +78,30 @@ function createRequestClient(baseURL: string) { // response数据解构 client.addResponseInterceptor({ fulfilled: (response) => { - const { data: responseData, status } = response; + // const { config, data: responseData, status, request } = response; + const { data: responseData, request } = response; + // 这个判断的目的是:excel 导出等情况下,系统执行异常,此时返回的是 json,而不是二进制数据 + if ( + (request.responseType === 'blob' || + request.responseType === 'arraybuffer') && + responseData?.code === undefined + ) { + return responseData; + } - const { code, data } = responseData; - if (status >= 200 && status < 400 && code === 0) { - return data; + // const { isReturnNativeResponse, isTransformResponse } = config; + // // 是否返回原生响应头 比如:需要获取响应头时使用该属性 + // if (isReturnNativeResponse) { + // return response; + // } + // // 不进行任何处理,直接返回,用于页面代码可能需要直接获取code,data,message这些信息时开启 + // if (!isTransformResponse) { + // return response.data; + // } + + const { code, data: result } = responseData; + if (responseData && Reflect.has(responseData, 'code') && code === 0) { + return result; } throw Object.assign({}, response, { response }); diff --git a/apps/web-antd/src/preferences.ts b/apps/web-antd/src/preferences.ts index b2e9ace4..5ec90ae7 100644 --- a/apps/web-antd/src/preferences.ts +++ b/apps/web-antd/src/preferences.ts @@ -8,6 +8,9 @@ import { defineOverridesPreferences } from '@vben/preferences'; export const overridesPreferences = defineOverridesPreferences({ // overrides app: { + /** 后端路由模式 */ + accessMode: 'backend', name: import.meta.env.VITE_APP_TITLE, + enableRefreshToken: false, }, }); diff --git a/apps/web-antd/src/router/access.ts b/apps/web-antd/src/router/access.ts index 3a48be23..3ca4c59d 100644 --- a/apps/web-antd/src/router/access.ts +++ b/apps/web-antd/src/router/access.ts @@ -1,19 +1,58 @@ import type { ComponentRecordType, GenerateMenuAndRoutesOptions, + RouteRecordStringComponent, } from '@vben/types'; import { generateAccessible } from '@vben/access'; import { preferences } from '@vben/preferences'; +import { useUserStore } from '@vben/stores'; +import { cloneDeep } from '@vben/utils'; import { message } from 'ant-design-vue'; -import { getAllMenusApi } from '#/api'; import { BasicLayout, IFrameView } from '#/layouts'; import { $t } from '#/locales'; +import { buildMenus } from './helper'; + const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); +/** + * dashboard路由 + */ +const dashboardMenus: RouteRecordStringComponent[] = [ + { + component: 'BasicLayout', + meta: { + order: -1, + title: 'page.dashboard.title', + }, + name: 'Dashboard', + path: '/', + redirect: '/analytics', + children: [ + { + name: 'Analytics', + path: '/analytics', + component: '/dashboard/analytics/index', + meta: { + affixTab: true, + title: 'page.dashboard.analytics', + }, + }, + { + name: 'Workspace', + path: '/workspace', + component: '/dashboard/workspace/index', + meta: { + title: 'page.dashboard.workspace', + }, + }, + ], + }, +]; + async function generateAccess(options: GenerateMenuAndRoutesOptions) { const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); @@ -29,7 +68,11 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { content: `${$t('common.loadingMenu')}...`, duration: 1.5, }); - return await getAllMenusApi(); + const userStore = useUserStore(); + const menus = userStore.userInfo?.menus; + const routes = buildMenus(menus); + const menuList = [...cloneDeep(dashboardMenus), ...routes]; + return menuList; }, // 可以指定没有权限跳转403页面 forbiddenComponent, diff --git a/apps/web-antd/src/router/helper.ts b/apps/web-antd/src/router/helper.ts new file mode 100644 index 00000000..3c365baa --- /dev/null +++ b/apps/web-antd/src/router/helper.ts @@ -0,0 +1,84 @@ +import type { RouteRecordStringComponent } from '@vben/types'; + +import type { AppRouteRecordRaw } from '#/types'; + +import { isHttpUrl } from '@vben/utils'; + +function buildMenus( + menuList: AppRouteRecordRaw[], + parent = '', +): RouteRecordStringComponent[] { + const menus: RouteRecordStringComponent[] = []; + menuList.forEach((menu) => { + // 处理顶级链接菜单 + if (isHttpUrl(menu.path) && menu.parentId === 0) { + const urlMenu: RouteRecordStringComponent = { + component: 'BasicLayout', + meta: { + icon: menu.icon, + title: menu.name, + }, + name: menu.name, + path: `/${menu.path}`, + children: [ + { + component: 'IFrameView', + meta: { + hideInMenu: !menu.visible, + icon: menu.icon, + link: menu.path, + orderNo: menu.sort, + title: menu.name, + }, + name: menu.name, + path: `/${menu.path}/index`, + }, + ], + }; + menus.push(urlMenu); + return; + } else if (menu.children && menu.parentId === 0) { + menu.component = 'BasicLayout'; + } else if (!menu.children) { + menu.component = menu.component as string; + } + if (menu.component === 'Layout') { + menu.component = 'BasicLayout'; + } + + if (menu.children && menu.parentId !== 0) { + menu.component = ''; + } + + // path + if (parent) { + menu.path = `${parent}/${menu.path}`; + } + + if (!menu.path.startsWith('/')) { + menu.path = `/${menu.path}`; + } + + const buildMenu: RouteRecordStringComponent = { + component: menu.component, + meta: { + hideInMenu: !menu.visible, + icon: menu.icon, + keepAlive: menu.keepAlive, + orderNo: menu.sort, + title: menu.name, + }, + name: menu.name, + path: menu.path, + }; + + if (menu.children && menu.children.length > 0) { + buildMenu.children = buildMenus(menu.children, menu.path); + } + + menus.push(buildMenu); + }); + return menus; +} + +export { buildMenus }; diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index 9d64d205..3d163e17 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -1,4 +1,6 @@ -import type { Recordable, UserInfo } from '@vben/types'; +import type { Recordable } from '@vben/types'; + +import type { YudaoUserInfo } from '#/types'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -9,8 +11,9 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; -import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; +import { getUserInfo, loginApi, logoutApi } from '#/api'; import { $t } from '#/locales'; +import { setAccessToken, setRefreshToken } from '#/utils'; export const useAuthStore = defineStore('auth', () => { const accessStore = useAccessStore(); @@ -29,40 +32,42 @@ export const useAuthStore = defineStore('auth', () => { onSuccess?: () => Promise | void, ) { // 异步处理用户登录操作并获取 accessToken - let userInfo: null | UserInfo = null; + let userInfo: null | YudaoUserInfo = null; try { loginLoading.value = true; - const { accessToken } = await loginApi(params); + const { accessToken, expiresTime, refreshToken } = await loginApi(params); // 如果成功获取到 accessToken if (accessToken) { accessStore.setAccessToken(accessToken); + accessStore.setRefreshToken(refreshToken); + setAccessToken(accessToken, expiresTime); + setRefreshToken(refreshToken); // 获取用户信息并存储到 accessStore 中 - const [fetchUserInfoResult, accessCodes] = await Promise.all([ - fetchUserInfo(), - getAccessCodesApi(), - ]); - + const fetchUserInfoResult = await fetchUserInfo(); userInfo = fetchUserInfoResult; + if (userInfo) { + if (userInfo.roles) { + userStore.setUserRoles(userInfo.roles); + } + // userStore.setMenus(userInfo.menus); + accessStore.setAccessCodes(userInfo.permissions); + if (accessStore.loginExpired) { + accessStore.setLoginExpired(false); + } else { + onSuccess + ? await onSuccess?.() + : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); + } - userStore.setUserInfo(userInfo); - accessStore.setAccessCodes(accessCodes); - - if (accessStore.loginExpired) { - accessStore.setLoginExpired(false); - } else { - onSuccess - ? await onSuccess?.() - : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); - } - - if (userInfo?.realName) { - notification.success({ - description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, - duration: 3, - message: $t('authentication.loginSuccess'), - }); + if (userInfo?.realName) { + notification.success({ + description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + duration: 3, + message: $t('authentication.loginSuccess'), + }); + } } } } finally { @@ -95,8 +100,8 @@ export const useAuthStore = defineStore('auth', () => { } async function fetchUserInfo() { - let userInfo: null | UserInfo = null; - userInfo = await getUserInfoApi(); + let userInfo: null | YudaoUserInfo = null; + userInfo = await getUserInfo(); userStore.setUserInfo(userInfo); return userInfo; } diff --git a/apps/web-antd/src/types/index.ts b/apps/web-antd/src/types/index.ts new file mode 100644 index 00000000..f65cf3a8 --- /dev/null +++ b/apps/web-antd/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './menus'; +export * from './user'; diff --git a/apps/web-antd/src/types/menus.ts b/apps/web-antd/src/types/menus.ts new file mode 100644 index 00000000..073cd101 --- /dev/null +++ b/apps/web-antd/src/types/menus.ts @@ -0,0 +1,19 @@ +import type { RouteMeta, RouteRecordRaw } from 'vue-router'; + +interface AppRouteRecordRaw extends Omit { + children?: AppRouteRecordRaw[]; + component?: any; + componentName?: string; + components?: any; + fullPath?: string; + icon?: string; + keepAlive?: boolean; + meta: RouteMeta; + name: string; + parentId?: number; + props?: any; + sort?: number; + visible?: boolean; +} + +export type { AppRouteRecordRaw }; diff --git a/apps/web-antd/src/types/user.ts b/apps/web-antd/src/types/user.ts new file mode 100644 index 00000000..570a7328 --- /dev/null +++ b/apps/web-antd/src/types/user.ts @@ -0,0 +1,22 @@ +import type { BasicUserInfo } from '@vben/types'; + +import type { AppRouteRecordRaw } from '#/types'; + +/** 用户信息 */ +type ExBasicUserInfo = { + deptId: number; +} & BasicUserInfo; + +/** 用户信息 */ +interface YudaoUserInfo extends ExBasicUserInfo { + permissions: string[]; + menus: AppRouteRecordRaw[]; + /** + * 首页地址 + */ + homePath: string; + roles: string[]; + user: ExBasicUserInfo; +} + +export type { ExBasicUserInfo, YudaoUserInfo }; diff --git a/apps/web-antd/src/utils/auth.ts b/apps/web-antd/src/utils/auth.ts new file mode 100644 index 00000000..2b068515 --- /dev/null +++ b/apps/web-antd/src/utils/auth.ts @@ -0,0 +1,45 @@ +import { StorageManager } from '@vben/utils'; +// token key +const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN__'; + +const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN__'; + +const TENANT_ID_KEY = 'TENANT_ID__'; + +const storage = new StorageManager({ + prefix: import.meta.env.VITE_APP_NAMESPACE, + storageType: 'sessionStorage', +}); + +function getAccessToken(): null | string { + return storage.getItem(ACCESS_TOKEN_KEY); +} + +function setAccessToken(value: string, unix: number) { + return storage.setItem(ACCESS_TOKEN_KEY, value, unix - Date.now()); +} + +function getRefreshToken(): null | string { + return storage.getItem(REFRESH_TOKEN_KEY); +} + +function setRefreshToken(value: string) { + return storage.setItem(REFRESH_TOKEN_KEY, value); +} + +function getTenantId(): null | number { + return storage.getItem(TENANT_ID_KEY); +} + +function setTenantId(value: number) { + return storage.setItem(TENANT_ID_KEY, value); +} + +export { + getAccessToken, + getRefreshToken, + getTenantId, + setAccessToken, + setRefreshToken, + setTenantId, +}; diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts new file mode 100644 index 00000000..269586ee --- /dev/null +++ b/apps/web-antd/src/utils/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/apps/web-antd/src/views/_core/authentication/login.vue b/apps/web-antd/src/views/_core/authentication/login.vue index 099e4c8c..e9ea11bd 100644 --- a/apps/web-antd/src/views/_core/authentication/login.vue +++ b/apps/web-antd/src/views/_core/authentication/login.vue @@ -1,73 +1,47 @@