diff --git a/src/app.d.ts b/src/app.d.ts index d48c210..11a7753 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -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 {} diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte index c5be0ea..dba3526 100644 --- a/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte @@ -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"], }, ]; {#each items as item (item.title)} - {#if item.isTenatOnly === false || $auth.user?.tenantId} + {#if item.isTenantOnly === false || $auth.user?.tenantId} {#snippet child({ props })} diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte index 3b4f34f..8296a7a 100644 --- a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte @@ -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"], }, ]; {#each items as item (item.title)} - {#if item.isTenatOnly === false || $auth.user?.tenantId} - - - {#snippet child({ props })} - - - {item.title} - {#if item.url.startsWith("http")} - - {/if} - - {/snippet} - - + {#if item.isTenantOnly === false || $auth.user?.tenantId} + {#if $auth.user && item.roles.includes($auth.user?.role)} + + + {#snippet child({ props })} + + + {item.title} + {#if item.url.startsWith("http")} + + {/if} + + {/snippet} + + + {/if} {/if} {/each} diff --git a/src/lib/components/layouts/sidebar-layout/index.ts b/src/lib/components/layouts/sidebar-layout/index.ts index d34dd9a..b3662f8 100644 --- a/src/lib/components/layouts/sidebar-layout/index.ts +++ b/src/lib/components/layouts/sidebar-layout/index.ts @@ -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[]; +}; diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts index 676ad7f..f5db593 100644 --- a/src/lib/stores/auth.ts +++ b/src/lib/stores/auth.ts @@ -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; }; diff --git a/src/lib/stores/sidebar.ts b/src/lib/stores/sidebar.ts index 2584bfa..393bd52 100644 --- a/src/lib/stores/sidebar.ts +++ b/src/lib/stores/sidebar.ts @@ -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({ 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 })); }, }; diff --git a/src/lib/utils/cookies.ts b/src/lib/utils/cookies.ts new file mode 100644 index 0000000..0fdeffd --- /dev/null +++ b/src/lib/utils/cookies.ts @@ -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; +}; diff --git a/src/routes/(pages)/dashboard/+layout.server.ts b/src/routes/(pages)/dashboard/+layout.server.ts new file mode 100644 index 0000000..e142848 --- /dev/null +++ b/src/routes/(pages)/dashboard/+layout.server.ts @@ -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, + }; +}; diff --git a/src/routes/(pages)/dashboard/+layout.svelte b/src/routes/(pages)/dashboard/+layout.svelte index 8872502..9cbacd0 100644 --- a/src/routes/(pages)/dashboard/+layout.svelte +++ b/src/routes/(pages)/dashboard/+layout.svelte @@ -1,13 +1,18 @@