feat: 对接登录和命名空间规范接口

This commit is contained in:
xlsea 2024-03-21 11:53:27 +08:00
parent 31bf70fa73
commit 6b1cf17428
19 changed files with 104 additions and 51 deletions

View File

@ -1,5 +1,5 @@
# backend service base url, test environment # backend service base url, test environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default VITE_SERVICE_BASE_URL=http://preview.easyretry.com/easy-retry
# other backend service base url, test environment # other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{ VITE_OTHER_SERVICE_BASE_URL= `{

View File

@ -29,6 +29,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem) {
proxy[item.proxyPattern] = { proxy[item.proxyPattern] = {
target: item.baseURL, target: item.baseURL,
changeOrigin: true, changeOrigin: true,
ws: false,
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '') rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
}; };

View File

@ -58,6 +58,7 @@
"naive-ui": "2.38.1", "naive-ui": "2.38.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "2.1.7", "pinia": "2.1.7",
"ts-md5": "^1.3.1",
"vue": "3.4.21", "vue": "3.4.21",
"vue-draggable-plus": "0.3.5", "vue-draggable-plus": "0.3.5",
"vue-i18n": "9.10.2", "vue-i18n": "9.10.2",

View File

@ -53,6 +53,9 @@ importers:
pinia: pinia:
specifier: 2.1.7 specifier: 2.1.7
version: 2.1.7(typescript@5.4.2)(vue@3.4.21) version: 2.1.7(typescript@5.4.2)(vue@3.4.21)
ts-md5:
specifier: ^1.3.1
version: 1.3.1
vue: vue:
specifier: 3.4.21 specifier: 3.4.21
version: 3.4.21(typescript@5.4.2) version: 3.4.21(typescript@5.4.2)
@ -7330,6 +7333,11 @@ packages:
typescript: 5.4.2 typescript: 5.4.2
dev: true dev: true
/ts-md5@1.3.1:
resolution: {integrity: sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg==}
engines: {node: '>=12'}
dev: false
/tslib@2.3.0: /tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false dev: false

View File

@ -82,7 +82,7 @@ function handleDropdown(key: DropdownKey) {
<div> <div>
<ButtonIcon> <ButtonIcon>
<SvgIcon icon="ph:user-circle" class="text-icon-large" /> <SvgIcon icon="ph:user-circle" class="text-icon-large" />
<span class="text-16px font-medium">{{ authStore.userInfo.userName }}</span> <span class="text-16px font-medium">{{ authStore.userInfo.username }}</span>
</ButtonIcon> </ButtonIcon>
</div> </div>
</NDropdown> </NDropdown>

View File

@ -8,6 +8,7 @@ import HorizontalMenu from '../global-menu/base-menu.vue';
import GlobalLogo from '../global-logo/index.vue'; import GlobalLogo from '../global-logo/index.vue';
import GlobalBreadcrumb from '../global-breadcrumb/index.vue'; import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
import GlobalSearch from '../global-search/index.vue'; import GlobalSearch from '../global-search/index.vue';
import NamespaceSelect from '../namespace-select/index.vue';
import { useMixMenuContext } from '../../hooks/use-mix-menu'; import { useMixMenuContext } from '../../hooks/use-mix-menu';
import ThemeButton from './components/theme-button.vue'; import ThemeButton from './components/theme-button.vue';
import UserAvatar from './components/user-avatar.vue'; import UserAvatar from './components/user-avatar.vue';
@ -55,6 +56,7 @@ const headerMenus = computed(() => {
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" /> <GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div> </div>
<div class="h-full flex-y-center justify-end"> <div class="h-full flex-y-center justify-end">
<NamespaceSelect />
<GlobalSearch /> <GlobalSearch />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" /> <FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" /> <LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />

View File

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { localStg } from '@/utils/storage';
const router = useRouter();
const namespaceId = ref<string>(localStg.get('namespaceId')!);
const userInfo = localStg.get('userInfo');
const options = ref(
userInfo?.namespaceIds.map(item => {
return { label: item.name, value: item.uniqueId };
})
);
const onChange = (value: string) => {
localStg.set('namespaceId', value);
setTimeout(() => {
router.go(0);
}, 500);
};
</script>
<template>
<NSelect v-model:value="namespaceId" class="namespace-select" :options="options" @update:value="onChange" />
</template>
<style lang="scss" scoped>
.namespace-select {
width: 150px;
:deep(.n-base-selection) {
border-radius: 32px !important;
}
}
</style>

View File

@ -30,10 +30,7 @@ export function createPermissionGuard(router: Router) {
// 2. if the user is super admin, then it is allowed to access // 2. if the user is super admin, 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 // 3. if the user's role is included in the route's "roles", then it is allowed to access
const SUPER_ADMIN = 'R_SUPER'; const SUPER_ADMIN = 'R_SUPER';
const hasPermission = const hasPermission = !routeRoles.length || authStore.userInfo.role === SUPER_ADMIN;
!routeRoles.length ||
authStore.userInfo.roles.includes(SUPER_ADMIN) ||
authStore.userInfo.roles.some(role => routeRoles.includes(role));
const strategicPatterns: CommonType.StrategicPattern[] = [ const strategicPatterns: CommonType.StrategicPattern[] = [
// 1. if it is login route when logged in, change to the root page // 1. if it is login route when logged in, change to the root page

View File

@ -3,15 +3,15 @@ import { request } from '../request';
/** /**
* Login * Login
* *
* @param userName User name * @param username User name
* @param password Password * @param password Password
*/ */
export function fetchLogin(userName: string, password: string) { export function fetchLogin(username: string, password: string) {
return request<Api.Auth.LoginToken>({ return request<Api.Auth.LoginToken>({
url: '/auth/login', url: '/auth/login',
method: 'post', method: 'post',
data: { data: {
userName, username,
password password
} }
}); });
@ -19,7 +19,7 @@ export function fetchLogin(userName: string, password: string) {
/** Get user info */ /** Get user info */
export function fetchGetUserInfo() { export function fetchGetUserInfo() {
return request<Api.Auth.UserInfo>({ url: '/auth/getUserInfo' }); return request<Api.Auth.UserInfo>({ url: '/user/info' });
} }
/** /**

View File

@ -8,9 +8,7 @@ const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy
export const request = createFlatRequest<App.Service.Response>( export const request = createFlatRequest<App.Service.Response>(
{ {
baseURL, baseURL,
headers: { timeout: 6000
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
}
}, },
{ {
async onRequest(config) { async onRequest(config) {
@ -18,15 +16,18 @@ export const request = createFlatRequest<App.Service.Response>(
// set token // set token
const token = localStg.get('token'); const token = localStg.get('token');
const Authorization = token ? `Bearer ${token}` : null; const namespaceId = localStg.get('namespaceId');
Object.assign(headers, { Authorization }); // const Authorization = token ? `Bearer ${token}` : null;
headers['EASY-RETRY-AUTH'] = token;
headers['EASY-RETRY-NAMESPACE-ID'] = namespaceId;
Object.assign(headers, { 'EASY-RETRY-AUTH': token, 'EASY-RETRY-NAMESPACE-ID': namespaceId });
return config; return config;
}, },
isBackendSuccess(response) { isBackendSuccess(response) {
// when the backend response code is "0000", it means the request is success // when the backend response code is "0000", it means the request is success
// you can change this logic by yourself // you can change this logic by yourself
return response.data.code === '0000'; return response.data.status === 1;
}, },
async onBackendFail(_response) { async onBackendFail(_response) {
// when the backend response code is not "0000", it means the request is fail // when the backend response code is not "0000", it means the request is fail
@ -42,7 +43,7 @@ export const request = createFlatRequest<App.Service.Response>(
// show backend error message // show backend error message
if (error.code === BACKEND_ERROR_CODE) { if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message; message = error.response?.data?.message || message;
} }
window.$message?.error(message); window.$message?.error(message);

View File

@ -58,7 +58,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
if (routeStore.isInitAuthRoute) { if (routeStore.isInitAuthRoute) {
window.$notification?.success({ window.$notification?.success({
title: $t('page.login.common.loginSuccess'), title: $t('page.login.common.loginSuccess'),
content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }), content: $t('page.login.common.welcomeBack', { userName: userInfo.username }),
duration: 4500 duration: 4500
}); });
} }
@ -73,7 +73,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
async function loginByToken(loginToken: Api.Auth.LoginToken) { async function loginByToken(loginToken: Api.Auth.LoginToken) {
// 1. stored in the localStorage, the later requests need it in headers // 1. stored in the localStorage, the later requests need it in headers
localStg.set('token', loginToken.token); localStg.set('token', loginToken.token);
localStg.set('refreshToken', loginToken.refreshToken); localStg.set('namespaceId', loginToken.namespaceIds[0].uniqueId);
const { data: info, error } = await fetchGetUserInfo(); const { data: info, error } = await fetchGetUserInfo();

View File

@ -8,9 +8,11 @@ export function getToken() {
/** Get user info */ /** Get user info */
export function getUserInfo() { export function getUserInfo() {
const emptyInfo: Api.Auth.UserInfo = { const emptyInfo: Api.Auth.UserInfo = {
userId: '', id: '',
userName: '', mode: '',
roles: [] username: '',
role: '',
namespaceIds: []
}; };
const userInfo = localStg.get('userInfo') || emptyInfo; const userInfo = localStg.get('userInfo') || emptyInfo;
@ -20,6 +22,6 @@ export function getUserInfo() {
/** Clear auth storage */ /** Clear auth storage */
export function clearAuthStorage() { export function clearAuthStorage() {
localStg.remove('token'); localStg.remove('token');
localStg.remove('refreshToken'); localStg.remove('namespaceId');
localStg.remove('userInfo'); localStg.remove('userInfo');
} }

View File

@ -9,7 +9,7 @@ import { ROOT_ROUTE, createRoutes, getAuthVueRoutes } from '@/router/routes';
import { getRouteName, getRoutePath } from '@/router/elegant/transform'; import { getRouteName, getRoutePath } from '@/router/elegant/transform';
import { fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api'; import { fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
import { useAppStore } from '../app'; import { useAppStore } from '../app';
import { useAuthStore } from '../auth'; // import { useAuthStore } from '../auth';
import { useTabStore } from '../tab'; import { useTabStore } from '../tab';
import { import {
filterAuthRoutesByRoles, filterAuthRoutesByRoles,
@ -25,7 +25,7 @@ import {
export const useRouteStore = defineStore(SetupStoreId.Route, () => { export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const appStore = useAppStore(); const appStore = useAppStore();
const authStore = useAuthStore(); // const authStore = useAuthStore();
const tabStore = useTabStore(); const tabStore = useTabStore();
const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean(); const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean();
const removeRouteFns: (() => void)[] = []; const removeRouteFns: (() => void)[] = [];
@ -160,7 +160,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
async function initStaticAuthRoute() { async function initStaticAuthRoute() {
const { authRoutes } = createRoutes(); const { authRoutes } = createRoutes();
const filteredAuthRoutes = filterAuthRoutesByRoles(authRoutes, authStore.userInfo.roles); const filteredAuthRoutes = filterAuthRoutesByRoles(authRoutes, []);
handleAuthRoutes(filteredAuthRoutes); handleAuthRoutes(filteredAuthRoutes);

20
src/typings/api.d.ts vendored
View File

@ -52,14 +52,28 @@ declare namespace Api {
*/ */
namespace Auth { namespace Auth {
interface LoginToken { interface LoginToken {
id: string;
mode: string;
role: String;
token: string; token: string;
refreshToken: string; refreshToken: string;
createDt: string;
updateDt: string;
namespaceIds: NamespaceId[];
} }
interface UserInfo { interface UserInfo {
userId: string; id: string;
userName: string; mode: string;
roles: string[]; username: string;
role: string;
namespaceIds: NamespaceId[];
}
interface NamespaceId {
id: string;
name: string;
uniqueId: string;
} }
} }

View File

@ -631,9 +631,9 @@ declare namespace App {
/** The backend service response data */ /** The backend service response data */
type Response<T = unknown> = { type Response<T = unknown> = {
/** The backend service response code */ /** The backend service response code */
code: string; status: number;
/** The backend service response message */ /** The backend service response message */
msg: string; message: string;
/** The backend service response data */ /** The backend service response data */
data: T; data: T;
}; };

View File

@ -16,21 +16,14 @@ declare module 'vue' {
FullScreen: typeof import('./../components/common/full-screen.vue')['default'] FullScreen: typeof import('./../components/common/full-screen.vue')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default'] IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'] IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default'] IconLocalLogo: typeof import('~icons/local/logo')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconUilSearch: typeof import('~icons/uil/search')['default'] IconUilSearch: typeof import('~icons/uil/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default'] LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default'] LookForward: typeof import('./../components/custom/look-forward.vue')['default']
@ -41,9 +34,6 @@ declare module 'vue' {
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox'] NCheckbox: typeof import('naive-ui')['NCheckbox']
NColorPicker: typeof import('naive-ui')['NColorPicker'] NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer'] NDrawer: typeof import('naive-ui')['NDrawer']
@ -52,7 +42,6 @@ declare module 'vue' {
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
@ -65,10 +54,6 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
@ -76,7 +61,6 @@ declare module 'vue' {
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing'] NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']

View File

@ -14,6 +14,8 @@ declare namespace StorageType {
lang: App.I18n.LangType; lang: App.I18n.LangType;
/** The token */ /** The token */
token: string; token: string;
/** The namespace id */
namespaceId: string;
/** The refresh token */ /** The refresh token */
refreshToken: string; refreshToken: string;
/** The user info */ /** The user info */

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive } from 'vue'; import { computed, reactive } from 'vue';
import { Md5 } from 'ts-md5';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { loginModuleRecord } from '@/constants/app'; import { loginModuleRecord } from '@/constants/app';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
@ -20,8 +21,8 @@ interface FormModel {
} }
const model: FormModel = reactive({ const model: FormModel = reactive({
userName: 'Soybean', userName: 'admin',
password: '123456' password: '654321'
}); });
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => { const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
@ -35,7 +36,10 @@ const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
async function handleSubmit() { async function handleSubmit() {
await validate(); await validate();
await authStore.login(model.userName, model.password); const md5 = new Md5();
md5.appendAsciiStr(model.password);
const password: string = md5.end() as string;
await authStore.login(model.userName, password);
} }
</script> </script>

View File

@ -48,7 +48,7 @@ const statisticData = computed<StatisticData[]>(() => [
</div> </div>
<div class="pl-12px"> <div class="pl-12px">
<h3 class="text-18px font-semibold"> <h3 class="text-18px font-semibold">
{{ $t('page.home.greeting', { userName: authStore.userInfo.userName }) }} {{ $t('page.home.greeting', { userName: authStore.userInfo.username }) }}
</h3> </h3>
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p> <p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
</div> </div>