Improve sidebar and show menu items to roles with permission

This commit is contained in:
Karl Ludwig Weise
2025-09-09 21:11:16 +02:00
parent ff315072e3
commit 2ffac44e14
10 changed files with 118 additions and 44 deletions
+9 -1
View File
@@ -1,5 +1,6 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
import type { UserRole } from "$lib/server/auth/authorization-service";
import type { JWTPayload } from "jose";
// for information about these interfaces
@@ -7,7 +8,14 @@ declare global {
namespace App {
// interface Error {}
interface Locals {
user?: JWTPayload & { userId: string; sessionId: string };
user?: JWTPayload & {
userId: string;
sessionId: string;
name: string;
email: string;
role: UserRole;
tenantId?: string | null;
};
}
// interface PageData {}
// interface PageState {}
@@ -11,50 +11,57 @@
import AbsencesIcon from "@lucide/svelte/icons/tree-palm";
import AgentsIcon from "@lucide/svelte/icons/user-star";
import { m } from "$i18n/messages";
import type { NavItem } from "..";
const items = [
const items: NavItem[] = [
{
title: m["nav.home"](),
url: ROUTES.DASHBOARD.MAIN,
isTenatOnly: false,
isTenantOnly: false,
icon: HomeIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"],
},
{
title: m["nav.tenants"](),
url: ROUTES.DASHBOARD.TENANTS,
isTenatOnly: false,
isTenantOnly: false,
icon: TenantsIcon,
roles: ["GLOBAL_ADMIN"],
},
{
title: m["nav.calendar"](),
url: ROUTES.DASHBOARD.CALENDAR,
isTenatOnly: true,
isTenantOnly: true,
icon: CalendarIcon,
roles: ["TENANT_ADMIN", "STAFF"],
},
{
title: m["nav.agents"](),
url: ROUTES.DASHBOARD.AGENTS,
isTenatOnly: true,
isTenantOnly: true,
icon: AgentsIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"],
},
{
title: m["nav.channels"](),
url: ROUTES.DASHBOARD.CHANNELS,
isTenatOnly: true,
isTenantOnly: true,
icon: ChannelsIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"],
},
{
title: m["nav.absences"](),
url: ROUTES.DASHBOARD.ABSENCES,
isTenatOnly: true,
isTenantOnly: true,
icon: AbsencesIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"],
},
];
</script>
<Sidebar.Group class="gap-1">
{#each items as item (item.title)}
{#if item.isTenatOnly === false || $auth.user?.tenantId}
{#if item.isTenantOnly === false || $auth.user?.tenantId}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isCurrentSection(item.url)} tooltipContent={item.title}>
{#snippet child({ props })}
@@ -9,49 +9,55 @@
import StaffIcon from "@lucide/svelte/icons/users";
import { auth } from "$lib/stores/auth";
import { m } from "$i18n/messages";
import type { NavItem } from "..";
const items = [
const items: NavItem[] = [
{
title: m["nav.staff"](),
url: ROUTES.DASHBOARD.STAFF,
isTenatOnly: true,
isTenantOnly: true,
icon: StaffIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN"],
},
{
title: m["nav.settings"](),
url: ROUTES.DASHBOARD.SETTINGS,
isTenatOnly: true,
isTenantOnly: true,
icon: SettingsIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN"],
},
{
title: m["nav.documentation"](),
url: "https://open-reception.org",
isTenatOnly: false,
isTenantOnly: false,
icon: DocsIcon,
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"],
},
];
</script>
<Sidebar.Group>
{#each items as item (item.title)}
{#if item.isTenatOnly === false || $auth.user?.tenantId}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isCurrentSection(item.url)} tooltipContent={item.title}>
{#snippet child({ props })}
<a
href={item.url}
{...props}
target={item.url.startsWith("http") ? "_blank" : undefined}
>
<item.icon />
<Text style="xs">{item.title}</Text>
{#if item.url.startsWith("http")}
<ExternalLinkIcon class="text-light ml-auto !size-3" />
{/if}
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{#if item.isTenantOnly === false || $auth.user?.tenantId}
{#if $auth.user && item.roles.includes($auth.user?.role)}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isCurrentSection(item.url)} tooltipContent={item.title}>
{#snippet child({ props })}
<a
href={item.url}
{...props}
target={item.url.startsWith("http") ? "_blank" : undefined}
>
<item.icon />
<Text style="xs">{item.title}</Text>
{#if item.url.startsWith("http")}
<ExternalLinkIcon class="text-light ml-auto !size-3" />
{/if}
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/if}
{/if}
{/each}
</Sidebar.Group>
@@ -1,3 +1,13 @@
import type { Component } from "svelte";
import SidebarLayout from "./root.svelte";
import type { UserRole } from "$lib/server/auth/authorization-service";
export { SidebarLayout };
export type NavItem = {
title: string;
url: string;
isTenantOnly?: boolean;
icon: Component;
roles: UserRole[];
};
+3 -2
View File
@@ -1,13 +1,14 @@
import type { UserRole } from "$lib/server/auth/authorization-service";
import { writable } from "svelte/store";
interface AuthState {
export interface AuthState {
isAuthenticated: boolean;
isRefreshing: boolean;
user?: {
id: string;
email: string;
name: string;
role: string;
role: UserRole;
// The currently selected tenant
tenantId?: string | null;
};
+6 -3
View File
@@ -1,14 +1,15 @@
import { browser } from "$app/environment";
import { getCookie } from "$lib/utils/cookies";
import { writable } from "svelte/store";
interface AuthState {
isOpen: boolean;
}
const LOCAL_STORAGE_KEY = "sidebar-is-open";
export const STORAGE_KEY = "sidebar-is-open";
function createSidebarStore() {
const storedValue = browser ? localStorage.getItem(LOCAL_STORAGE_KEY) : null;
const storedValue = browser ? getCookie(STORAGE_KEY) : null;
const store = writable<AuthState>({
isOpen: storedValue === "true" ? true : false,
});
@@ -16,7 +17,9 @@ function createSidebarStore() {
return {
...store,
setOpen: (isOpen: boolean) => {
localStorage.setItem(LOCAL_STORAGE_KEY, `${isOpen}`);
if (browser) {
document.cookie = `${STORAGE_KEY}=${isOpen}; path=/; max-age=604800`;
}
store.update((state) => ({ ...state, isOpen }));
},
};
+8
View File
@@ -0,0 +1,8 @@
export const getCookie = (name: string): string | undefined => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift();
}
return undefined;
};
@@ -0,0 +1,26 @@
import { auth, type AuthState } from "$lib/stores/auth";
import { sidebar, STORAGE_KEY } from "$lib/stores/sidebar";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async (event) => {
// Initialize sidebar state from cookies for ssr
const isOpen = event.cookies.get(STORAGE_KEY);
sidebar.setOpen(isOpen === "true" ? true : false);
// Initialize auth state from locals for ssr
let user: AuthState["user"] | null = null;
if (event.locals.user) {
user = {
id: event.locals.user.userId,
name: event.locals.user.name,
email: event.locals.user.email,
role: event.locals.user.role,
tenantId: event.locals.user.tenantId,
};
auth.setUser(user);
}
return {
user,
};
};
+8 -3
View File
@@ -1,13 +1,18 @@
<script lang="ts">
import { refreshSession } from "$lib/utils/session";
import { refreshSession, refreshUserData } from "$lib/utils/session";
import { onMount } from "svelte";
import type { LayoutProps } from "./$types";
import { auth } from "$lib/stores/auth";
let { children } = $props();
let { data, children }: LayoutProps = $props();
let intervalId: ReturnType<typeof setInterval> | null = null;
onMount(() => {
refreshSession();
if (data?.user) {
auth.setUser(data.user);
}
const unsubscribe = () => {
if (!intervalId) {
refreshSession();
+5 -5
View File
@@ -3,7 +3,6 @@ import { SessionService } from "$lib/server/auth/session-service";
import { AuthorizationService } from "$lib/server/auth/authorization-service";
import { getAccessToken } from "./utils/accessToken";
import { ROUTES } from "$lib/const/routes";
import { auth } from "$lib/stores/auth";
export const authGuard: Handle = async ({ event, resolve }) => {
const { url } = event;
@@ -26,13 +25,14 @@ export const authGuard: Handle = async ({ event, resolve }) => {
if (!sessionData) redirect(302, ROUTES.LOGIN);
// Set user in auth store for SSR
auth.setUser({
id: sessionData.user.id,
email: sessionData.user.email,
event.locals.user = {
userId: sessionData.user.id,
sessionId: sessionData.sessionId,
name: sessionData.user.name,
email: sessionData.user.email,
role: sessionData.user.role,
tenantId: sessionData.user.tenantId,
});
};
switch (true) {
case isDashboardRoute &&