Role based home page rendering
This commit is contained in:
Binary file not shown.
@@ -8,6 +8,7 @@ import Login from "~/routes/login";
|
|||||||
import Management from "~/routes/management";
|
import Management from "~/routes/management";
|
||||||
import NotFound from "~/routes/[...404]";
|
import NotFound from "~/routes/[...404]";
|
||||||
import Toasts from "~/components/Toasts";
|
import Toasts from "~/components/Toasts";
|
||||||
|
import { APP_ROLES, hasAnyRole } from "~/auth/roles";
|
||||||
import { initializeLanguage, useLanguage } from "~/i18n";
|
import { initializeLanguage, useLanguage } from "~/i18n";
|
||||||
import { sessionAtom } from "~/state/appState";
|
import { sessionAtom } from "~/state/appState";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
@@ -52,7 +53,7 @@ function AppShell() {
|
|||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route
|
<Route
|
||||||
path="/management"
|
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="*" element={<NotFound />} />
|
||||||
<Route path="/index.html" element={<Navigate to="/" replace />} />
|
<Route path="/index.html" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
24
ui/src/auth/roles.ts
Normal file
24
ui/src/auth/roles.ts
Normal 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));
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
|
import { APP_ROLES, hasAnyRole } from "~/auth/roles";
|
||||||
import { sessionAtom } from "~/state/appState";
|
import { sessionAtom } from "~/state/appState";
|
||||||
import { useLanguage, useT } from "~/i18n";
|
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">
|
<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="/" className={linkClass("/")}>{t("nav.home")}</Link>
|
||||||
<Link to="/about" className={linkClass("/about")}>{t("nav.about")}</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>
|
<Link to="/management" className={linkClass("/management")}>{t("nav.management")}</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
27
ui/src/components/RoleGuard.tsx
Normal file
27
ui/src/components/RoleGuard.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import OpenHoursForm from "~/components/OpenHoursForm";
|
import OpenHoursForm from "~/components/OpenHoursForm";
|
||||||
|
import RoleGuard from "~/components/RoleGuard";
|
||||||
|
import { APP_ROLES } from "~/auth/roles";
|
||||||
import { useT } from "~/i18n";
|
import { useT } from "~/i18n";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -14,7 +16,9 @@ export default function Home() {
|
|||||||
<h1 className="text-center">{t("home.heading")}</h1>
|
<h1 className="text-center">{t("home.heading")}</h1>
|
||||||
<h2 className="text-center">{t("home.subheading")}</h2>
|
<h2 className="text-center">{t("home.subheading")}</h2>
|
||||||
<img src="/favicon.svg" alt={t("home.logoAlt")} className="w-28" />
|
<img src="/favicon.svg" alt={t("home.logoAlt")} className="w-28" />
|
||||||
|
<RoleGuard roles={[APP_ROLES.lok]}>
|
||||||
<OpenHoursForm />
|
<OpenHoursForm />
|
||||||
|
</RoleGuard>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user