feat(projects): add auth example

This commit is contained in:
Soybean 2024-03-24 15:39:41 +08:00
parent 41e8bc44f8
commit c11d56da29
20 changed files with 269 additions and 22 deletions

5
.env
View File

@ -12,7 +12,7 @@ VITE_ICON_PREFIX=icon
VITE_ICON_LOCAL_PREFIX=icon-local VITE_ICON_LOCAL_PREFIX=icon-local
# auth route mode: static dynamic # auth route mode: static dynamic
VITE_AUTH_ROUTE_MODE=static VITE_AUTH_ROUTE_MODE=dynamic
# static auth route home # static auth route home
VITE_ROUTE_HOME=home VITE_ROUTE_HOME=home
@ -37,3 +37,6 @@ VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request # token expired codes of backend service, when the code is received, it will refresh the token and resend the request
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998 VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998
# when the route mode is static, the defined super role
VITE_STATIC_SUPER_ROLE=R_SUPER

View File

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({ name: 'ExceptionBase' }); defineOptions({ name: 'ExceptionBase' });
@ -19,6 +20,8 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
const { routerPushByKey } = useRouterPush();
const iconMap: Record<ExceptionType, string> = { const iconMap: Record<ExceptionType, string> = {
'403': 'no-permission', '403': 'no-permission',
'404': 'not-found', '404': 'not-found',
@ -33,9 +36,7 @@ const icon = computed(() => iconMap[props.type]);
<div class="flex text-400px text-primary"> <div class="flex text-400px text-primary">
<SvgIcon :local-icon="icon" /> <SvgIcon :local-icon="icon" />
</div> </div>
<RouterLink to="/"> <NButton type="primary" @click="routerPushByKey('root')">{{ $t('common.backToHome') }}</NButton>
<NButton type="primary">{{ $t('common.backToHome') }}</NButton>
</RouterLink>
</div> </div>
</template> </template>

View File

@ -0,0 +1,21 @@
import { useAuthStore } from '@/store/modules/auth';
export function useAuth() {
const authStore = useAuthStore();
function hasAuth(codes: string | string[]) {
if (!authStore.isLogin) {
return false;
}
if (typeof codes === 'string') {
return authStore.userInfo.buttons.includes(codes);
}
return codes.some(code => authStore.userInfo.buttons.includes(code));
}
return {
hasAuth
};
}

View File

@ -148,6 +148,8 @@ const local: App.I18n.Schema = {
'function_hide-child_two': 'Two', 'function_hide-child_two': 'Two',
'function_hide-child_three': 'Three', 'function_hide-child_three': 'Three',
function_request: 'Request', function_request: 'Request',
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
manage: 'System Manage', manage: 'System Manage',
manage_user: 'User Manage', manage_user: 'User Manage',
'manage_user-detail': 'User Detail', 'manage_user-detail': 'User Detail',
@ -187,9 +189,9 @@ const local: App.I18n.Schema = {
register: 'Register', register: 'Register',
otherAccountLogin: 'Other Account Login', otherAccountLogin: 'Other Account Login',
otherLoginMode: 'Other Login Mode', otherLoginMode: 'Other Login Mode',
superAdmin: 'Super Administrator', superAdmin: 'Super Admin',
admin: 'Administrator', admin: 'Admin',
user: 'Ordinary User' user: 'User'
}, },
codeLogin: { codeLogin: {
title: 'Verification Code Login', title: 'Verification Code Login',
@ -275,6 +277,13 @@ const local: App.I18n.Schema = {
multiTab: { multiTab: {
routeParam: 'Route Param', routeParam: 'Route Param',
backTab: 'Back function_tab' backTab: 'Back function_tab'
},
toggleAuth: {
toggleAccount: 'Toggle Account',
authHook: 'Auth Hook Function `hasAuth`',
superAdminVisible: 'Super Admin Visible',
adminVisible: 'Admin Visible',
adminOrUserVisible: 'Admin and User Visible'
} }
}, },
manage: { manage: {

View File

@ -148,6 +148,8 @@ const local: App.I18n.Schema = {
'function_hide-child_two': '菜单二', 'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三', 'function_hide-child_three': '菜单三',
function_request: '请求', function_request: '请求',
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
manage: '系统管理', manage: '系统管理',
manage_user: '用户管理', manage_user: '用户管理',
'manage_user-detail': '用户详情', 'manage_user-detail': '用户详情',
@ -275,6 +277,13 @@ const local: App.I18n.Schema = {
multiTab: { multiTab: {
routeParam: '路由参数', routeParam: '路由参数',
backTab: '返回 function_tab' backTab: '返回 function_tab'
},
toggleAuth: {
toggleAccount: '切换账号',
authHook: '权限钩子函数 `hasAuth`',
superAdminVisible: '超级管理员可见',
adminVisible: '管理员可见',
adminOrUserVisible: '管理员和用户可见'
} }
}, },
manage: { manage: {

View File

@ -25,7 +25,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"), "function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"), "function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
function_request: () => import("@/views/function/request/index.vue"), function_request: () => import("@/views/function/request/index.vue"),
"function_super-page": () => import("@/views/function/super-page/index.vue"),
function_tab: () => import("@/views/function/tab/index.vue"), function_tab: () => import("@/views/function/tab/index.vue"),
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
home: () => import("@/views/home/index.vue"), home: () => import("@/views/home/index.vue"),
manage_menu: () => import("@/views/manage/menu/index.vue"), manage_menu: () => import("@/views/manage/menu/index.vue"),
manage_role: () => import("@/views/manage/role/index.vue"), manage_role: () => import("@/views/manage/role/index.vue"),

View File

@ -64,7 +64,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'function_hide-child', title: 'function_hide-child',
i18nKey: 'route.function_hide-child', i18nKey: 'route.function_hide-child',
icon: 'material-symbols:filter-list-off' icon: 'material-symbols:filter-list-off',
order: 2
}, },
redirect: '/function/hide-child/one', redirect: '/function/hide-child/one',
children: [ children: [
@ -124,7 +125,19 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'function_request', title: 'function_request',
i18nKey: 'route.function_request', i18nKey: 'route.function_request',
icon: 'carbon:network-overlay' icon: 'carbon:network-overlay',
order: 3
}
},
{
name: 'function_super-page',
path: '/function/super-page',
component: 'view.function_super-page',
meta: {
title: 'function_super-page',
i18nKey: 'route.function_super-page',
icon: 'ic:round-supervisor-account',
order: 5
} }
}, },
{ {
@ -134,7 +147,20 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'function_tab', title: 'function_tab',
i18nKey: 'route.function_tab', i18nKey: 'route.function_tab',
icon: 'ic:round-tab' icon: 'ic:round-tab',
order: 1
}
},
{
name: 'function_toggle-auth',
path: '/function/toggle-auth',
component: 'view.function_toggle-auth',
meta: {
title: 'function_toggle-auth',
i18nKey: 'route.function_toggle-auth',
icon: 'ic:round-construction',
order: 4,
roles: ['R_SUPER']
} }
} }
] ]

View File

@ -158,7 +158,9 @@ const routeMap: RouteMap = {
"function_hide-child_two": "/function/hide-child/two", "function_hide-child_two": "/function/hide-child/two",
"function_multi-tab": "/function/multi-tab", "function_multi-tab": "/function/multi-tab",
"function_request": "/function/request", "function_request": "/function/request",
"function_super-page": "/function/super-page",
"function_tab": "/function/tab", "function_tab": "/function/tab",
"function_toggle-auth": "/function/toggle-auth",
"home": "/home", "home": "/home",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"manage": "/manage", "manage": "/manage",

View File

@ -27,13 +27,10 @@ export function createPermissionGuard(router: Router) {
// check whether the user has permission to access the route // check whether the user has permission to access the route
// 1. if the route's "roles" is empty, then it is allowed to access // 1. if the route's "roles" is empty, then it is allowed to access
// 2. if the user is super admin, 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 // 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 hasPermission = const hasPermission =
!routeRoles.length || !routeRoles.length || authStore.isStaticSuper || authStore.userInfo.roles.some(role => routeRoles.includes(role));
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

@ -18,6 +18,13 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const userInfo: Api.Auth.UserInfo = reactive(getUserInfo()); const userInfo: Api.Auth.UserInfo = reactive(getUserInfo());
/** is super role in static route */
const isStaticSuper = computed(() => {
const { VITE_AUTH_ROUTE_MODE, VITE_STATIC_SUPER_ROLE } = import.meta.env;
return VITE_AUTH_ROUTE_MODE === 'static' && userInfo.roles.includes(VITE_STATIC_SUPER_ROLE);
});
/** Is login */ /** Is login */
const isLogin = computed(() => Boolean(token.value)); const isLogin = computed(() => Boolean(token.value));
@ -41,8 +48,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
* *
* @param userName User name * @param userName User name
* @param password Password * @param password Password
* @param [redirect=true] Whether to redirect after login. Default is `true`
*/ */
async function login(userName: string, password: string) { async function login(userName: string, password: string, redirect = true) {
startLoading(); startLoading();
const { data: loginToken, error } = await fetchLogin(userName, password); const { data: loginToken, error } = await fetchLogin(userName, password);
@ -53,7 +61,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
if (pass) { if (pass) {
await routeStore.initAuthRoute(); await routeStore.initAuthRoute();
if (redirect) {
await redirectFromLogin(); await redirectFromLogin();
}
if (routeStore.isInitAuthRoute) { if (routeStore.isInitAuthRoute) {
window.$notification?.success({ window.$notification?.success({
@ -94,6 +104,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
return { return {
token, token,
userInfo, userInfo,
isStaticSuper,
isLogin, isLogin,
loginLoading, loginLoading,
resetStore, resetStore,

View File

@ -10,10 +10,16 @@ export function getUserInfo() {
const emptyInfo: Api.Auth.UserInfo = { const emptyInfo: Api.Auth.UserInfo = {
userId: '', userId: '',
userName: '', userName: '',
roles: [] roles: [],
buttons: []
}; };
const userInfo = localStg.get('userInfo') || emptyInfo; const userInfo = localStg.get('userInfo') || emptyInfo;
// fix new property: buttons, this will be removed in the next version `1.1.0`
if (!userInfo.buttons) {
userInfo.buttons = [];
}
return userInfo; return userInfo;
} }

View File

@ -194,6 +194,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const vueRoutes = getAuthVueRoutes(sortRoutes); const vueRoutes = getAuthVueRoutes(sortRoutes);
resetVueRoutes();
addRoutesToVueRouter(vueRoutes); addRoutesToVueRouter(vueRoutes);
getGlobalMenus(sortRoutes); getGlobalMenus(sortRoutes);

View File

@ -10,6 +10,7 @@ import { useSvgIcon } from '@/hooks/common/icon';
* @param roles Roles * @param roles Roles
*/ */
export function filterAuthRoutesByRoles(routes: ElegantConstRoute[], roles: string[]) { 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'; const SUPER_ROLE = 'R_SUPER';
// if the user is super admin, then it is allowed to access all routes // if the user is super admin, then it is allowed to access all routes
@ -30,9 +31,7 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
const routeRoles = (route.meta && route.meta.roles) || []; const routeRoles = (route.meta && route.meta.roles) || [];
// if the route's "roles" is empty, then it is allowed to access // if the route's "roles" is empty, then it is allowed to access
if (!routeRoles.length) { const isEmptyRoles = !routeRoles.length;
return [route];
}
// if the user's role is included in the route's "roles", then it is allowed to access // if the user's role is included in the route's "roles", then it is allowed to access
const hasPermission = routeRoles.some(role => roles.includes(role)); const hasPermission = routeRoles.some(role => roles.includes(role));
@ -43,7 +42,7 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles)); filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles));
} }
return hasPermission ? [filterRoute] : []; return hasPermission || isEmptyRoles ? [filterRoute] : [];
} }
/** /**

View File

@ -60,6 +60,7 @@ declare namespace Api {
userId: string; userId: string;
userName: string; userName: string;
roles: string[]; roles: string[];
buttons: string[];
} }
} }

View File

@ -458,6 +458,13 @@ declare namespace App {
routeParam: string; routeParam: string;
backTab: string; backTab: string;
}; };
toggleAuth: {
toggleAccount: string;
authHook: string;
superAdminVisible: string;
adminVisible: string;
adminOrUserVisible: string;
};
}; };
manage: { manage: {
common: { common: {

View File

@ -32,7 +32,9 @@ declare module "@elegant-router/types" {
"function_hide-child_two": "/function/hide-child/two"; "function_hide-child_two": "/function/hide-child/two";
"function_multi-tab": "/function/multi-tab"; "function_multi-tab": "/function/multi-tab";
"function_request": "/function/request"; "function_request": "/function/request";
"function_super-page": "/function/super-page";
"function_tab": "/function/tab"; "function_tab": "/function/tab";
"function_toggle-auth": "/function/toggle-auth";
"home": "/home"; "home": "/home";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"manage": "/manage"; "manage": "/manage";
@ -119,7 +121,9 @@ declare module "@elegant-router/types" {
| "function_hide-child_two" | "function_hide-child_two"
| "function_multi-tab" | "function_multi-tab"
| "function_request" | "function_request"
| "function_super-page"
| "function_tab" | "function_tab"
| "function_toggle-auth"
| "home" | "home"
| "manage_menu" | "manage_menu"
| "manage_role" | "manage_role"

View File

@ -57,6 +57,8 @@ declare namespace Env {
* use "," to separate multiple codes * use "," to separate multiple codes
*/ */
readonly VITE_SERVICE_EXPIRED_TOKEN_CODES: string; readonly VITE_SERVICE_EXPIRED_TOKEN_CODES: string;
/** when the route mode is static, the defined super role */
readonly VITE_STATIC_SUPER_ROLE: string;
/** /**
* other backend service base url * other backend service base url
* *

View File

@ -38,6 +38,40 @@ async function handleSubmit() {
await validate(); await validate();
await authStore.login(model.userName, model.password); await authStore.login(model.userName, model.password);
} }
type AccountKey = 'super' | 'admin' | 'user';
interface Account {
key: AccountKey;
label: string;
userName: string;
password: string;
}
const accounts = computed<Account[]>(() => [
{
key: 'super',
label: $t('page.login.pwdLogin.superAdmin'),
userName: 'Super',
password: '123456'
},
{
key: 'admin',
label: $t('page.login.pwdLogin.admin'),
userName: 'Admin',
password: '123456'
},
{
key: 'user',
label: $t('page.login.pwdLogin.user'),
userName: 'User',
password: '123456'
}
]);
async function handleAccountLogin(account: Account) {
await authStore.login(account.userName, account.password);
}
</script> </script>
<template> <template>
@ -71,6 +105,12 @@ async function handleSubmit() {
{{ $t(loginModuleRecord.register) }} {{ $t(loginModuleRecord.register) }}
</NButton> </NButton>
</div> </div>
<NDivider class="text-14px text-#666 !m-0">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</NDivider>
<div class="flex-center gap-12px">
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
{{ item.label }}
</NButton>
</div>
</NSpace> </NSpace>
</NForm> </NForm>
</template> </template>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useLoading } from '@sa/hooks';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore } from '@/store/modules/auth';
import { useAuth } from '@/hooks/business/auth';
const appStore = useAppStore();
const authStore = useAuthStore();
const { hasAuth } = useAuth();
const { loading, startLoading, endLoading } = useLoading();
type AccountKey = 'super' | 'admin' | 'user';
interface Account {
key: AccountKey;
label: string;
userName: string;
password: string;
}
const accounts = computed<Account[]>(() => [
{
key: 'super',
label: $t('page.login.pwdLogin.superAdmin'),
userName: 'Super',
password: '123456'
},
{
key: 'admin',
label: $t('page.login.pwdLogin.admin'),
userName: 'Admin',
password: '123456'
},
{
key: 'user',
label: $t('page.login.pwdLogin.user'),
userName: 'User',
password: '123456'
}
]);
const loginAccount = ref<AccountKey>('super');
async function handleToggleAccount(account: Account) {
loginAccount.value = account.key;
startLoading();
await authStore.login(account.userName, account.password, false);
endLoading();
appStore.reloadPage();
}
</script>
<template>
<NSpace vertical :size="16">
<NCard :title="$t('route.function_toggle-auth')" :bordered="false" size="small" segmented class="card-wrapper">
<NDescriptions bordered :column="1">
<NDescriptionsItem :label="$t('page.manage.user.userRole')">
<NSpace>
<NTag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</NTag>
</NSpace>
</NDescriptionsItem>
<NDescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
<NSpace>
<NButton
v-for="account in accounts"
:key="account.key"
:loading="loading && loginAccount === account.key"
:disabled="loading && loginAccount !== account.key"
@click="handleToggleAccount(account)"
>
{{ account.label }}
</NButton>
</NSpace>
</NDescriptionsItem>
</NDescriptions>
</NCard>
<NCard
:title="$t('page.function.toggleAuth.authHook')"
:bordered="false"
size="small"
segmented
class="card-wrapper"
>
<NSpace>
<NButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</NButton>
<NButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</NButton>
<NButton v-if="hasAuth('B_CODE3')">
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
</NButton>
</NSpace>
</NCard>
</NSpace>
</template>
<style scoped></style>