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.
173 lines
7.2 KiB
Svelte
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>
|