navigation permission and ability checker

This commit is contained in:
Julian Krauser 2024-08-26 17:56:07 +02:00
parent cb80771f7a
commit 35cba95887
6 changed files with 213 additions and 92 deletions

View file

@ -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<Payload> {
return new Promise<Payload>(async (resolve, reject) => {
const auth = useAuthStore();
const account = useAccountStore();
const ability = useAbilityStore();
let decoded: Payload | string = "";
try {
decoded = jwtDecode<Payload>(localStorage.getItem("accessToken") ?? "");
@ -72,7 +74,8 @@ export async function isAuthenticatedPromise(): Promise<Payload> {
}
auth.setSuccess();
account.setAccountData(firstname, lastname, mail, username, permissions);
account.setAccountData(firstname, lastname, mail, username);
ability.setAbility(permissions);
resolve(decoded);
}
});

33
src/stores/ability.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -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";
</script>
<script lang="ts">
@ -57,13 +58,26 @@ export default defineComponent({
},
},
created() {
useAbilityStore().$subscribe(() => {
this.updateTopLevel();
this.updateNavigation();
});
this.updateTopLevel();
this.updateNavigation();
this.setLink(this.activeTopLevelObject.levelDefault);
},
beforeUnmount() {
this.resetNavigation();
},
methods: {
...mapActions(useNavigationStore, ["setLink", "resetNavigation", "setTopLevel"]),
...mapActions(useNavigationStore, [
"setLink",
"resetNavigation",
"setTopLevel",
"updateTopLevel",
"updateNavigation",
]),
},
});
</script>