Complete daisyUI migration with CSS fixes
- Migrate all pages to daisyUI components - Add data-theme="light" to app.html for proper theme - Fix config to load .env from multiple locations - Update avatar to use avatar-placeholder pattern - Use @tailwindcss/vite plugin instead of PostCSS
This commit is contained in:
parent
70aa813c7f
commit
0217f0b4c4
14 changed files with 739 additions and 1564 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
|
|
|
|||
|
|
@ -3,30 +3,20 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { decodeJWT, isTokenValid } from '$lib/auth';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
let user: { userId?: number; username?: 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 isInitialized = $state(false);
|
||||
|
||||
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');
|
||||
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);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -74,189 +64,106 @@
|
|||
localStorage.removeItem('selectedDomain');
|
||||
user = null;
|
||||
selectedDomain = null;
|
||||
showDropdown = false;
|
||||
goto('/auth/login');
|
||||
}
|
||||
|
||||
function toggleDropdown(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
showDropdown = !showDropdown;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Loading...</div>
|
||||
{:else if user?.role === 'admin'}
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1>IMC Vibe</h1>
|
||||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/domains">Domains</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" bind:this={dropdownRef}>
|
||||
<button class="dropdown-trigger" onclick={toggleDropdown}>
|
||||
<span class="username">{user.username}</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 class="min-h-screen bg-base-200">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if user?.role === 'admin'}
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<header class="navbar bg-base-100 shadow-sm">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="nav-drawer" class="btn btn-square btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1">
|
||||
<a href="/" class="btn btn-ghost text-xl">IMC Vibe</a>
|
||||
</div>
|
||||
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
||||
<div class="avatar avatar-placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-10 rounded-full">
|
||||
<span class="text-xl">{user.username?.[0]?.toUpperCase() || 'U'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li class="menu-title px-4 py-2">
|
||||
<span class="text-sm opacity-60">{user.username}</span>
|
||||
</li>
|
||||
<li><a href="/auth/change-password">Change Password</a></li>
|
||||
<li><a onclick={logout} class="text-error">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="p-6">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 0.75rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #ecf0f1;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid #7f8c8d;
|
||||
color: #ecf0f1;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<div class="drawer-side z-40">
|
||||
<label for="nav-drawer" class="drawer-overlay"></label>
|
||||
<ul class="menu p-4 w-64 min-h-full bg-base-200 text-base-content">
|
||||
<li class="menu-title">
|
||||
<span class="text-sm font-semibold uppercase opacity-60">Navigation</span>
|
||||
</li>
|
||||
<li><a href="/" class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Dashboard
|
||||
</a></li>
|
||||
<li><a href="/domains" class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
Domains
|
||||
</a></li>
|
||||
{#if selectedDomain}
|
||||
<li><a href={usersLink} class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Users
|
||||
</a></li>
|
||||
<li><a href={aliasesLink} class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Aliases
|
||||
</a></li>
|
||||
{/if}
|
||||
<li><a href="/queue" class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Mail Queue
|
||||
</a></li>
|
||||
<li><a href="/logs" class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Logs
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,63 +38,42 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<h2>Dashboard</h2>
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-2xl font-bold">Dashboard</h2>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>{stats.totalDomains}</h3>
|
||||
<p>Domains</p>
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>{stats.totalUsers}</h3>
|
||||
<p>Users</p>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<a href="/domains" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold text-primary">{stats.totalDomains}</h2>
|
||||
<p class="text-base-content/60">Domains</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/domains" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold text-secondary">{stats.totalUsers}</h2>
|
||||
<p class="text-base-content/60">Users</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/domains" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold text-accent">{stats.totalAliases}</h2>
|
||||
<p class="text-base-content/60">Aliases</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/queue" class="card bg-base-100 shadow hover:shadow-md transition-shadow {stats.queueSize > 10 ? 'ring-2 ring-error' : ''}">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold {stats.queueSize > 10 ? 'text-error' : 'text-warning'}">{stats.queueSize}</h2>
|
||||
<p class="text-base-content/60">Queued Emails</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>{stats.totalAliases}</h3>
|
||||
<p>Aliases</p>
|
||||
</div>
|
||||
<div class="stat-card" class:warning={stats.queueSize > 10}>
|
||||
<h3>{stats.queueSize}</h3>
|
||||
<p>Queued Emails</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.warning {
|
||||
border: 2px solid #e74c3c;
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
font-size: 2.5rem;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
margin: 0.5rem 0 0;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.warning h3 {
|
||||
color: #e74c3c;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { decodeJWT } from '$lib/auth';
|
||||
|
||||
let user: { email?: string; username?: string } | null = $state(null);
|
||||
let user: { username?: string } | null = $state(null);
|
||||
let oldPassword = $state('');
|
||||
let newPassword = $state('');
|
||||
let confirmPassword = $state('');
|
||||
|
|
@ -17,20 +18,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
await goto('/auth/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
user = data.data;
|
||||
} catch {
|
||||
await goto('/auth/login');
|
||||
const payload = decodeJWT(token);
|
||||
if (payload) {
|
||||
user = { username: payload.username };
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -84,200 +74,88 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="page-container">
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1>Change Password</h1>
|
||||
<div class="user-info">
|
||||
<div class="min-h-screen bg-base-200" data-theme="light">
|
||||
<header class="bg-base-100 shadow">
|
||||
<div class="max-w-2xl mx-auto px-6 py-4 flex justify-between items-center">
|
||||
<h1 class="text-xl font-bold">Change Password</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if user}
|
||||
<span>Logged in as <strong>{user.email || user.username}</strong></span>
|
||||
<span class="text-base-content/60">Logged in as <strong>{user.username}</strong></span>
|
||||
{/if}
|
||||
<button class="logout" onclick={handleLogout}>Logout</button>
|
||||
<button class="btn btn-error btn-sm" onclick={handleLogout}>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
<main class="max-w-md mx-auto mt-8 p-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="success">
|
||||
Password changed successfully!
|
||||
{#if success}
|
||||
<div class="alert alert-success mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Password changed successfully!</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Current Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={oldPassword}
|
||||
placeholder="Enter current password"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">New Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={newPassword}
|
||||
placeholder="Enter new password (min 8 characters)"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Confirm New Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={confirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Changing...
|
||||
{:else}
|
||||
Change Password
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="/" class="link link-primary">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit}>
|
||||
<label>
|
||||
Current Password
|
||||
<input
|
||||
type="password"
|
||||
bind:value={oldPassword}
|
||||
placeholder="Enter current password"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
New Password
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
placeholder="Enter new password (min 8 characters)"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Confirm New Password
|
||||
<input
|
||||
type="password"
|
||||
bind:value={confirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="links">
|
||||
<a href="/auth/dashboard">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logout {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #efe;
|
||||
color: #060;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let identifier = $state('');
|
||||
let error = $state('');
|
||||
let success = $state(false);
|
||||
|
|
@ -34,165 +32,57 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1>Reset Password</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
<div class="min-h-screen flex items-center justify-center bg-base-200" data-theme="light">
|
||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
||||
<div class="card-body">
|
||||
<h1 class="text-2xl font-bold text-center mb-4">Reset Password</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="success">
|
||||
<p>If an account exists with that email or username, a password reset link has been sent.</p>
|
||||
<p>Please check your inbox.</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="/auth/login">Back to Login</a>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="info">Enter your email address or username and we'll send you a link to reset your password.</p>
|
||||
{#if success}
|
||||
<div class="alert alert-success mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>If an account exists with that email or username, a password reset link has been sent.</span>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/auth/login" class="btn btn-primary">Back to Login</a>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-base-content/60 text-center mb-6">Enter your email address or username and we'll send you a link to reset your password.</p>
|
||||
|
||||
<form onsubmit={handleSubmit}>
|
||||
<label>
|
||||
Email or Username
|
||||
<input
|
||||
type="text"
|
||||
bind:value={identifier}
|
||||
placeholder="user@example.org or username"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Email or Username</legend>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={identifier}
|
||||
placeholder="user@example.org or username"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Sending...
|
||||
{:else}
|
||||
Send Reset Link
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="links">
|
||||
<a href="/auth/login">Back to Login</a>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-center mt-4">
|
||||
<a href="/auth/login" class="link link-primary">Back to Login</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #efe;
|
||||
color: #060;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -39,151 +39,56 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1>IMC Vibe</h1>
|
||||
<p class="subtitle">Mail Server Administration</p>
|
||||
<div class="min-h-screen flex items-center justify-center bg-base-200" data-theme="light">
|
||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
||||
<div class="card-body">
|
||||
<h1 class="text-2xl font-bold text-center mb-2">IMC Vibe</h1>
|
||||
<p class="text-center text-base-content/60 mb-6">Mail Server Administration</p>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<label>
|
||||
Email or Username
|
||||
<input
|
||||
type="text"
|
||||
bind:value={identifier}
|
||||
placeholder="user@example.org or username"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
</label>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Email or Username</legend>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={identifier}
|
||||
placeholder="user@example.org or username"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
placeholder="Enter password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={password}
|
||||
placeholder="Enter password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div class="forgot">
|
||||
<a href="/auth/forgot">Forgot password?</a>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<a href="/auth/forgot" class="link link-primary text-sm">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Signing in...
|
||||
{:else}
|
||||
Sign In
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
padding: 3rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
margin: 0.5rem 0 2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
color: #c00;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.forgot {
|
||||
text-align: right;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.forgot a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.forgot a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.875rem;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -87,154 +87,91 @@
|
|||
$effect(() => { loadData(); });
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<h2>Domains</h2>
|
||||
{#if isAdmin}
|
||||
<button onclick={openModal}>Add Domain</button>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold">Domains</h2>
|
||||
{#if isAdmin}
|
||||
<button class="btn btn-primary" onclick={openModal}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Add Domain
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if domains.length === 0}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-12">
|
||||
<p class="text-base-content/60">No domains configured yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Users</th>
|
||||
<th>Aliases</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each domains as domain}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<a href="/domains/{domain.name}/users" class="font-medium text-primary hover:underline">
|
||||
{domain.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{domain.userCount}</td>
|
||||
<td>{domain.aliasCount}</td>
|
||||
<td>
|
||||
{#if isAdmin}
|
||||
<button class="btn btn-error btn-sm" onclick={() => deleteDomain(domain.name)}>
|
||||
Delete
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if domains.length === 0}
|
||||
<p>No domains configured yet.</p>
|
||||
{:else}
|
||||
<h3>Domains</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Users</th>
|
||||
<th>Aliases</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each domains as domain}
|
||||
<tr>
|
||||
<td><a href="/domains/{domain.name}/users">{domain.name}</a></td>
|
||||
<td>{domain.userCount}</td>
|
||||
<td>{domain.aliasCount}</td>
|
||||
<td>
|
||||
<button class="danger" onclick={() => deleteDomain(domain.name)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<dialog bind:this={dialogEl}>
|
||||
<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>
|
||||
{#if createError}
|
||||
<p class="error">{createError}</p>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit">Create</button>
|
||||
</div>
|
||||
<dialog bind:this={dialogEl} class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Add Domain</h3>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createDomain(); }}>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Domain name</legend>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={newDomain.name}
|
||||
placeholder="example.org"
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
{#if createError}
|
||||
<div class="alert alert-error mt-4">
|
||||
<span>{createError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick={closeModal}>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
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 {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #95a5a6;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
dialog h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
dialog form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
dialog label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
dialog input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
dialog .error {
|
||||
color: #e74c3c;
|
||||
background: #fdecea;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -37,81 +37,46 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else if domain}
|
||||
<div class="header">
|
||||
<a href="/domains" class="back">← Back to Domains</a>
|
||||
<h2>{domain.name}</h2>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/domains" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back to Domains
|
||||
</a>
|
||||
{#if domain}
|
||||
<h2 class="text-2xl font-bold">{domain.name}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="cards-grid">
|
||||
<a href="/domains/{domain.name}/users" class="card">
|
||||
<h3>{domain.userCount}</h3>
|
||||
<p>Users</p>
|
||||
</a>
|
||||
<a href="/domains/{domain.name}/aliases" class="card">
|
||||
<h3>{domain.aliasCount}</h3>
|
||||
<p>Aliases</p>
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Domain not found</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0.5rem 0 0;
|
||||
color: #7f8c8d;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else if domain}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl">
|
||||
<a href="/domains/{domain.name}/users" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold text-primary">{domain.userCount}</h2>
|
||||
<p class="text-base-content/60">Users</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/domains/{domain.name}/aliases" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<h2 class="card-title text-4xl font-bold text-secondary">{domain.aliasCount}</h2>
|
||||
<p class="text-base-content/60">Aliases</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<span>Domain not found</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -92,192 +92,102 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<a href="/domains/{domainName}" class="back">← Back to {domainName}</a>
|
||||
<h2>Aliases</h2>
|
||||
<button onclick={openModal}>Add Alias</button>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold">Aliases - {domainName}</h2>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick={openModal}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Add Alias
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if aliases.length === 0}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-12">
|
||||
<p class="text-base-content/60">No aliases configured yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Destination</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each aliases as alias}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{alias.source}</td>
|
||||
<td>{alias.destination}</td>
|
||||
<td>
|
||||
<button class="btn btn-error btn-sm" onclick={() => deleteAlias(alias.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if aliases.length === 0}
|
||||
<p>No aliases configured yet.</p>
|
||||
{:else}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Destination</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each aliases as alias}
|
||||
<tr>
|
||||
<td>{alias.source}</td>
|
||||
<td>{alias.destination}</td>
|
||||
<td>
|
||||
<button class="danger" onclick={() => deleteAlias(alias.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<dialog bind:this={dialogEl}>
|
||||
<h3>Add Alias</h3>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createAlias(); }}>
|
||||
<label class="email-input">
|
||||
<span class="label-text">Source (alias address):</span>
|
||||
<div class="input-group">
|
||||
<input type="text" bind:value={sourceLocalPart} placeholder="alias-name" required />
|
||||
<span class="domain-suffix">@{domainName}</span>
|
||||
<dialog bind:this={dialogEl} class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Add Alias</h3>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createAlias(); }}>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Source (alias address)</legend>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered join-item flex-1"
|
||||
bind:value={sourceLocalPart}
|
||||
placeholder="alias-name"
|
||||
required
|
||||
/>
|
||||
<span class="btn btn-disabled join-item">@{domainName}</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset mt-4">
|
||||
<legend class="fieldset-legend">Destination (forward to)</legend>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={newAlias.destination}
|
||||
placeholder="user@example.org"
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
{#if createError}
|
||||
<div class="alert alert-error mt-4">
|
||||
<span>{createError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
Destination (forward to):
|
||||
<input type="text" bind:value={newAlias.destination} placeholder="user@example.org" required />
|
||||
</label>
|
||||
{#if createError}
|
||||
<p class="error">{createError}</p>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick={closeModal}>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.back {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #95a5a6;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
dialog h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
dialog form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
dialog label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
dialog input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.email-input .label-text {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.domain-suffix {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
dialog .error {
|
||||
color: #e74c3c;
|
||||
background: #fdecea;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -114,198 +114,112 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<a href="/domains/{domainName}" class="back">← Back to {domainName}</a>
|
||||
<h2>Users</h2>
|
||||
<button onclick={openModal}>Add User</button>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold">Users - {domainName}</h2>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick={openModal}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if users.length === 0}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-12">
|
||||
<p class="text-base-content/60">No users configured yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Quota</th>
|
||||
<th>Used</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{user.email}</td>
|
||||
<td>{formatQuota(user.quota)}</td>
|
||||
<td>{formatUsed(user.usedQuota, user.quota)}</td>
|
||||
<td>
|
||||
<button class="btn btn-error btn-sm" onclick={() => deleteUser(user.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if users.length === 0}
|
||||
<p>No users configured yet.</p>
|
||||
{:else}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Quota</th>
|
||||
<th>Used</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user}
|
||||
<tr>
|
||||
<td>{user.email}</td>
|
||||
<td>{formatQuota(user.quota)}</td>
|
||||
<td>{formatUsed(user.usedQuota, user.quota)}</td>
|
||||
<td>
|
||||
<button class="danger" onclick={() => deleteUser(user.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<dialog bind:this={dialogEl}>
|
||||
<h3>Add User</h3>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createUser(); }}>
|
||||
<label class="email-input">
|
||||
<span class="label-text">Email:</span>
|
||||
<div class="input-group">
|
||||
<input type="text" bind:value={localPart} placeholder="username" required />
|
||||
<span class="domain-suffix">@{domainName}</span>
|
||||
<dialog bind:this={dialogEl} class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Add User</h3>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createUser(); }}>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Username</legend>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered join-item flex-1"
|
||||
bind:value={localPart}
|
||||
placeholder="username"
|
||||
required
|
||||
/>
|
||||
<span class="btn btn-disabled join-item">@{domainName}</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset mt-4">
|
||||
<legend class="fieldset-legend">Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={newUser.password}
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset mt-4">
|
||||
<legend class="fieldset-legend">Quota (bytes, 0 = default)</legend>
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={newUser.quota}
|
||||
min="0"
|
||||
/>
|
||||
</fieldset>
|
||||
{#if createError}
|
||||
<div class="alert alert-error mt-4">
|
||||
<span>{createError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
Password:
|
||||
<input type="password" bind:value={newUser.password} required />
|
||||
</label>
|
||||
<label>
|
||||
Quota (bytes, 0 = default):
|
||||
<input type="number" bind:value={newUser.quota} min="0" />
|
||||
</label>
|
||||
{#if createError}
|
||||
<p class="error">{createError}</p>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick={closeModal}>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.back {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #95a5a6;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
dialog h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
dialog form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
dialog label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
dialog input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.email-input .label-text {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.domain-suffix {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
dialog .error {
|
||||
color: #e74c3c;
|
||||
background: #fdecea;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -35,134 +35,58 @@
|
|||
|
||||
function getPriorityClass(priority: string): string {
|
||||
const p = priority.toLowerCase();
|
||||
if (p === 'err' || p === 'error') return 'error';
|
||||
if (p === 'warning' || p === 'warn') return 'warning';
|
||||
if (p === 'info') return 'info';
|
||||
if (p === 'err' || p === 'error') return 'text-error';
|
||||
if (p === 'warning' || p === 'warn') return 'text-warning';
|
||||
return '';
|
||||
}
|
||||
|
||||
$effect(() => { loadLogs(); });
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<h2>Mail Logs</h2>
|
||||
<div class="controls">
|
||||
<label>
|
||||
Hours:
|
||||
<select bind:value={hours} onchange={loadLogs}>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center flex-wrap gap-4">
|
||||
<h2 class="text-2xl font-bold">Mail Logs</h2>
|
||||
<div class="flex gap-2 items-center">
|
||||
<select class="select select-bordered" bind:value={hours} onchange={loadLogs}>
|
||||
<option value={1}>Last hour</option>
|
||||
<option value={6}>Last 6 hours</option>
|
||||
<option value={24}>Last 24 hours</option>
|
||||
<option value={48}>Last 48 hours</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Filter:
|
||||
<input type="text" bind:value={filter} placeholder="Filter logs..." onkeydown={(e) => e.key === 'Enter' && loadLogs()} />
|
||||
</label>
|
||||
<button onclick={loadLogs}>Refresh</button>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={filter}
|
||||
placeholder="Filter logs..."
|
||||
onkeydown={(e) => e.key === 'Enter' && loadLogs()}
|
||||
/>
|
||||
<button class="btn btn-primary" onclick={loadLogs}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else if logs.length === 0}
|
||||
<p>No log entries found.</p>
|
||||
{:else}
|
||||
<div class="log-container">
|
||||
{#each logs as entry}
|
||||
<div class="log-entry {getPriorityClass(entry.priority)}">
|
||||
<span class="timestamp">{entry.timestamp}</span>
|
||||
<span class="priority">[{entry.priority}]</span>
|
||||
<span class="message">{entry.message}</span>
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else if logs.length === 0}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-12">
|
||||
<p class="text-base-content/60">No log entries found.</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.controls select,
|
||||
.controls input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #888;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.priority {
|
||||
margin-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-entry.error .priority {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.log-entry.warning .priority {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.log-entry.info .priority {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-neutral text-neutral-content rounded-lg p-4 max-h-[600px] overflow-y-auto font-mono text-sm">
|
||||
{#each logs as entry}
|
||||
<div class="py-1 border-b border-neutral/20 last:border-0">
|
||||
<span class="text-neutral-content/50 mr-2">{entry.timestamp}</span>
|
||||
<span class="{getPriorityClass(entry.priority)} font-bold">[{entry.priority}]</span>
|
||||
<span class="ml-2">{entry.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,102 +70,62 @@
|
|||
$effect(() => { loadQueue(); });
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<h2>Mail Queue</h2>
|
||||
<button onclick={loadQueue}>Refresh</button>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold">Mail Queue</h2>
|
||||
<button class="btn btn-primary" onclick={loadQueue}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else if queue.length === 0}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-12">
|
||||
<p class="text-base-content/60">Queue is empty.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Sender</th>
|
||||
<th>Recipients</th>
|
||||
<th>Size</th>
|
||||
<th>Reason</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each queue as entry}
|
||||
<tr class="hover">
|
||||
<td class="font-mono text-sm">{entry.id}</td>
|
||||
<td class="text-sm">{entry.sender}</td>
|
||||
<td class="text-sm">{entry.recipients.join(', ')}</td>
|
||||
<td>{formatSize(entry.size)}</td>
|
||||
<td class="text-error text-sm">{entry.reason}</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-warning" onclick={() => requeue(entry.id)}>Requeue</button>
|
||||
<button class="btn btn-sm btn-error" onclick={() => deleteFromQueue(entry.id)}>Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading...</p>
|
||||
{:else if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else if queue.length === 0}
|
||||
<p>Queue is empty.</p>
|
||||
{:else}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Sender</th>
|
||||
<th>Recipients</th>
|
||||
<th>Size</th>
|
||||
<th>Time</th>
|
||||
<th>Reason</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each queue as entry}
|
||||
<tr>
|
||||
<td class="id">{entry.id}</td>
|
||||
<td>{entry.sender}</td>
|
||||
<td>{entry.recipients.join(', ')}</td>
|
||||
<td>{formatSize(entry.size)}</td>
|
||||
<td>{entry.time}</td>
|
||||
<td class="reason">{entry.reason}</td>
|
||||
<td>
|
||||
<button onclick={() => requeue(entry.id)}>Requeue</button>
|
||||
<button class="danger" onclick={() => deleteFromQueue(entry.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.id {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reason {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [tailwindcss(), sveltekit()]
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue