From 36da7ba7bf27741b1a0b8579f4ad86d32b8f59e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veikko=20Lintuj=C3=A4rvi?= Date: Wed, 11 Mar 2026 19:54:41 +0200 Subject: [PATCH] Role based home page rendering --- api/Database/klapi.db | Bin 53248 -> 53248 bytes ui/src/app.tsx | 3 ++- ui/src/auth/roles.ts | 24 ++++++++++++++++++++++++ ui/src/components/Nav.tsx | 3 ++- ui/src/components/RoleGuard.tsx | 27 +++++++++++++++++++++++++++ ui/src/routes/index.tsx | 6 +++++- 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 ui/src/auth/roles.ts create mode 100644 ui/src/components/RoleGuard.tsx diff --git a/api/Database/klapi.db b/api/Database/klapi.db index 042dbba99f8047c63aefe88cf1fd0791ada054c5..322d07f7fb296774c11614b882bfdbcefb14c5c2 100644 GIT binary patch delta 49 zcmZozz}&Ead4e>f-9#B@M!Ss(^Yt0IHW@sS7cvYnG`BJ}u`)2$Gc+?cvM@E+{K=oM F0RV9$4ub#y delta 49 zcmZozz}&Ead4e>f)kGO*Myrhp^Yt0oHW@sS7cvMjGPE+Xure~zGchwVu&^-N{K=oM F0RV4M4tM|n 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")}

{t("home.logoAlt")} - + + + ); }