diff --git a/GUI/src/Layout.vue b/GUI/src/Layout.vue index 47808cf..15c4851 100644 --- a/GUI/src/Layout.vue +++ b/GUI/src/Layout.vue @@ -5,6 +5,12 @@ import { useRoute, useRouter } from 'vue-router' import iconImage from '@/assets/images/icon.svg' import { Visibility, routes } from '@/plugins/routesLayout' +import { + AuthRequestError, + fetchCurrentUser, + logout, + type CurrentUser, +} from '@/services/authSession' const display = useDisplay() const theme = useTheme() @@ -13,6 +19,9 @@ const router = useRouter() const showDrawer = ref(true) const currentYear = new Date().getFullYear() const themeStorageKey = 'theme' +const currentUser = ref(null) +const isLoggingOut = ref(false) +const authMessage = ref('') function normalizeRoutePath(path: string) { if (!path || path === '/') { @@ -55,6 +64,14 @@ const sidebarRoutes = computed(() => return true } + if (item.visible === Visibility.Authenticated) { + return currentUser.value !== null + } + + if (item.visible === Visibility.Unauthenticated) { + return currentUser.value === null + } + if (item.visible !== Visibility.Route) { return false } @@ -64,6 +81,9 @@ const sidebarRoutes = computed(() => ) }), ) +const firstAuthenticatedSidebarIndex = computed(() => + sidebarRoutes.value.findIndex((item) => item.visible === Visibility.Authenticated), +) const footerRoutes = computed(() => routes.filter((x) => x.visible === Visibility.Footer)) const activeRoute = computed(() => { @@ -85,6 +105,16 @@ const themeIcon = computed(() => (isDarkTheme.value ? 'mdi-white-balance-sunny' const themeLabel = computed(() => isDarkTheme.value ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren', ) +const isAuthenticated = computed(() => currentUser.value !== null) +const userLabel = computed(() => currentUser.value?.userName ?? 'Konto') +const showAuthMessage = computed({ + get: () => authMessage.value.length > 0, + set: (nextValue: boolean) => { + if (!nextValue) { + authMessage.value = '' + } + }, +}) function applyTheme(nextTheme: 'light' | 'dark', persist = true) { theme.global.name.value = nextTheme @@ -103,6 +133,38 @@ function toggleTheme() { applyTheme(isDarkTheme.value ? 'light' : 'dark') } +async function refreshAuthState(options: { force?: boolean } = {}) { + try { + currentUser.value = await fetchCurrentUser({ force: options.force === true }) + } catch { + currentUser.value = null + } +} + +async function handleLogout() { + if (isLoggingOut.value) { + return + } + + isLoggingOut.value = true + authMessage.value = '' + + try { + await logout() + currentUser.value = null + await router.replace({ name: 'Login' }) + } catch (error) { + if (error instanceof AuthRequestError) { + authMessage.value = error.message + } else { + authMessage.value = 'Abmeldung fehlgeschlagen. Bitte versuche es erneut.' + } + } finally { + isLoggingOut.value = false + void refreshAuthState() + } +} + function changeWebsiteTitle() { if (activeRoute.value) { document.title = `Hoard | ${activeRoute.value.name}` @@ -119,12 +181,14 @@ onMounted(() => { const storedDrawer = localStorage.getItem('drawer') showDrawer.value = storedDrawer ? storedDrawer.startsWith('Y') : !display.mobile.value + void refreshAuthState({ force: true }) }) watch( () => route.fullPath, () => { changeWebsiteTitle() + void refreshAuthState() if (display.mobile.value) { showDrawer.value = false } @@ -177,23 +241,83 @@ watch( {{ themeLabel }} - - - Zum Login - + + + @@ -211,22 +335,27 @@ watch( - + @@ -323,7 +461,17 @@ watch( .topbar-actions { display: inline-flex; align-items: center; - gap: 0; + gap: var(--space-2); +} + +.account-button { + max-width: 220px; +} + +:deep(.account-button .v-btn__content) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .page-name, diff --git a/GUI/src/plugins/routesLayout.ts b/GUI/src/plugins/routesLayout.ts index c81f39b..fb9893e 100644 --- a/GUI/src/plugins/routesLayout.ts +++ b/GUI/src/plugins/routesLayout.ts @@ -1,6 +1,7 @@ import type { RouteRecordRaw } from 'vue-router' import Home from '@/routes/Home.vue' +import Dashboard from '@/routes/dashboard/Dashboard.vue' import NotFound from '@/routes/404NotFound.vue' import Login from '@/routes/authentication/Login.vue' import Impressum from '@/routes/Impressum.vue' @@ -38,17 +39,32 @@ export interface LayoutRoute { */ export const routes: LayoutRoute[] = [ { - path: '/', + path: '/welcome', name: 'Startseite', description: 'Self-hosted Datei-Workspace für Hoard', icon: 'mdi-home', - visible: Visibility.Public, + visible: Visibility.Unauthenticated, meta: { name: 'Home', - path: '/', + path: '/welcome', component: Home, }, }, + { + path: '/', + name: 'Dash', + description: 'Geschützter Bereich für dein Konto', + icon: 'mdi-view-dashboard-outline', + visible: Visibility.Authenticated, + meta: { + name: 'Dashboard', + path: '/', + component: Dashboard, + meta: { + requiresAuth: true, + }, + }, + }, { path: '/login', name: 'Login', @@ -59,6 +75,9 @@ export const routes: LayoutRoute[] = [ path: '/login', name: 'Login', component: Login, + meta: { + guestOnly: true, + }, }, }, { diff --git a/GUI/src/router/index.ts b/GUI/src/router/index.ts index f79212d..acc1519 100644 --- a/GUI/src/router/index.ts +++ b/GUI/src/router/index.ts @@ -1,9 +1,58 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { routes } from '@/plugins/routesLayout' +import { fetchCurrentUser } from '@/services/authSession' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: routes.filter((x) => x.meta !== undefined).map((x) => x.meta) as RouteRecordRaw[], }) +router.beforeEach(async (to) => { + const requiresAuth = to.meta.requiresAuth === true + const guestOnly = to.meta.guestOnly === true + + if (!requiresAuth && !guestOnly) { + return true + } + + let currentUser = null + + try { + currentUser = await fetchCurrentUser() + } catch { + if (requiresAuth) { + const query = to.fullPath !== '/' ? { redirect: to.fullPath } : {} + + return { + name: 'Login', + query, + replace: true, + } + } + + return true + } + + const isAuthenticated = currentUser !== null + + if (requiresAuth && !isAuthenticated) { + const query = to.fullPath !== '/' ? { redirect: to.fullPath } : {} + + return { + name: 'Login', + query, + replace: true, + } + } + + if (guestOnly && isAuthenticated) { + return { + name: 'Dashboard', + replace: true, + } + } + + return true +}) + export default router diff --git a/GUI/src/routes/404NotFound.vue b/GUI/src/routes/404NotFound.vue index 2b34d08..2e8b1c6 100644 --- a/GUI/src/routes/404NotFound.vue +++ b/GUI/src/routes/404NotFound.vue @@ -11,7 +11,7 @@ function navigateBack() { return } - router.push('/') + router.push('/welcome') } @@ -33,7 +33,7 @@ function navigateBack() {

- Zur Startseite + Zur Startseite Zurück
diff --git a/GUI/src/routes/Impressum.vue b/GUI/src/routes/Impressum.vue index 2a72e7e..f9ffdc3 100644 --- a/GUI/src/routes/Impressum.vue +++ b/GUI/src/routes/Impressum.vue @@ -55,7 +55,7 @@ const legalNotes = [
- Zur Startseite + Zur Startseite Zum Login
diff --git a/GUI/src/routes/authentication/Login.vue b/GUI/src/routes/authentication/Login.vue index 24a8b37..60831ce 100644 --- a/GUI/src/routes/authentication/Login.vue +++ b/GUI/src/routes/authentication/Login.vue @@ -1,7 +1,56 @@