@@ -19,15 +21,15 @@ function PermissionDenied() {
{t("permission-wall.subtitle")}
-
+
{t("permission-wall.message")}
-
+
{t("permission-wall.contact")}
-
+
.
-
+
{t("permission-wall.quote")} -{" "}
{t("permission-wall.quote_author")}
@@ -53,8 +55,20 @@ export default function PermissionWall({
mustHave?: "any" | "all";
children: ReactNode;
}) {
- const auth = useAuthState();
- const perm = auth.getPermissions();
+ const permissionsState = usePermissionsState();
+
+ if (permissionsState.isPending) {
+ return ;
+ }
+
+ if (permissionsState.isError) {
+ return (
+
+ );
+ }
+
+ const perm = permissionsState.permissions;
+
let allowed = false;
if (mustHave === "all") {
allowed = perm.hasRequiredPermissions(requiredPermissions);
@@ -66,5 +80,5 @@ export default function PermissionWall({
if (allowed) {
return <>{children}>;
}
- return PermissionDenied();
+ return ;
}
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index d8ac25a1..d90357a7 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,11 +1,15 @@
"use client";
+import { getMyPermissionsOptions } from "@/api/@tanstack/react-query.gen";
import type { BearerResponse } from "@/api";
import type { ActionEnum, TargetEnum } from "@/api";
+import { useQuery } from "@tanstack/react-query";
+import { useMemo } from "react";
import { create } from "zustand";
export type RequiredPermission = [ActionEnum, TargetEnum];
-class PermissionMap extends Map> {
+
+export class PermissionMap extends Map> {
/**
* Checks a users permission against a list of required permissions.
*
@@ -15,9 +19,8 @@ class PermissionMap extends Map> {
* Checking that a user has `view` permission for `CAR` and `manage` permission for `USER`
* ```ts
* import type { ActionEnum, TargetEnum } from "@/api";
- * import { useAuthState } from "@/lib/auth";
- * const auth = useAuthState();
- * const permissions = auth.getPermissions();
+ * import { usePermissions } from "@/lib/auth";
+ * const permissions = usePermissions();
* const isAllowed = permissions.hasRequiredPermissions([[ActionEnum.VIEW, TargetEnum.CAR], [ActionEnum.MANAGE, TargetEnum.USER]]);
* ```
*/
@@ -37,58 +40,59 @@ type AuthState = {
setAccessToken: (data: BearerResponse) => void;
authorizationHeader: () => string | null;
isAuthenticated: () => boolean;
- getPermissions: () => PermissionMap;
};
-export const useAuthState = create((set, get) => {
- let permissionMap = new PermissionMap();
+function buildPermissionMap(
+ permissions: [string, string][] | null | undefined,
+): PermissionMap {
+ const map = new PermissionMap();
+ if (!permissions) return map;
- function buildPermissionMap(token: BearerResponse | null): PermissionMap {
- const map = new PermissionMap();
- if (!token) return map;
- try {
- const payload = JSON.parse(
- Buffer.from(token.access_token.split(".")[1], "base64").toString(),
- ) as { permissions: string[] };
+ for (const [actionStr, targetStr] of permissions) {
+ const actionEnum = actionStr as ActionEnum;
+ const targetEnum = targetStr as TargetEnum;
- for (const entry of payload.permissions) {
- const parts = entry.split(":");
- if (parts.length !== 2) continue;
+ if (!actionEnum || !targetEnum) {
+ console.warn(
+ `Unknown permission from backend: ${actionStr}:${targetStr}`,
+ );
+ continue;
+ }
- const [actionStr, targetStr] = parts;
+ if (!map.has(targetEnum)) {
+ map.set(targetEnum, new Set());
+ }
+ // biome-ignore lint/style/noNonNullAssertion: Just checked that it exists
+ map.get(targetEnum)!.add(actionEnum);
+ }
- const actionEnum = actionStr as ActionEnum;
- const targetEnum = targetStr as TargetEnum;
+ return map;
+}
- if (!actionEnum || !targetEnum) continue;
+export function usePermissions(): PermissionMap {
+ return usePermissionsState().permissions;
+}
- if (!map.has(targetEnum)) {
- map.set(targetEnum, new Set());
- }
- // biome-ignore lint/style/noNonNullAssertion: Just checked that it exists
- map.get(targetEnum)!.add(actionEnum);
- }
- } catch {
- // If decoding or parsing fails, just return an empty map
- }
+export function usePermissionsState() {
+ const query = useQuery({
+ ...getMyPermissionsOptions(),
+ staleTime: 60 * 1000,
+ });
+ const permissions = useMemo(
+ () => buildPermissionMap(query.data),
+ [query.data],
+ );
- return map;
- }
+ return { ...query, permissions };
+}
- function updatePermissions(token: BearerResponse | null) {
- permissionMap = buildPermissionMap(token);
- }
+export type UsePermissionsState = ReturnType;
+export const useAuthState = create((set, get) => {
return {
accessToken: null,
setAccessToken(data) {
- set((state) => {
- if (state.accessToken?.access_token === data.access_token) {
- return { accessToken: data };
- }
- updatePermissions(data);
- return { accessToken: data };
- });
+ set({ accessToken: data });
},
authorizationHeader() {
const accessToken = get().accessToken;
@@ -114,8 +118,5 @@ export const useAuthState = create((set, get) => {
isAuthenticated() {
return !!get().authorizationHeader();
},
- getPermissions() {
- return permissionMap;
- },
};
});
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 88e9a5b4..c2f34e68 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -366,7 +366,7 @@
"stop": "Stop",
"subtitle": "Slow down there!",
"message": "You do not have permission to view this page. If you believe this is an error, ",
- "contact": "please contact your local webmaster.",
+ "contact": "please contact your local webmaster",
"quote": "You are not a subset of the set of all users with access to this page.",
"quote_author": "The wise Debianne server",
"button": "Back to homepage"
diff --git a/src/locales/sv/main.json b/src/locales/sv/main.json
index ad0443d9..6e548342 100644
--- a/src/locales/sv/main.json
+++ b/src/locales/sv/main.json
@@ -366,7 +366,7 @@
"stop": "Stopp",
"subtitle": "Sakta i backarna!",
"message": "Du har inte behörighet att se denna sida. Om du tror att något är fel, ",
- "contact": "kontakta din lokala spindelman.",
+ "contact": "kontakta din lokala spindelman",
"quote": "Här har vi begått lite småillegala saker längs vägen.",
"quote_author": "Mikael \"Micke P\" Persson Sundqvist",
"button": "Tillbaka till hemsidan"