permission system and no access redirect
This commit is contained in:
parent
214f0ddf21
commit
cb80771f7a
8 changed files with 107 additions and 18 deletions
|
@ -3,25 +3,33 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useAccountStore } from "@/stores/account";
|
import { useAccountStore } from "@/stores/account";
|
||||||
import { jwtDecode, type JwtPayload } from "jwt-decode";
|
import { jwtDecode, type JwtPayload } from "jwt-decode";
|
||||||
import { refreshToken } from "../serverCom";
|
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) {
|
export async function isAuthenticated(to: any, from: any, next: any) {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
if (auth.authCheck && localStorage.getItem("access_token")) {
|
if (auth.authCheck && localStorage.getItem("access_token") && localStorage.getItem("refresh_token")) {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await isAuthenticatedPromise()
|
await isAuthenticatedPromise()
|
||||||
.then(async (result: any) => {
|
.then(async (result: Payload) => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
next();
|
next();
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: string) => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
next({ name: "login" });
|
next({ name: err ?? "login" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,29 +41,38 @@ export async function isAuthenticatedPromise(): Promise<Payload> {
|
||||||
try {
|
try {
|
||||||
decoded = jwtDecode<Payload>(localStorage.getItem("accessToken") ?? "");
|
decoded = jwtDecode<Payload>(localStorage.getItem("accessToken") ?? "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject("failed");
|
auth.setFailed();
|
||||||
|
reject("login");
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.setSuccess();
|
|
||||||
if (typeof decoded == "string" || !decoded) {
|
if (typeof decoded == "string" || !decoded) {
|
||||||
reject("failed");
|
auth.setFailed();
|
||||||
|
reject("login");
|
||||||
} else {
|
} else {
|
||||||
// check jwt expiry
|
// check jwt expiry
|
||||||
const exp = decoded.exp ?? 0;
|
const exp = decoded.exp ?? 0;
|
||||||
const localTimezoneOffset = new Date().getTimezoneOffset();
|
const correctedLocalTime = new Date().getTime();
|
||||||
const correctedLocalTime = new Date().getTime() + localTimezoneOffset * 60000;
|
|
||||||
if (exp < Math.floor(correctedLocalTime / 1000)) {
|
if (exp < Math.floor(correctedLocalTime / 1000)) {
|
||||||
await refreshToken()
|
await refreshToken()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("fetched new token");
|
console.log("fetched new token");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err: string) => {
|
||||||
reject("expired");
|
console.log("expired");
|
||||||
|
auth.setFailed();
|
||||||
|
reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var { firstname, lastname, mail, username } = decoded;
|
var { firstname, lastname, mail, username, permissions } = decoded;
|
||||||
account.setAccountData(firstname, lastname, mail, username);
|
|
||||||
|
if (Object.keys(permissions).length === 0) {
|
||||||
|
auth.setFailed();
|
||||||
|
reject("nopermissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.setSuccess();
|
||||||
|
account.setAccountData(firstname, lastname, mail, username, permissions);
|
||||||
resolve(decoded);
|
resolve(decoded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,6 +42,11 @@ const router = createRouter({
|
||||||
component: () => import("../views/admin/View.vue"),
|
component: () => import("../views/admin/View.vue"),
|
||||||
beforeEnter: [isAuthenticated],
|
beforeEnter: [isAuthenticated],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/nopermissions",
|
||||||
|
name: "nopermissions",
|
||||||
|
component: () => import("../views/NoPermission.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "404",
|
name: "404",
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { isAuthenticatedPromise, type Payload } from "./router/authGuards";
|
||||||
|
import router from "./router";
|
||||||
|
|
||||||
let devMode = process.env.NODE_ENV === "development";
|
let devMode = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: devMode ? "http://localhost:5000" : server_adress,
|
baseURL: devMode ? "http://localhost:5000" : process.env.SERVER_ADDRESS,
|
||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
Pragma: "no-cache",
|
Pragma: "no-cache",
|
||||||
|
@ -59,17 +61,22 @@ export async function refreshToken(): Promise<void> {
|
||||||
accessToken: localStorage.getItem("accessToken"),
|
accessToken: localStorage.getItem("accessToken"),
|
||||||
refreshToken: localStorage.getItem("refreshToken"),
|
refreshToken: localStorage.getItem("refreshToken"),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
const { accessToken, refreshToken } = response.data;
|
const { accessToken, refreshToken } = response.data;
|
||||||
|
|
||||||
localStorage.setItem("accessToken", accessToken);
|
localStorage.setItem("accessToken", accessToken);
|
||||||
localStorage.setItem("refreshToken", refreshToken);
|
localStorage.setItem("refreshToken", refreshToken);
|
||||||
|
|
||||||
|
await isAuthenticatedPromise().catch((err: string) => {
|
||||||
|
router.push({ name: err ?? "login" });
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error refreshing token:", error);
|
console.error("Error refreshing token:", error);
|
||||||
reject();
|
reject("login");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import type { PermissionObject } from "../types/permissionTypes";
|
||||||
|
|
||||||
export const useAccountStore = defineStore("account", {
|
export const useAccountStore = defineStore("account", {
|
||||||
state: () => {
|
state: () => {
|
||||||
|
@ -7,6 +8,7 @@ export const useAccountStore = defineStore("account", {
|
||||||
lastname: "" as string,
|
lastname: "" as string,
|
||||||
mail: "" as string,
|
mail: "" as string,
|
||||||
alias: "" as string,
|
alias: "" as string,
|
||||||
|
permissions: {} as PermissionObject,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -15,11 +17,12 @@ export const useAccountStore = defineStore("account", {
|
||||||
localStorage.removeItem("refreshToken");
|
localStorage.removeItem("refreshToken");
|
||||||
window.open("/login", "_self");
|
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.firstname = firstname;
|
||||||
this.lastname = lastname;
|
this.lastname = lastname;
|
||||||
this.mail = mail;
|
this.mail = mail;
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
|
this.permissions = permissions;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { shallowRef, defineAsyncComponent } from "vue";
|
import { shallowRef, defineAsyncComponent } from "vue";
|
||||||
|
import { useAccountStore } from "../account";
|
||||||
|
|
||||||
export interface navigationModel {
|
export interface navigationModel {
|
||||||
club: navigationSplitModel;
|
club: navigationSplitModel;
|
||||||
|
@ -30,6 +31,7 @@ export interface navigationLinkModel {
|
||||||
|
|
||||||
export const useNavigationStore = defineStore("navigation", {
|
export const useNavigationStore = defineStore("navigation", {
|
||||||
state: () => {
|
state: () => {
|
||||||
|
const accountStore = useAccountStore();
|
||||||
return {
|
return {
|
||||||
activeNavigation: "club" as topLevelNavigationType,
|
activeNavigation: "club" as topLevelNavigationType,
|
||||||
activeLink: null as null | navigationLinkModel,
|
activeLink: null as null | navigationLinkModel,
|
||||||
|
|
|
@ -10,5 +10,8 @@ export const useAuthStore = defineStore("auth", {
|
||||||
setSuccess() {
|
setSuccess() {
|
||||||
this.authCheck = true;
|
this.authCheck = true;
|
||||||
},
|
},
|
||||||
|
setFailed() {
|
||||||
|
this.authCheck = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
20
src/types/permissionTypes.ts
Normal file
20
src/types/permissionTypes.ts
Normal file
|
@ -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<PermissionType> | "*";
|
||||||
|
} & { all?: PermissionType | "*" };
|
||||||
|
} & {
|
||||||
|
admin?: boolean;
|
||||||
|
};
|
32
src/views/NoPermission.vue
Normal file
32
src/views/NoPermission.vue
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<br />
|
||||||
|
<h1 class="w-full p-4 text-center font-bold text-3xl">Kein Zugriff</h1>
|
||||||
|
<br />
|
||||||
|
<p class="w-full text-center">
|
||||||
|
Sie haben keine Berechtigungen. <br />
|
||||||
|
Um Zugriff auf das Admin-Portal zu erhalten, wenden Sie sich an einen Administrator.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<button primary class="!w-fit" @click="refetch">Zum Admin-Portal</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { refreshToken } from "@/serverCom";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
methods: {
|
||||||
|
async refetch() {
|
||||||
|
await refreshToken()
|
||||||
|
.then(() => {
|
||||||
|
this.$router.push({ name: "admin" });
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in a new issue