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
# auth route mode: static dynamic
VITE_AUTH_ROUTE_MODE=static
VITE_AUTH_ROUTE_MODE=dynamic
# static auth route 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
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>
import { computed } from 'vue';
import { $t } from '@/locales';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({ name: 'ExceptionBase' });
@ -19,6 +20,8 @@ interface Props {
const props = defineProps<Props>();
const { routerPushByKey } = useRouterPush();
const iconMap: Record<ExceptionType, string> = {
'403': 'no-permission',
'404': 'not-found',
@ -33,9 +36,7 @@ const icon = computed(() => iconMap[props.type]);
<div class="flex text-400px text-primary">
<SvgIcon :local-icon="icon" />
</div>
<RouterLink to="/">
<NButton type="primary">{{ $t('common.backToHome') }}</NButton>
</RouterLink>
<NButton type="primary" @click="routerPushByKey('root')">{{ $t('common.backToHome') }}</NButton>
</div>
</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_three': 'Three',
function_request: 'Request',
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
manage: 'System Manage',
manage_user: 'User Manage',
'manage_user-detail': 'User Detail',
@ -187,9 +189,9 @@ const local: App.I18n.Schema = {
register: 'Register',
otherAccountLogin: 'Other Account Login',
otherLoginMode: 'Other Login Mode',
superAdmin: 'Super Administrator',
admin: 'Administrator',
user: 'Ordinary User'
superAdmin: 'Super Admin',
admin: 'Admin',
user: 'User'
},
codeLogin: {
title: 'Verification Code Login',
@ -275,6 +277,13 @@ const local: App.I18n.Schema = {
multiTab: {
routeParam: 'Route Param',
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: {

View File

@ -148,6 +148,8 @@ const local: App.I18n.Schema = {
'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三',
function_request: '请求',
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
manage: '系统管理',
manage_user: '用户管理',
'manage_user-detail': '用户详情',
@ -275,6 +277,13 @@ const local: App.I18n.Schema = {
multiTab: {
routeParam: '路由参数',
backTab: '返回 function_tab'
},
toggleAuth: {
toggleAccount: '切换账号',
authHook: '权限钩子函数 `hasAuth`',
superAdminVisible: '超级管理员可见',
adminVisible: '管理员可见',
adminOrUserVisible: '管理员和用户可见'
}
},
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_multi-tab": () => import("@/views/function/multi-tab/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_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
home: () => import("@/views/home/index.vue"),
manage_menu: () => import("@/views/manage/menu/index.vue"),
manage_role: () => import("@/views/manage/role/index.vue"),

View File

@ -64,7 +64,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: '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',
children: [
@ -124,7 +125,19 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: '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: {
title: '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_multi-tab": "/function/multi-tab",
"function_request": "/function/request",
"function_super-page": "/function/super-page",
"function_tab": "/function/tab",
"function_toggle-auth": "/function/toggle-auth",
"home": "/home",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"manage": "/manage",

View File

@ -27,13 +27,10 @@ export function createPermissionGuard(router: Router) {
// 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, 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 SUPER_ADMIN = 'R_SUPER';
const hasPermission =
!routeRoles.length ||
authStore.userInfo.roles.includes(SUPER_ADMIN) ||
authStore.userInfo.roles.some(role => routeRoles.includes(role));
!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

View File

@ -18,6 +18,13 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
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 */
const isLogin = computed(() => Boolean(token.value));
@ -41,8 +48,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
*
* @param userName User name
* @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();
const { data: loginToken, error } = await fetchLogin(userName, password);
@ -53,7 +61,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
if (pass) {
await routeStore.initAuthRoute();
if (redirect) {
await redirectFromLogin();
}
if (routeStore.isInitAuthRoute) {
window.$notification?.success({
@ -94,6 +104,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
return {
token,
userInfo,
isStaticSuper,
isLogin,
loginLoading,
resetStore,

View File

@ -10,10 +10,16 @@ export function getUserInfo() {
const emptyInfo: Api.Auth.UserInfo = {
userId: '',
userName: '',
roles: []
roles: [],
buttons: []
};
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;
}

View File

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

View File

@ -10,6 +10,7 @@ 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
@ -30,9 +31,7 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
const routeRoles = (route.meta && route.meta.roles) || [];
// if the route's "roles" is empty, then it is allowed to access
if (!routeRoles.length) {
return [route];
}
const isEmptyRoles = !routeRoles.length;
// 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));
@ -43,7 +42,7 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
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;
userName: string;
roles: string[];
buttons: string[];
}
}

View File

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

View File

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

View File

@ -57,6 +57,8 @@ declare namespace Env {
* use "," to separate multiple codes
*/
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
*

View File

@ -38,6 +38,40 @@ async function handleSubmit() {
await validate();
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>
<template>
@ -71,6 +105,12 @@ async function handleSubmit() {
{{ $t(loginModuleRecord.register) }}
</NButton>
</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>
</NForm>
</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>