Add CLI flags for setup and server configuration
- Add --setup flag to create admin user - Add --admin-user and --admin-password for setup - Generate cryptographically random passwords for --setup - Add --bind and --port flags for server binding - Add BIND env var support - Add SMTP email service for password reset - Add password reset token storage - Add auth header to queue/logs frontend pages - Fix journalctl timestamp format - Fix logs to return empty array instead of error when no entries - Fix queue handler to return proper error message - Hide Users/Aliases nav links when no domain selected
This commit is contained in:
parent
2c3a4c3daa
commit
53ce3b06ae
14 changed files with 435 additions and 69 deletions
|
|
@ -7,25 +7,35 @@
|
|||
let user: { username?: string; email?: string; role?: string } | null = $state(null);
|
||||
let selectedDomain = $state<string | null>(null);
|
||||
let loading = $state(true);
|
||||
let showDropdown = $state(false);
|
||||
let dropdownRef = $state<HTMLDivElement | null>(null);
|
||||
|
||||
let usersLink = $derived(selectedDomain ? `/domains/${selectedDomain}/users` : '/domains');
|
||||
let aliasesLink = $derived(selectedDomain ? `/domains/${selectedDomain}/aliases` : '/domains');
|
||||
let isInitialized = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
selectedDomain = localStorage.getItem('selectedDomain');
|
||||
updateSelectedDomainFromPath($page.url.pathname);
|
||||
isInitialized = true;
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||
showDropdown = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
function updateSelectedDomainFromPath(path: string) {
|
||||
$effect(() => {
|
||||
if (!isInitialized) return;
|
||||
const path = $page.url.pathname;
|
||||
const match = path.match(/^\/domains\/([^/]+)/);
|
||||
if (match) {
|
||||
selectedDomain = match[1];
|
||||
localStorage.setItem('selectedDomain', selectedDomain);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updateSelectedDomainFromPath($page.url.pathname);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -73,8 +83,14 @@
|
|||
localStorage.removeItem('selectedDomain');
|
||||
user = null;
|
||||
selectedDomain = null;
|
||||
showDropdown = false;
|
||||
goto('/auth/login');
|
||||
}
|
||||
|
||||
function toggleDropdown(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
showDropdown = !showDropdown;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
|
|
@ -86,17 +102,24 @@
|
|||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/domains">Domains</a>
|
||||
<a href={usersLink}>Users</a>
|
||||
<a href={aliasesLink}>Aliases</a>
|
||||
{#if selectedDomain}
|
||||
<a href={usersLink}>Users</a>
|
||||
<a href={aliasesLink}>Aliases</a>
|
||||
{/if}
|
||||
<a href="/queue">Queue</a>
|
||||
<a href="/logs">Logs</a>
|
||||
</nav>
|
||||
<div class="user-menu">
|
||||
{#if user}
|
||||
<span>{user.username || user.email}</span>
|
||||
<div class="user-menu" bind:this={dropdownRef}>
|
||||
<button class="dropdown-trigger" onclick={toggleDropdown}>
|
||||
<span class="username">{user.username || user.email}</span>
|
||||
<span class="arrow" class:open={showDropdown}>▼</span>
|
||||
</button>
|
||||
{#if showDropdown}
|
||||
<div class="dropdown">
|
||||
<a href="/auth/change-password" onclick={() => showDropdown = false}>Change Password</a>
|
||||
<button class="logout-btn" onclick={logout}>Logout</button>
|
||||
</div>
|
||||
{/if}
|
||||
<a href="/auth/change-password">Change Password</a>
|
||||
<button onclick={logout}>Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -169,40 +192,80 @@
|
|||
}
|
||||
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-menu span {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.user-menu a {
|
||||
color: #ecf0f1;
|
||||
text-decoration: none;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #7f8c8d;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.user-menu a:hover {
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
.user-menu button {
|
||||
gap: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid #7f8c8d;
|
||||
color: #ecf0f1;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-menu button:hover {
|
||||
.dropdown-trigger:hover {
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 0.7rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.arrow.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdown a {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dropdown a:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.dropdown .logout-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #e74c3c;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dropdown .logout-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@
|
|||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch(`/api/logs?hours=${hours}&filter=${encodeURIComponent(filter)}`);
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(`/api/logs?hours=${hours}&filter=${encodeURIComponent(filter)}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
logs = data.data || [];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch('/api/queue');
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch('/api/queue', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
queue = data.data || [];
|
||||
|
|
@ -33,7 +36,11 @@
|
|||
|
||||
async function requeue(id: string) {
|
||||
try {
|
||||
await fetch(`/api/queue/${id}/requeue`, { method: 'POST' });
|
||||
const token = localStorage.getItem('token');
|
||||
await fetch(`/api/queue/${id}/requeue`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
await loadQueue();
|
||||
} catch (e) {
|
||||
console.error('Failed to requeue:', e);
|
||||
|
|
@ -43,7 +50,11 @@
|
|||
async function deleteFromQueue(id: string) {
|
||||
if (!confirm('Delete this message from queue?')) return;
|
||||
try {
|
||||
await fetch(`/api/queue/${id}`, { method: 'DELETE' });
|
||||
const token = localStorage.getItem('token');
|
||||
await fetch(`/api/queue/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
await loadQueue();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete from queue:', e);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue