From cb80771f7a50c2bc50955910d4e25f7d89e92f84 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 26 Aug 2024 13:46:54 +0200 Subject: [PATCH] permission system and no access redirect --- src/router/authGuards.ts | 45 +++++++++++++++++++++++----------- src/router/index.ts | 5 ++++ src/serverCom.ts | 13 +++++++--- src/stores/account.ts | 5 +++- src/stores/admin/navigation.ts | 2 ++ src/stores/auth.ts | 3 +++ src/types/permissionTypes.ts | 20 +++++++++++++++ src/views/NoPermission.vue | 32 ++++++++++++++++++++++++ 8 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 src/types/permissionTypes.ts create mode 100644 src/views/NoPermission.vue diff --git a/src/router/authGuards.ts b/src/router/authGuards.ts index d694615..a59f762 100644 --- a/src/router/authGuards.ts +++ b/src/router/authGuards.ts @@ -3,25 +3,33 @@ import { useAuthStore } from "@/stores/auth"; import { useAccountStore } from "@/stores/account"; import { jwtDecode, type JwtPayload } from "jwt-decode"; import { refreshToken } from "../serverCom"; +import type { PermissionObject } from "../types/permissionTypes"; -type Payload = JwtPayload & { userId: number; username: string; firstname: string; lastname: string; mail: string }; +export type Payload = JwtPayload & { + userId: number; + username: string; + firstname: string; + lastname: string; + mail: string; + permissions: PermissionObject; +}; export async function isAuthenticated(to: any, from: any, next: any) { const auth = useAuthStore(); NProgress.start(); - if (auth.authCheck && localStorage.getItem("access_token")) { + if (auth.authCheck && localStorage.getItem("access_token") && localStorage.getItem("refresh_token")) { NProgress.done(); next(); return; } await isAuthenticatedPromise() - .then(async (result: any) => { + .then(async (result: Payload) => { NProgress.done(); next(); }) - .catch((err: Error) => { + .catch((err: string) => { NProgress.done(); - next({ name: "login" }); + next({ name: err ?? "login" }); }); } @@ -33,29 +41,38 @@ export async function isAuthenticatedPromise(): Promise { try { decoded = jwtDecode(localStorage.getItem("accessToken") ?? ""); } catch (error) { - reject("failed"); + auth.setFailed(); + reject("login"); } - auth.setSuccess(); if (typeof decoded == "string" || !decoded) { - reject("failed"); + auth.setFailed(); + reject("login"); } else { // check jwt expiry const exp = decoded.exp ?? 0; - const localTimezoneOffset = new Date().getTimezoneOffset(); - const correctedLocalTime = new Date().getTime() + localTimezoneOffset * 60000; + const correctedLocalTime = new Date().getTime(); if (exp < Math.floor(correctedLocalTime / 1000)) { await refreshToken() .then(() => { console.log("fetched new token"); }) - .catch(() => { - reject("expired"); + .catch((err: string) => { + console.log("expired"); + auth.setFailed(); + reject(err); }); } - var { firstname, lastname, mail, username } = decoded; - account.setAccountData(firstname, lastname, mail, username); + var { firstname, lastname, mail, username, permissions } = decoded; + + if (Object.keys(permissions).length === 0) { + auth.setFailed(); + reject("nopermissions"); + } + + auth.setSuccess(); + account.setAccountData(firstname, lastname, mail, username, permissions); resolve(decoded); } }); diff --git a/src/router/index.ts b/src/router/index.ts index f01480d..e1b1114 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -42,6 +42,11 @@ const router = createRouter({ component: () => import("../views/admin/View.vue"), beforeEnter: [isAuthenticated], }, + { + path: "/nopermissions", + name: "nopermissions", + component: () => import("../views/NoPermission.vue"), + }, { path: "/:pathMatch(.*)*", name: "404", diff --git a/src/serverCom.ts b/src/serverCom.ts index 82667bd..9022b09 100644 --- a/src/serverCom.ts +++ b/src/serverCom.ts @@ -1,9 +1,11 @@ import axios from "axios"; +import { isAuthenticatedPromise, type Payload } from "./router/authGuards"; +import router from "./router"; let devMode = process.env.NODE_ENV === "development"; const http = axios.create({ - baseURL: devMode ? "http://localhost:5000" : server_adress, + baseURL: devMode ? "http://localhost:5000" : process.env.SERVER_ADDRESS, headers: { "Cache-Control": "no-cache", Pragma: "no-cache", @@ -59,17 +61,22 @@ export async function refreshToken(): Promise { accessToken: localStorage.getItem("accessToken"), refreshToken: localStorage.getItem("refreshToken"), }) - .then((response) => { + .then(async (response) => { const { accessToken, refreshToken } = response.data; localStorage.setItem("accessToken", accessToken); localStorage.setItem("refreshToken", refreshToken); + await isAuthenticatedPromise().catch((err: string) => { + router.push({ name: err ?? "login" }); + reject(err); + }); + resolve(); }) .catch((error) => { console.error("Error refreshing token:", error); - reject(); + reject("login"); }); }); } diff --git a/src/stores/account.ts b/src/stores/account.ts index 8c11fd9..51e3f68 100644 --- a/src/stores/account.ts +++ b/src/stores/account.ts @@ -1,4 +1,5 @@ import { defineStore } from "pinia"; +import type { PermissionObject } from "../types/permissionTypes"; export const useAccountStore = defineStore("account", { state: () => { @@ -7,6 +8,7 @@ export const useAccountStore = defineStore("account", { lastname: "" as string, mail: "" as string, alias: "" as string, + permissions: {} as PermissionObject, }; }, actions: { @@ -15,11 +17,12 @@ export const useAccountStore = defineStore("account", { localStorage.removeItem("refreshToken"); window.open("/login", "_self"); }, - setAccountData(firstname: string, lastname: string, mail: string, alias: string) { + setAccountData(firstname: string, lastname: string, mail: string, alias: string, permissions: PermissionObject) { 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 7509445..c6b532f 100644 --- a/src/stores/admin/navigation.ts +++ b/src/stores/admin/navigation.ts @@ -1,5 +1,6 @@ import { defineStore } from "pinia"; import { shallowRef, defineAsyncComponent } from "vue"; +import { useAccountStore } from "../account"; export interface navigationModel { club: navigationSplitModel; @@ -30,6 +31,7 @@ export interface navigationLinkModel { export const useNavigationStore = defineStore("navigation", { state: () => { + const accountStore = useAccountStore(); return { activeNavigation: "club" as topLevelNavigationType, activeLink: null as null | navigationLinkModel, diff --git a/src/stores/auth.ts b/src/stores/auth.ts index fdaf4d2..21133cc 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -10,5 +10,8 @@ export const useAuthStore = defineStore("auth", { setSuccess() { this.authCheck = true; }, + setFailed() { + this.authCheck = false; + }, }, }); diff --git a/src/types/permissionTypes.ts b/src/types/permissionTypes.ts new file mode 100644 index 0000000..cc276b0 --- /dev/null +++ b/src/types/permissionTypes.ts @@ -0,0 +1,20 @@ +export type PermissionSection = "club" | "settings" | "user"; + +export type PermissionModule = "protocoll" | "user"; + +export type PermissionType = "create" | "read" | "update" | "delete"; + +export type PermissionString = + | `${PermissionSection}.${PermissionModule}.${PermissionType}` // für spezifische Berechtigungen + | `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul + | `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt + | `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt + | "*"; // für Admin + +export type PermissionObject = { + [section in PermissionSection]?: { + [module in PermissionModule]?: Array | "*"; + } & { all?: PermissionType | "*" }; +} & { + admin?: boolean; +}; diff --git a/src/views/NoPermission.vue b/src/views/NoPermission.vue new file mode 100644 index 0000000..9ca26b2 --- /dev/null +++ b/src/views/NoPermission.vue @@ -0,0 +1,32 @@ + + + + +