feat: request && login && router

pull/48/MERGE
xingyuv 2024-11-16 22:37:06 +08:00
parent 64ed920646
commit e6939e22b1
15 changed files with 404 additions and 120 deletions

View File

@ -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<AuthApi.LoginResult>('/auth/login', data);
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
}
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
return baseRequestClient.post<AuthApi.LoginResult>(
`/system/auth/refresh-token?refreshToken=${getRefreshToken()}`,
{
withCredentials: true,
},
);
}
/**
* 使
* @param name
* @returns
*/
export function getTenantIdByName(name: string) {
return requestClient.get<number>(
`/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<string[]>('/auth/codes');
// 获取用户权限信息
export function getUserInfo() {
return requestClient.get<YudaoUserInfo>('/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,
});
}

View File

@ -1,3 +1 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@ -1,10 +0,0 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@ -1,10 +0,0 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}

View File

@ -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<HttpResponse>({
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;
// }
// // 不进行任何处理,直接返回,用于页面代码可能需要直接获取codedatamessage这些信息时开启
// 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 });

View File

@ -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,
},
});

View File

@ -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,

View File

@ -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 };

View File

@ -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> | 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;
}

View File

@ -0,0 +1,2 @@
export * from './menus';
export * from './user';

View File

@ -0,0 +1,19 @@
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
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 };

View File

@ -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 };

View File

@ -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,
};

View File

@ -0,0 +1 @@
export * from './auth';

View File

@ -1,73 +1,47 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import type { Recordable } from '@vben/types';
import { computed, markRaw } from 'vue';
import { computed, ref } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales';
import { getTenantByWebsite, getTenantIdByName } from '#/api/core/auth';
import { useAuthStore } from '#/store';
import { setTenantId } from '#/utils';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const { tenantEnable, captchaEnable } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
component: 'VbenInput',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
fieldName: 'tenantName',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
value: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
value: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
},
{
component: 'VbenInputPassword',
@ -77,22 +51,59 @@ const formSchema = computed((): VbenFormSchema[] => {
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
value: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
},
];
});
const loginData = ref({
captchaVerification: '',
username: '',
password: '',
tenantName: '',
});
//
async function getCode(params: Recordable<any>) {
if (params) {
loginData.value = params;
}
getTenant()
.then()
.finally(async () => {
//
if (captchaEnable === 'false') {
await handleLogin();
} else {
//
//
verify.value.show();
}
});
}
// && ID
async function getTenant() {
if (tenantEnable === 'true') {
const website = location.host;
const tenant = await getTenantByWebsite(website);
if (tenant) {
loginData.value.tenantName = tenant.name;
setTenantId(tenant.id);
} else {
const res = await getTenantIdByName(loginData.value.tenantName);
setTenantId(res);
}
}
}
async function handleLogin() {
authStore.authLogin(loginData.value);
}
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
@submit="authStore.authLogin"
@submit="getCode"
/>
</template>