remove build artifacts that should not be checked in
This commit is contained in:
parent
f4be03ceba
commit
3cb21bf5cf
102 changed files with 461 additions and 424 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue