diff --git a/packages/frontend/src/components/AuthBoundary.tsx b/packages/frontend/src/components/AuthBoundary.tsx new file mode 100644 index 0000000..6244d36 --- /dev/null +++ b/packages/frontend/src/components/AuthBoundary.tsx @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { useAuth } from '../stores/authStore.js'; + +export function AuthBoundary() { + const { user, ready, hydrate } = useAuth(); + const location = useLocation(); + + useEffect(() => { + if (!ready) hydrate(); + }, [ready, hydrate]); + + if (!ready) { + return ( +
+
+
+ ); + } + if (!user) { + const next = encodeURIComponent(location.pathname + location.search); + return ; + } + return ; +} diff --git a/packages/frontend/src/components/Layout.tsx b/packages/frontend/src/components/Layout.tsx index 48a3af9..ed01196 100644 --- a/packages/frontend/src/components/Layout.tsx +++ b/packages/frontend/src/components/Layout.tsx @@ -1,14 +1,17 @@ import { NavLink, Outlet } from 'react-router-dom'; import { useSettings } from '../stores/settingsStore.js'; +import { useAuth } from '../stores/authStore.js'; +import { UserMenu } from './UserMenu.js'; const navItems = [ { to: '/', label: 'Dashboard', end: true }, - { to: '/admin', label: 'Admin' }, + { to: '/admin', label: 'Lessen' }, { to: '/stats', label: 'Stats' }, ]; export function Layout() { const { theme, toggleTheme } = useSettings(); + const user = useAuth((s) => s.user); return (
@@ -17,37 +20,27 @@ export function Layout() { Flashcards - + {user && ( + + )}
- - `rounded-xl px-3 py-1.5 text-sm font-medium transition ${ - isActive - ? 'bg-brand-100 text-brand-700 dark:bg-brand-900/40 dark:text-brand-200' - : 'text-slate-600 hover:bg-white/70 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-900/60' - }` - } - > - Instellingen - +
- + {user && ( + + )}
diff --git a/packages/frontend/src/components/RoleGuard.tsx b/packages/frontend/src/components/RoleGuard.tsx new file mode 100644 index 0000000..dc1ef1a --- /dev/null +++ b/packages/frontend/src/components/RoleGuard.tsx @@ -0,0 +1,17 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import type { Role } from '@flashcard/shared'; +import { useAuth } from '../stores/authStore.js'; + +export function RoleGuard({ role }: { role: Role }) { + const user = useAuth((s) => s.user); + if (!user) return ; + if (user.role !== role) { + return ( +
+

Geen toegang

+

Deze pagina is alleen voor beheerders.

+
+ ); + } + return ; +} diff --git a/packages/frontend/src/components/UserMenu.tsx b/packages/frontend/src/components/UserMenu.tsx new file mode 100644 index 0000000..ea64030 --- /dev/null +++ b/packages/frontend/src/components/UserMenu.tsx @@ -0,0 +1,55 @@ +import { useState, useRef, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../stores/authStore.js'; + +export function UserMenu() { + const { user, logout } = useAuth(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + function onClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener('mousedown', onClick); + return () => document.removeEventListener('mousedown', onClick); + }, []); + + if (!user) return null; + + const initials = user.displayName.trim().split(/\s+/).map((p) => p[0]).slice(0, 2).join('').toUpperCase(); + + async function handleLogout() { + await logout(); + navigate('/login'); + } + + return ( +
+ + {open && ( +
+
+
{user.displayName}
+
{user.email}
+
+
+ setOpen(false)}>Profiel + {user.role === 'sysadmin' && ( + setOpen(false)}>👑 Systeembeheer + )} + +
+ )} +
+ ); +} diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index 853e39f..7f4e8cc 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -3,10 +3,16 @@ import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { router } from './router.js'; import { useSettings } from './stores/settingsStore.js'; +import { useAuth } from './stores/authStore.js'; +import { onUnauthorized } from './api/client.js'; import './styles.css'; useSettings.getState().hydrate(); +onUnauthorized(() => { + useAuth.setState({ user: null, ready: true }); +}); + const root = createRoot(document.getElementById('root')!); root.render(