The header nav now shows a Users link only for owners/admins, and the username block becomes a profile-link chip rendering the role pill next to the name. Both react to the currentUser store, so they update on login without an extra fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
176 lines
3.3 KiB
Svelte
176 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { base } from '$app/paths';
|
|
import { goto } from '$app/navigation';
|
|
import { onMount } from 'svelte';
|
|
import { page } from '$app/state';
|
|
import { api } from '$lib/api';
|
|
import { currentUser, getToken } from '$lib/auth';
|
|
import RoleChip from '$lib/RoleChip.svelte';
|
|
|
|
let { children } = $props();
|
|
|
|
let booting = $state(true);
|
|
const user = $derived($currentUser);
|
|
|
|
const isLoginRoute = $derived(page.url.pathname.endsWith('/login'));
|
|
|
|
onMount(async () => {
|
|
// Hydrate the session: if there's a token, ask the server who we
|
|
// are. On 401 the fetch wrapper already redirects to /login and
|
|
// clears state; on success we land in the SPA fully signed in.
|
|
const tok = getToken();
|
|
if (!tok) {
|
|
if (!isLoginRoute) {
|
|
await goto(`${base}/login`);
|
|
}
|
|
booting = false;
|
|
return;
|
|
}
|
|
try {
|
|
const me = await api.auth.me();
|
|
currentUser.set(me);
|
|
} catch {
|
|
// adminRequest handles 401 redirects. For other errors fall
|
|
// through — the page will surface its own error state.
|
|
}
|
|
booting = false;
|
|
});
|
|
|
|
async function handleLogout() {
|
|
await api.auth.logout();
|
|
await goto(`${base}/login`);
|
|
}
|
|
</script>
|
|
|
|
<div class="shell">
|
|
<header>
|
|
<a href={base + '/'} class="brand">PiCloud</a>
|
|
<nav>
|
|
<a href={base + '/apps'}>Apps</a>
|
|
{#if user && user.instance_role !== 'member'}
|
|
<a href={base + '/users'}>Users</a>
|
|
{/if}
|
|
</nav>
|
|
<div class="spacer"></div>
|
|
{#if user}
|
|
<div class="usermenu">
|
|
<a href={base + '/profile'} class="profile-chip" title="View profile">
|
|
<RoleChip role={user.instance_role} size="sm" />
|
|
<span class="username">{user.username}</span>
|
|
</a>
|
|
<button type="button" class="logout" onclick={handleLogout}>Logout</button>
|
|
</div>
|
|
{/if}
|
|
</header>
|
|
<main>
|
|
{#if booting}
|
|
<p class="boot">Loading…</p>
|
|
{:else}
|
|
{@render children?.()}
|
|
{/if}
|
|
</main>
|
|
</div>
|
|
|
|
<style>
|
|
:global(html, body) {
|
|
margin: 0;
|
|
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
background: #0f172a;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.shell {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2rem;
|
|
padding: 1rem 2rem;
|
|
border-bottom: 1px solid #1e293b;
|
|
background: #0b1220;
|
|
}
|
|
|
|
.brand {
|
|
font-weight: 700;
|
|
font-size: 1.125rem;
|
|
color: #38bdf8;
|
|
text-decoration: none;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
nav a {
|
|
color: #94a3b8;
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
nav a:hover {
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.usermenu {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.profile-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.25rem 0.55rem;
|
|
border-radius: 0.375rem;
|
|
text-decoration: none;
|
|
border: 1px solid transparent;
|
|
}
|
|
.profile-chip:hover {
|
|
background: #1e293b;
|
|
border-color: #334155;
|
|
}
|
|
|
|
.username {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.logout {
|
|
background: transparent;
|
|
color: #94a3b8;
|
|
border: 1px solid #334155;
|
|
padding: 0.35rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.logout:hover {
|
|
background: #1e293b;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
main {
|
|
flex: 1;
|
|
padding: 2rem;
|
|
max-width: 64rem;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.boot {
|
|
color: #64748b;
|
|
}
|
|
</style>
|