feat(auth): login, roles, user mgmt, setup wizard, and session guard
- New skald-setup crate: interactive first-run wizard that creates the admin user, prompts for encryption choice and password - Auth system: session-based login/logout with cookie, guard middleware - Roles API: CRUD for data-driven roles, seeded on first boot - Users management API: create, list, edit, delete users - Setup state API: check if first admin has been created - Frontend: login-page, setup-page, users-page, roles-page, profile-page components with corresponding CSS - Topbar: avatar dropdown with profile link and logout - Sidebar: nav entries for Users and Roles (admin only) - Page shell CSS: layout support for the new pages - build.sh: builds both skald and skald-setup binaries - run.sh: runs skald-setup before the server loop - CLAUDE.md: updated workspace layout and build/run docs
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { html } from 'lit';
|
||||
import { html, nothing } from 'lit';
|
||||
import { LightElement } from '../lib/base.js';
|
||||
|
||||
export class AppTopbar extends LightElement {
|
||||
static properties = {
|
||||
_theme: { state: true },
|
||||
_theme: { state: true },
|
||||
_copilotCollapsed: { state: true },
|
||||
_menuOpen: { state: true },
|
||||
_me: { state: true },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._theme = document.documentElement.getAttribute('data-bs-theme') ?? 'light';
|
||||
this._copilotCollapsed = false;
|
||||
this._menuOpen = false;
|
||||
this._me = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -18,6 +22,19 @@ export class AppTopbar extends LightElement {
|
||||
window.addEventListener('copilot-collapsed', (e) => {
|
||||
this._copilotCollapsed = e.detail.collapsed;
|
||||
});
|
||||
this._loadMe();
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this._menuOpen && !e.composedPath().includes(this)) {
|
||||
this._menuOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _loadMe() {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
if (res.ok) this._me = await res.json();
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
_toggleTheme() {
|
||||
@@ -27,6 +44,27 @@ export class AppTopbar extends LightElement {
|
||||
localStorage.setItem('theme', next);
|
||||
}
|
||||
|
||||
_toggleMenu(e) {
|
||||
e.stopPropagation();
|
||||
this._menuOpen = !this._menuOpen;
|
||||
}
|
||||
|
||||
_goProfile() {
|
||||
this._menuOpen = false;
|
||||
history.pushState({ page: 'profile' }, '', '#profile');
|
||||
window.dispatchEvent(new CustomEvent('llm-page-change', { detail: { page: 'profile' } }));
|
||||
}
|
||||
|
||||
_logout() {
|
||||
this._menuOpen = false;
|
||||
fetch('/api/auth/logout', { method: 'POST' }).then(() => window.location.reload());
|
||||
}
|
||||
|
||||
get _initial() {
|
||||
const name = this._me?.display_name || this._me?.username || '?';
|
||||
return name.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
render() {
|
||||
const isDark = this._theme === 'dark';
|
||||
return html`
|
||||
@@ -42,6 +80,25 @@ export class AppTopbar extends LightElement {
|
||||
@click=${() => this._toggleTheme()}>
|
||||
<i class="bi ${isDark ? 'bi-sun' : 'bi-moon-stars'}"></i>
|
||||
</button>
|
||||
<div class="topbar-profile-wrapper">
|
||||
<button class="topbar-avatar" title="Account" @click=${(e) => this._toggleMenu(e)}>
|
||||
${this._initial}
|
||||
</button>
|
||||
${this._menuOpen ? html`
|
||||
<div class="topbar-dropdown">
|
||||
<div class="topbar-dropdown-header">
|
||||
<div class="topbar-dropdown-name">${this._me?.display_name || this._me?.username || ''}</div>
|
||||
<div class="topbar-dropdown-sub">@${this._me?.username || ''}</div>
|
||||
</div>
|
||||
<button class="topbar-dropdown-item" @click=${() => this._goProfile()}>
|
||||
<i class="bi bi-person"></i> Profile
|
||||
</button>
|
||||
<button class="topbar-dropdown-item topbar-dropdown-logout" @click=${() => this._logout()}>
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</button>
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user