mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-23 17:34:51 +02:00
Improve sidebar and show menu items to roles with permission
This commit is contained in:
Vendored
+9
-1
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user