diff --git a/api/Database/klapi.db b/api/Database/klapi.db
index 042dbba..322d07f 100644
Binary files a/api/Database/klapi.db and b/api/Database/klapi.db differ
diff --git a/ui/src/app.tsx b/ui/src/app.tsx
index 387aa9b..4dc392e 100644
--- a/ui/src/app.tsx
+++ b/ui/src/app.tsx
@@ -8,6 +8,7 @@ import Login from "~/routes/login";
import Management from "~/routes/management";
import NotFound from "~/routes/[...404]";
import Toasts from "~/components/Toasts";
+import { APP_ROLES, hasAnyRole } from "~/auth/roles";
import { initializeLanguage, useLanguage } from "~/i18n";
import { sessionAtom } from "~/state/appState";
import "./app.css";
@@ -52,7 +53,7 @@ function AppShell() {
} />
: }
+ element={hasAnyRole(session, [APP_ROLES.admin]) ? : }
/>
} />
} />
diff --git a/ui/src/auth/roles.ts b/ui/src/auth/roles.ts
new file mode 100644
index 0000000..ab7147d
--- /dev/null
+++ b/ui/src/auth/roles.ts
@@ -0,0 +1,24 @@
+import type { Session } from "~/state/appState";
+
+export const APP_ROLES = {
+ admin: "admin",
+ lok: "lok",
+} as const;
+
+export type AppRole = (typeof APP_ROLES)[keyof typeof APP_ROLES];
+
+export const hasAnyRole = (session: Session | null, roles: readonly AppRole[]): boolean => {
+ if (!session || roles.length === 0) {
+ return false;
+ }
+
+ return roles.some((role) => session.roles.includes(role));
+};
+
+export const hasAllRoles = (session: Session | null, roles: readonly AppRole[]): boolean => {
+ if (!session || roles.length === 0) {
+ return false;
+ }
+
+ return roles.every((role) => session.roles.includes(role));
+};
diff --git a/ui/src/components/Nav.tsx b/ui/src/components/Nav.tsx
index 7301532..1463606 100644
--- a/ui/src/components/Nav.tsx
+++ b/ui/src/components/Nav.tsx
@@ -1,5 +1,6 @@
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useRecoilState } from "recoil";
+import { APP_ROLES, hasAnyRole } from "~/auth/roles";
import { sessionAtom } from "~/state/appState";
import { useLanguage, useT } from "~/i18n";
@@ -32,7 +33,7 @@ export default function Nav() {
{t("nav.home")}
{t("nav.about")}
- {session?.roles.includes("admin") ? (
+ {hasAnyRole(session, [APP_ROLES.admin]) ? (
{t("nav.management")}
) : null}
diff --git a/ui/src/components/RoleGuard.tsx b/ui/src/components/RoleGuard.tsx
new file mode 100644
index 0000000..ffaff76
--- /dev/null
+++ b/ui/src/components/RoleGuard.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from "react";
+import { useRecoilValue } from "recoil";
+import { sessionAtom } from "~/state/appState";
+import type { AppRole } from "~/auth/roles";
+import { hasAllRoles, hasAnyRole } from "~/auth/roles";
+
+type RoleGuardProps = {
+ roles: readonly AppRole[];
+ match?: "any" | "all";
+ fallback?: ReactNode;
+ children: ReactNode;
+};
+
+export default function RoleGuard({
+ roles,
+ match = "any",
+ fallback = null,
+ children,
+}: RoleGuardProps) {
+ const session = useRecoilValue(sessionAtom);
+
+ const isAllowed = match === "all"
+ ? hasAllRoles(session, roles)
+ : hasAnyRole(session, roles);
+
+ return isAllowed ? <>{children}> : <>{fallback}>;
+}
diff --git a/ui/src/routes/index.tsx b/ui/src/routes/index.tsx
index ca371d7..e21f7ba 100644
--- a/ui/src/routes/index.tsx
+++ b/ui/src/routes/index.tsx
@@ -1,5 +1,7 @@
import { useEffect } from "react";
import OpenHoursForm from "~/components/OpenHoursForm";
+import RoleGuard from "~/components/RoleGuard";
+import { APP_ROLES } from "~/auth/roles";
import { useT } from "~/i18n";
export default function Home() {
@@ -14,7 +16,9 @@ export default function Home() {
{t("home.heading")}
{t("home.subheading")}
-
+
+
+
);
}