imc-vibe/frontend/src/routes/+layout.svelte
Christoph Haas 629c66cc34 Fix layout rendering for /auth/change-password page
The change-password page was showing both the main layout sidebar
and its own header. Now only auth-only routes (login, forgot)
skip the main layout.
2026-03-23 22:17:32 +01:00

173 lines
7.2 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { decodeJWT, isTokenValid } from '$lib/auth';
import '../app.css';
let { children } = $props();
let user: { userId?: number; username?: string; role?: string } | null = $state(null);
let selectedDomain = $state<string | null>(null);
let loading = $state(true);
let isInitialized = $state(false);
let usersLink = $derived(selectedDomain ? `/domains/${selectedDomain}/users` : '/domains');
let aliasesLink = $derived(selectedDomain ? `/domains/${selectedDomain}/aliases` : '/domains');
onMount(() => {
selectedDomain = localStorage.getItem('selectedDomain');
isInitialized = true;
});
$effect(() => {
if (!isInitialized) return;
const path = $page.url.pathname;
const match = path.match(/^\/domains\/([^/]+)/);
if (match) {
selectedDomain = match[1];
localStorage.setItem('selectedDomain', selectedDomain);
}
});
$effect(() => {
const path = $page.url.pathname;
// Auth-only routes (login, forgot) - no auth required, no layout chrome
// /auth/change-password requires auth, so treat it like a regular route
const isAuthOnlyRoute = path === '/auth/login' || path === '/auth/forgot';
if (isAuthOnlyRoute) {
loading = false;
} else {
checkAuth();
}
});
function checkAuth() {
const token = localStorage.getItem('token');
if (!token || !isTokenValid(token)) {
goto('/auth/login');
return;
}
const payload = decodeJWT(token);
if (!payload) {
goto('/auth/login');
return;
}
user = {
userId: payload.userId,
username: payload.username,
role: payload.role
};
loading = false;
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('selectedDomain');
user = null;
selectedDomain = null;
goto('/auth/login');
}
</script>
<div class="min-h-screen bg-base-200">
{#if loading}
<div class="flex items-center justify-center min-h-screen">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if user?.role === 'admin'}
<div class="drawer lg:drawer-open">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-h-screen">
<header class="navbar bg-base-100 shadow-sm">
<div class="flex-none lg:hidden">
<label for="nav-drawer" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
</label>
</div>
<div class="flex-1">
<a href="/" class="btn btn-ghost text-xl">IMC Vibe</a>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<div class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-10 rounded-full">
<span class="text-xl">{user.username?.[0]?.toUpperCase() || 'U'}</span>
</div>
</div>
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul tabindex="0" class="menu dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li class="menu-title px-4 py-2">
<span class="text-sm opacity-60">{user.username}</span>
</li>
<li><a href="/auth/change-password">Change Password</a></li>
<li><button type="button" onclick={logout} class="btn btn-ghost text-error w-full justify-start">Logout</button></li>
</ul>
</div>
</div>
</header>
<main class="p-6">
{@render children()}
</main>
</div>
<div class="drawer-side z-40">
<label for="nav-drawer" class="drawer-overlay"></label>
<ul class="menu p-4 w-64 min-h-full bg-base-200 text-base-content">
<li class="menu-title">
<span class="text-sm font-semibold uppercase opacity-60">Navigation</span>
</li>
<li><a href="/" class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Dashboard
</a></li>
<li><a href="/domains" class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Domains
</a></li>
{#if selectedDomain}
<li><a href={usersLink} class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Users
</a></li>
<li><a href={aliasesLink} class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Aliases
</a></li>
{/if}
<li><a href="/queue" class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Mail Queue
</a></li>
<li><a href="/logs" class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Logs
</a></li>
</ul>
</div>
</div>
{:else}
{@render children()}
{/if}
</div>