Domain Admin
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
FiRefreshCw, FiList, FiLogOut, FiSettings, FiKey, FiTrash2, FiPlus, FiInbox,
|
||||
FiUsers, FiUser,
|
||||
} from 'react-icons/fi';
|
||||
|
||||
import Login from './components/Login';
|
||||
@@ -12,6 +13,8 @@ import NewMailboxModal from './components/NewMailboxModal';
|
||||
import PasswordResetModal from './components/PasswordResetModal';
|
||||
import ConfirmDialog from './components/ConfirmDialog';
|
||||
import AuditLogModal from './components/AuditLogModal';
|
||||
import AdminUsersModal from './components/AdminUsersModal';
|
||||
import ChangeMyPasswordModal from './components/ChangeMyPasswordModal';
|
||||
|
||||
import { authAPI, domainsAPI, mailboxesAPI } from './services/api';
|
||||
|
||||
@@ -23,19 +26,25 @@ function App() {
|
||||
const [selectedDomain, setSelectedDomain] = useState(null);
|
||||
const [mailboxes, setMailboxes] = useState([]);
|
||||
|
||||
const [busyMessage, setBusyMessage] = useState(''); // global blocking spinner
|
||||
const [busyMessage, setBusyMessage] = useState('');
|
||||
const [toast, setToast] = useState(null);
|
||||
|
||||
const [settingsTarget, setSettingsTarget] = useState(null); // { email, tab }
|
||||
const [settingsTarget, setSettingsTarget] = useState(null);
|
||||
const [pwTarget, setPwTarget] = useState(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
const [showAudit, setShowAudit] = useState(false);
|
||||
const [showAdmins, setShowAdmins] = useState(false);
|
||||
const [showChangePw, setShowChangePw] = useState(false);
|
||||
|
||||
const showToast = useCallback((message, type = 'success') => {
|
||||
setToast({ message, type });
|
||||
}, []);
|
||||
|
||||
const isSuperAdmin = user?.role === 'super_admin';
|
||||
// Hide left column entirely if a domain admin only has a single domain.
|
||||
const hideDomainList = !isSuperAdmin && domains.length <= 1;
|
||||
|
||||
// ---- data loading helpers ----
|
||||
const loadDomains = useCallback(async (resync = false) => {
|
||||
const list = await domainsAPI.list(resync);
|
||||
@@ -63,13 +72,13 @@ function App() {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// After login (or on first authenticated render): load domains.
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
(async () => {
|
||||
setBusyMessage('Loading domains...');
|
||||
try {
|
||||
const list = await loadDomains(true);
|
||||
// Only super admin should trigger an upstream DMS resync on login.
|
||||
const list = await loadDomains(isSuperAdmin);
|
||||
const first = list[0]?.domain || null;
|
||||
setSelectedDomain(first);
|
||||
if (first) {
|
||||
@@ -85,7 +94,6 @@ function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
// Switch domain -> reload mailboxes (with quota refresh, like the original).
|
||||
const selectDomain = async (domain) => {
|
||||
if (domain === selectedDomain) return;
|
||||
setSelectedDomain(domain);
|
||||
@@ -104,7 +112,6 @@ function App() {
|
||||
try {
|
||||
await domainsAPI.resync();
|
||||
const list = await loadDomains(false);
|
||||
// If selected domain disappeared, fall back to first.
|
||||
if (selectedDomain && !list.find((d) => d.domain === selectedDomain)) {
|
||||
const first = list[0]?.domain || null;
|
||||
setSelectedDomain(first);
|
||||
@@ -169,22 +176,34 @@ function App() {
|
||||
<div className="min-h-screen">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-bold text-gray-900">MailAdmin</h1>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{user.email} · <span className="font-medium">{user.role}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
<button onClick={() => setShowChangePw(true)} className="btn-secondary" title="Change my password">
|
||||
<FiUser className="w-4 h-4 mr-2" />
|
||||
My Password
|
||||
</button>
|
||||
<button onClick={() => setShowAudit(true)} className="btn-secondary">
|
||||
<FiList className="w-4 h-4 mr-2" />
|
||||
Audit Log
|
||||
</button>
|
||||
<button onClick={handleResync} className="btn-secondary">
|
||||
<FiRefreshCw className="w-4 h-4 mr-2" />
|
||||
DMS Resync
|
||||
</button>
|
||||
{isSuperAdmin && (
|
||||
<>
|
||||
<button onClick={() => setShowAdmins(true)} className="btn-secondary">
|
||||
<FiUsers className="w-4 h-4 mr-2" />
|
||||
Admins
|
||||
</button>
|
||||
<button onClick={handleResync} className="btn-secondary">
|
||||
<FiRefreshCw className="w-4 h-4 mr-2" />
|
||||
DMS Resync
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={handleLogout} className="btn-ghost">
|
||||
<FiLogOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
@@ -195,46 +214,50 @@ function App() {
|
||||
|
||||
{/* Main */}
|
||||
<main className="max-w-7xl mx-auto px-6 py-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[320px_1fr] gap-6 items-start">
|
||||
{/* Domains */}
|
||||
<section className="card">
|
||||
<h2 className="text-base font-semibold text-gray-900">Domains on this node</h2>
|
||||
<p className="text-xs text-gray-500 mt-1 mb-4">
|
||||
Domains are discovered dynamically from DMS accounts.
|
||||
</p>
|
||||
<div className={`grid grid-cols-1 ${hideDomainList ? '' : 'lg:grid-cols-[320px_1fr]'} gap-6 items-start`}>
|
||||
{/* Domains - hidden when single-domain user */}
|
||||
{!hideDomainList && (
|
||||
<section className="card">
|
||||
<h2 className="text-base font-semibold text-gray-900">Domains on this node</h2>
|
||||
<p className="text-xs text-gray-500 mt-1 mb-4">
|
||||
{isSuperAdmin
|
||||
? 'Domains are discovered dynamically from DMS accounts.'
|
||||
: 'Your assigned domains.'}
|
||||
</p>
|
||||
|
||||
{domains.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No domains found yet.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{domains.map((d) => {
|
||||
const active = d.domain === selectedDomain;
|
||||
return (
|
||||
<button
|
||||
key={d.domain}
|
||||
onClick={() => selectDomain(d.domain)}
|
||||
className={`text-left p-3 rounded-lg border transition-colors ${
|
||||
active
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-gray-900">{d.domain}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{d.active_mailboxes || 0} inboxes · {formatBytes(d.used_bytes)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<span className="pill">{d.current_node || d.node_name}</span>
|
||||
<span className={d.status === 'active' ? 'pill-success' : 'pill'}>
|
||||
{d.status}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{domains.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No domains available.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{domains.map((d) => {
|
||||
const active = d.domain === selectedDomain;
|
||||
return (
|
||||
<button
|
||||
key={d.domain}
|
||||
onClick={() => selectDomain(d.domain)}
|
||||
className={`text-left p-3 rounded-lg border transition-colors ${
|
||||
active
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-gray-900">{d.domain}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{d.active_mailboxes || 0} inboxes · {formatBytes(d.used_bytes)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<span className="pill">{d.current_node || d.node_name}</span>
|
||||
<span className={d.status === 'active' ? 'pill-success' : 'pill'}>
|
||||
{d.status}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Mailboxes */}
|
||||
<section className="card relative">
|
||||
@@ -293,7 +316,6 @@ function App() {
|
||||
<td className="py-3 pr-4 text-xs text-gray-500 whitespace-nowrap">
|
||||
{new Date(m.updated_at).toLocaleString()}
|
||||
</td>
|
||||
{/* Actions: single row, no wrapping. Punkt 3 deiner Liste. */}
|
||||
<td className="py-3 pr-2">
|
||||
<div className="flex flex-row flex-nowrap items-center gap-2">
|
||||
<button
|
||||
@@ -371,11 +393,21 @@ function App() {
|
||||
onClose={() => setShowAudit(false)}
|
||||
onToast={showToast}
|
||||
/>
|
||||
{isSuperAdmin && (
|
||||
<AdminUsersModal
|
||||
open={showAdmins}
|
||||
currentUser={user}
|
||||
onClose={() => setShowAdmins(false)}
|
||||
onToast={showToast}
|
||||
/>
|
||||
)}
|
||||
<ChangeMyPasswordModal
|
||||
open={showChangePw}
|
||||
onClose={() => setShowChangePw(false)}
|
||||
onToast={showToast}
|
||||
/>
|
||||
|
||||
{/* Global blocking overlay (Punkt 2 deiner Liste) */}
|
||||
{busyMessage && <LoadingOverlay message={busyMessage} fullscreen />}
|
||||
|
||||
{/* Toast */}
|
||||
{toast && <Toast {...toast} onClose={() => setToast(null)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user