Role based home page rendering

This commit is contained in:
2026-03-11 19:54:41 +02:00
parent e89d971f41
commit 36da7ba7bf
6 changed files with 60 additions and 3 deletions

View File

@@ -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() {
<Route path="/login" element={<Login />} />
<Route
path="/management"
element={session?.roles.includes("admin") ? <Management /> : <Navigate to="/" replace />}
element={hasAnyRole(session, [APP_ROLES.admin]) ? <Management /> : <Navigate to="/" replace />}
/>
<Route path="*" element={<NotFound />} />
<Route path="/index.html" element={<Navigate to="/" replace />} />

24
ui/src/auth/roles.ts Normal file
View File

@@ -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));
};

View File

@@ -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() {
<div className="order-2 sm:order-1 flex items-center justify-center sm:justify-start">
<Link to="/" className={linkClass("/")}>{t("nav.home")}</Link>
<Link to="/about" className={linkClass("/about")}>{t("nav.about")}</Link>
{session?.roles.includes("admin") ? (
{hasAnyRole(session, [APP_ROLES.admin]) ? (
<Link to="/management" className={linkClass("/management")}>{t("nav.management")}</Link>
) : null}
</div>

View File

@@ -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}</>;
}

View File

@@ -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() {
<h1 className="text-center">{t("home.heading")}</h1>
<h2 className="text-center">{t("home.subheading")}</h2>
<img src="/favicon.svg" alt={t("home.logoAlt")} className="w-28" />
<OpenHoursForm />
<RoleGuard roles={[APP_ROLES.lok]}>
<OpenHoursForm />
</RoleGuard>
</main>
);
}