remove build artifacts that should not be checked in

This commit is contained in:
Christoph Haas 2026-03-21 23:56:39 +01:00
parent f4be03ceba
commit 3cb21bf5cf
102 changed files with 461 additions and 424 deletions

View file

@ -1,11 +1,33 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
let { children } = $props();
let user: { username?: string; email?: string; role?: string } | null = $state(null);
let selectedDomain = $state<string | null>(null);
let loading = $state(true);
let usersLink = $derived(selectedDomain ? `/domains/${selectedDomain}/users` : '/domains');
let aliasesLink = $derived(selectedDomain ? `/domains/${selectedDomain}/aliases` : '/domains');
onMount(() => {
selectedDomain = localStorage.getItem('selectedDomain');
updateSelectedDomainFromPath($page.url.pathname);
});
function updateSelectedDomainFromPath(path: string) {
const match = path.match(/^\/domains\/([^/]+)/);
if (match) {
selectedDomain = match[1];
localStorage.setItem('selectedDomain', selectedDomain);
}
}
$effect(() => {
updateSelectedDomainFromPath($page.url.pathname);
});
$effect(() => {
const path = $page.url.pathname;
if (!path.startsWith('/auth/')) {
@ -48,7 +70,9 @@
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('selectedDomain');
user = null;
selectedDomain = null;
goto('/auth/login');
}
</script>
@ -62,8 +86,8 @@
<nav>
<a href="/">Dashboard</a>
<a href="/domains">Domains</a>
<a href="/users">Users</a>
<a href="/aliases">Aliases</a>
<a href={usersLink}>Users</a>
<a href={aliasesLink}>Aliases</a>
<a href="/queue">Queue</a>
<a href="/logs">Logs</a>
</nav>

View file

@ -8,10 +8,6 @@
queueSize: number;
}
interface ApiResponse {
data: Stats;
}
let stats = $state<Stats>({
totalDomains: 0,
totalUsers: 0,
@ -20,12 +16,19 @@
});
let loading = $state(true);
async function fetchWithAuth(url: string) {
const token = localStorage.getItem('token');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }
});
return res.json();
}
onMount(async () => {
try {
const res = await fetch('/api/stats');
if (res.ok) {
const json: ApiResponse = await res.json();
stats = json.data;
const statsRes = await fetchWithAuth('/api/stats');
if (statsRes.data) {
stats = statsRes.data;
}
} catch (e) {
console.error('Failed to fetch stats:', e);

View file

@ -6,20 +6,42 @@
aliasCount: number;
}
interface UserInfo {
id: number;
username: string;
role: string;
domains: string[];
}
let domains = $state<Domain[]>([]);
let user = $state<UserInfo | null>(null);
let loading = $state(true);
let showModal = $state(false);
let newDomain = $state({ name: '' });
async function loadDomains() {
async function fetchWithAuth(url: string) {
const token = localStorage.getItem('token');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }
});
return res.json();
}
async function loadData() {
try {
const res = await fetch('/api/domains');
if (res.ok) {
const data = await res.json();
domains = data.data || [];
const [domainsRes, userRes] = await Promise.all([
fetchWithAuth('/api/domains'),
fetchWithAuth('/api/auth/me')
]);
if (domainsRes.data) {
domains = domainsRes.data || [];
}
if (userRes.data) {
user = userRes.data;
}
} catch (e) {
console.error('Failed to load domains:', e);
console.error('Failed to load data:', e);
} finally {
loading = false;
}
@ -29,13 +51,13 @@
try {
const res = await fetch('/api/domains', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
body: JSON.stringify(newDomain)
});
if (res.ok) {
showModal = false;
newDomain = { name: '' };
await loadDomains();
await loadData();
}
} catch (e) {
console.error('Failed to create domain:', e);
@ -45,26 +67,48 @@
async function deleteDomain(id: number) {
if (!confirm('Delete this domain? Users and aliases will be deleted.')) return;
try {
await fetch(`/api/domains/${id}`, { method: 'DELETE' });
await loadDomains();
await fetch(`/api/domains/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
await loadData();
} catch (e) {
console.error('Failed to delete domain:', e);
}
}
$effect(() => { loadDomains(); });
function isAdmin() {
return user?.role === 'admin';
}
$effect(() => { loadData(); });
</script>
<div class="header">
<h2>Domains</h2>
<button onclick={() => showModal = true}>Add Domain</button>
{#if isAdmin()}
<button onclick={() => showModal = true}>Add Domain</button>
{/if}
</div>
{#if user && !isAdmin() && user.domains.length > 0}
<h3>Your Domains</h3>
<div class="domains-grid">
{#each user.domains as domainName}
<a href="/domains/{domainName}/users" class="domain-card">
<span class="domain-name">{domainName}</span>
<span class="domain-links">Manage →</span>
</a>
{/each}
</div>
{/if}
{#if loading}
<p>Loading...</p>
{:else if domains.length === 0}
{:else if isAdmin() && domains.length === 0}
<p>No domains configured yet.</p>
{:else}
{:else if isAdmin()}
<h3>All Domains</h3>
<table>
<thead>
<tr>
@ -90,20 +134,19 @@
{/if}
{#if showModal}
<div class="modal-backdrop" onclick={() => showModal = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h3>Add Domain</h3>
<form onsubmit={(e) => { e.preventDefault(); createDomain(); }}>
<label>
Domain name:
<input type="text" bind:value={newDomain.name} placeholder="example.org" required />
</label>
<div class="actions">
<button type="button" onclick={() => showModal = false}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</div>
<button class="modal-backdrop" onclick={() => showModal = false} aria-label="Close modal"></button>
<div class="modal">
<h3>Add Domain</h3>
<form onsubmit={(e) => { e.preventDefault(); createDomain(); }}>
<label>
Domain name:
<input type="text" bind:value={newDomain.name} placeholder="example.org" required />
</label>
<div class="actions">
<button type="button" onclick={() => showModal = false}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</div>
{/if}
@ -115,12 +158,53 @@
margin-bottom: 1.5rem;
}
h3 {
color: #2c3e50;
margin-top: 1.5rem;
}
.domains-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.domain-card {
background: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
text-decoration: none;
color: inherit;
transition: transform 0.2s, box-shadow 0.2s;
}
.domain-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.domain-name {
font-weight: 600;
color: #2c3e50;
}
.domain-links {
color: #3498db;
font-size: 0.9rem;
}
table {
width: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 1rem;
}
th, td {
@ -151,17 +235,25 @@
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
z-index: 100;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
border-radius: 8px;
min-width: 400px;
z-index: 101;
}
.modal h3 {
margin-top: 0;
}
.modal form {

View file

@ -18,6 +18,9 @@
if (res.ok) {
const data = await res.json();
domain = data.data;
if (domain) {
localStorage.setItem('selectedDomain', domain.name);
}
} else {
error = 'Domain not found';
}

View file

@ -15,7 +15,10 @@
async function loadAliases(name: string) {
try {
const res = await fetch(`/api/domains/${encodeURIComponent(name)}/aliases`);
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(name)}/aliases`, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
aliases = data.data || [];
@ -29,9 +32,13 @@
async function createAlias() {
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(domainName)}/aliases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(newAlias)
});
if (res.ok) {
@ -50,7 +57,11 @@
async function deleteAlias(id: number) {
if (!confirm('Delete this alias?')) return;
try {
await fetch(`/api/domains/${encodeURIComponent(domainName)}/aliases/${id}`, { method: 'DELETE' });
const token = localStorage.getItem('token');
await fetch(`/api/domains/${encodeURIComponent(domainName)}/aliases/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
});
await loadAliases(domainName);
} catch (e) {
console.error('Failed to delete alias:', e);

View file

@ -17,7 +17,10 @@
async function loadUsers(name: string) {
try {
const res = await fetch(`/api/domains/${encodeURIComponent(name)}/users`);
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(name)}/users`, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
users = data.data || [];
@ -31,9 +34,13 @@
async function createUser() {
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(domainName)}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(newUser)
});
if (res.ok) {
@ -52,7 +59,11 @@
async function deleteUser(id: number) {
if (!confirm('Delete this user? Mailbox will NOT be deleted.')) return;
try {
await fetch(`/api/domains/${encodeURIComponent(domainName)}/users/${id}`, { method: 'DELETE' });
const token = localStorage.getItem('token');
await fetch(`/api/domains/${encodeURIComponent(domainName)}/users/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
});
await loadUsers(domainName);
} catch (e) {
console.error('Failed to delete user:', e);