From 35cba95887df1cd49906faa4aa9b87b9d77278c8 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 26 Aug 2024 17:56:07 +0200 Subject: [PATCH] navigation permission and ability checker --- src/router/authGuards.ts | 5 +- src/stores/ability.ts | 33 +++++ src/stores/account.ts | 5 +- src/stores/admin/navigation.ts | 234 +++++++++++++++++++++------------ src/types/permissionTypes.ts | 12 +- src/views/admin/View.vue | 16 ++- 6 files changed, 213 insertions(+), 92 deletions(-) create mode 100644 src/stores/ability.ts diff --git a/src/router/authGuards.ts b/src/router/authGuards.ts index a59f762..2e14dae 100644 --- a/src/router/authGuards.ts +++ b/src/router/authGuards.ts @@ -4,6 +4,7 @@ import { useAccountStore } from "@/stores/account"; import { jwtDecode, type JwtPayload } from "jwt-decode"; import { refreshToken } from "../serverCom"; import type { PermissionObject } from "../types/permissionTypes"; +import { useAbilityStore } from "../stores/ability"; export type Payload = JwtPayload & { userId: number; @@ -37,6 +38,7 @@ export async function isAuthenticatedPromise(): Promise { return new Promise(async (resolve, reject) => { const auth = useAuthStore(); const account = useAccountStore(); + const ability = useAbilityStore(); let decoded: Payload | string = ""; try { decoded = jwtDecode(localStorage.getItem("accessToken") ?? ""); @@ -72,7 +74,8 @@ export async function isAuthenticatedPromise(): Promise { } auth.setSuccess(); - account.setAccountData(firstname, lastname, mail, username, permissions); + account.setAccountData(firstname, lastname, mail, username); + ability.setAbility(permissions); resolve(decoded); } }); diff --git a/src/stores/ability.ts b/src/stores/ability.ts new file mode 100644 index 0000000..ae743ca --- /dev/null +++ b/src/stores/ability.ts @@ -0,0 +1,33 @@ +import { defineStore } from "pinia"; +import type { PermissionModule, PermissionObject, PermissionSection, PermissionType } from "../types/permissionTypes"; + +export const useAbilityStore = defineStore("ability", { + state: () => { + return { + permissions: {} as PermissionObject, + }; + }, + getters: { + can: + (state) => + (type: PermissionType | "admin", section: PermissionSection, module?: PermissionModule): boolean => { + const permissions = state.permissions; + if (type == "admin") return permissions.admin ?? false; + if (permissions.admin) return true; + if ( + (!module && permissions[section] != undefined) || + permissions[section]?.all == "*" || + permissions[section]?.all?.includes(type) + ) + return true; + if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type))) + return true; + return false; + }, + }, + actions: { + setAbility(permissions: PermissionObject) { + this.permissions = permissions; + }, + }, +}); diff --git a/src/stores/account.ts b/src/stores/account.ts index 51e3f68..144d41a 100644 --- a/src/stores/account.ts +++ b/src/stores/account.ts @@ -1,5 +1,6 @@ import { defineStore } from "pinia"; import type { PermissionObject } from "../types/permissionTypes"; +import { useAbilityStore } from "./ability"; export const useAccountStore = defineStore("account", { state: () => { @@ -8,7 +9,6 @@ export const useAccountStore = defineStore("account", { lastname: "" as string, mail: "" as string, alias: "" as string, - permissions: {} as PermissionObject, }; }, actions: { @@ -17,12 +17,11 @@ export const useAccountStore = defineStore("account", { localStorage.removeItem("refreshToken"); window.open("/login", "_self"); }, - setAccountData(firstname: string, lastname: string, mail: string, alias: string, permissions: PermissionObject) { + setAccountData(firstname: string, lastname: string, mail: string, alias: string) { this.firstname = firstname; this.lastname = lastname; this.mail = mail; this.alias = alias; - this.permissions = permissions; }, }, }); diff --git a/src/stores/admin/navigation.ts b/src/stores/admin/navigation.ts index c6b532f..99fd45a 100644 --- a/src/stores/admin/navigation.ts +++ b/src/stores/admin/navigation.ts @@ -1,6 +1,6 @@ import { defineStore } from "pinia"; import { shallowRef, defineAsyncComponent } from "vue"; -import { useAccountStore } from "../account"; +import { useAbilityStore } from "../ability"; export interface navigationModel { club: navigationSplitModel; @@ -31,94 +31,11 @@ export interface navigationLinkModel { export const useNavigationStore = defineStore("navigation", { state: () => { - const accountStore = useAccountStore(); return { activeNavigation: "club" as topLevelNavigationType, activeLink: null as null | navigationLinkModel, - topLevel: [ - { - key: "club", - title: "Verein", - levelDefault: "#members", - }, - { - key: "settings", - title: "Einstellungen", - levelDefault: "#qualification", - }, - { - key: "user", - title: "Benutzer", - levelDefault: "#user", - }, - ] as Array, - navigation: { - club: { - mainTitle: "Verein", - main: [ - { - key: "#members", - title: "Mitglieder", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#calendar", - title: "Termine", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#newsletter", - title: "Newsletter", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#protocol", - title: "Protokolle", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - ], - }, - settings: { - mainTitle: "Einstellungen", - main: [ - { - key: "#qualification", - title: "Qualifikationen", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#award", - title: "Auszeichnungen", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#executive_position", - title: "Vereinsämter", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#communication", - title: "Mitgliederdaten", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - ], - }, - user: { - mainTitle: "Benutzer", - main: [ - { - key: "#user", - title: "Benutzer", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - { - key: "#roles", - title: "Rollen", - component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), - }, - ], - }, - } as navigationModel, + topLevel: [] as Array, + navigation: {} as navigationModel, componentOverwrite: null as null | any, }; }, @@ -161,5 +78,150 @@ export const useNavigationStore = defineStore("navigation", { resetNavigation() { this.$reset(); }, + updateTopLevel() { + const abilityStore = useAbilityStore(); + this.topLevel = [ + ...(abilityStore.can("read", "club") + ? [ + { + key: "club", + title: "Verein", + levelDefault: "#members", + } as topLevelNavigationModel, + ] + : []), + ...(abilityStore.can("read", "settings") + ? [ + { + key: "settings", + title: "Einstellungen", + levelDefault: "#qualification", + } as topLevelNavigationModel, + ] + : []), + ...(abilityStore.can("read", "user") + ? [ + { + key: "user", + title: "Benutzer", + levelDefault: "#user", + } as topLevelNavigationModel, + ] + : []), + ]; + if (this.topLevel.findIndex((e) => e.key == this.activeNavigation) == -1) + this.activeNavigation = this.topLevel[0]?.key ?? "club"; + }, + updateNavigation() { + const abilityStore = useAbilityStore(); + this.navigation = { + club: { + mainTitle: "Verein", + main: [ + ...(abilityStore.can("read", "club", "members") + ? [ + { + key: "#members", + title: "Mitglieder", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "club", "calendar") + ? [ + { + key: "#calendar", + title: "Termine", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "club", "newsletter") + ? [ + { + key: "#newsletter", + title: "Newsletter", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "club", "protocoll") + ? [ + { + key: "#protocol", + title: "Protokolle", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ], + }, + settings: { + mainTitle: "Einstellungen", + main: [ + ...(abilityStore.can("read", "settings", "qualification") + ? [ + { + key: "#qualification", + title: "Qualifikationen", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "settings", "award") + ? [ + { + key: "#award", + title: "Auszeichnungen", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "settings", "executive_position") + ? [ + { + key: "#executive_position", + title: "Vereinsämter", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("read", "settings", "communication") + ? [ + { + key: "#communication", + title: "Mitgliederdaten", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ], + }, + user: { + mainTitle: "Benutzer", + main: [ + ...(abilityStore.can("read", "user", "user") + ? [ + { + key: "#user", + title: "Benutzer", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ...(abilityStore.can("admin", "user", "role") + ? [ + { + key: "#role", + title: "Rollen", + component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))), + }, + ] + : []), + ], + }, + } as navigationModel; + if (this.topLevel.findIndex((e) => e.key == this.activeLink?.key) == -1) this.setLink(null); + }, }, }); diff --git a/src/types/permissionTypes.ts b/src/types/permissionTypes.ts index cc276b0..fd6d6dd 100644 --- a/src/types/permissionTypes.ts +++ b/src/types/permissionTypes.ts @@ -1,6 +1,16 @@ export type PermissionSection = "club" | "settings" | "user"; -export type PermissionModule = "protocoll" | "user"; +export type PermissionModule = + | "members" + | "calendar" + | "newsletter" + | "protocoll" + | "qualification" + | "award" + | "executive_position" + | "communication" + | "user" + | "role"; export type PermissionType = "create" | "read" | "update" | "delete"; diff --git a/src/views/admin/View.vue b/src/views/admin/View.vue index 723dcea..6ba55ff 100644 --- a/src/views/admin/View.vue +++ b/src/views/admin/View.vue @@ -28,6 +28,7 @@ import { useNavigationStore } from "@/stores/admin/navigation"; import SidebarLayout from "@/layouts/Sidebar.vue"; import SidebarTemplate from "@/templates/Sidebar.vue"; import RoutingLink from "@/components/admin/RoutingLink.vue"; +import { useAbilityStore } from "../../stores/ability";