diff --git a/backend/src/routes/mailboxes.ts b/backend/src/routes/mailboxes.ts index 9161bd1..4025a88 100644 --- a/backend/src/routes/mailboxes.ts +++ b/backend/src/routes/mailboxes.ts @@ -71,6 +71,8 @@ async function refreshQuotaForDomain(req: any, domain: string): Promise mailboxesRouter.get('/', async (req, res) => { const domain = String(req.query.domain ?? '').toLowerCase(); const refreshQuota = String(req.query.refreshQuota ?? '').toLowerCase() === 'true'; + // Hide soft-deleted mailboxes by default. Set ?includeDeleted=true if you ever want them. + const includeDeleted = String(req.query.includeDeleted ?? '').toLowerCase() === 'true'; if (domain) { ensureDomain(req, domain); @@ -82,6 +84,7 @@ mailboxesRouter.get('/', async (req, res) => { const params: unknown[] = [config.nodeName]; let where = 'WHERE node_name=$1'; + if (!includeDeleted) where += ` AND status <> 'deleted'`; if (domain) { params.push(domain); where += ` AND domain=$${params.length}`; } const result = await pool.query( diff --git a/frontend/src/components/PasswordResetModal.jsx b/frontend/src/components/PasswordResetModal.jsx index 000a991..727c969 100644 --- a/frontend/src/components/PasswordResetModal.jsx +++ b/frontend/src/components/PasswordResetModal.jsx @@ -4,15 +4,25 @@ import { mailboxesAPI } from '../services/api'; const PasswordResetModal = ({ open, email, onClose, onToast }) => { const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); useEffect(() => { - if (open) { setPassword(''); setError(''); } + if (open) { setPassword(''); setConfirm(''); setError(''); } }, [open, email]); + // Live mismatch hint, only after the user has typed something in the second field. + const mismatch = confirm.length > 0 && password !== confirm; + const tooShort = password.length > 0 && password.length < 8; + const canSubmit = password.length >= 8 && password === confirm; + const submit = async () => { - if (password.length < 8) { setError('Password must have at least 8 characters.'); return; } + if (!canSubmit) { + if (password.length < 8) setError('Password must have at least 8 characters.'); + else if (password !== confirm) setError('Passwords do not match.'); + return; + } setBusy(true); setError(''); try { await mailboxesAPI.setPassword(email, password); @@ -34,16 +44,51 @@ const PasswordResetModal = ({ open, email, onClose, onToast }) => { type="password" value={password} onChange={(e) => setPassword(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }} - className="input-field" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + document.getElementById('pw-confirm')?.focus(); + } + }} + className={`input-field ${tooShort ? 'border-red-500 focus:ring-red-500' : ''}`} minLength={8} autoFocus /> + {tooShort && ( +

Minimum 8 characters.

+ )} + +
+ + setConfirm(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }} + className={`input-field ${mismatch ? 'border-red-500 focus:ring-red-500' : ''}`} + minLength={8} + /> + {mismatch && ( +

Passwords do not match.

+ )} + {!mismatch && confirm.length > 0 && password === confirm && password.length >= 8 && ( +

Passwords match.

+ )} +
+ {error &&

{error}

} +
- - +
@@ -52,4 +97,4 @@ const PasswordResetModal = ({ open, email, onClose, onToast }) => { ); }; -export default PasswordResetModal; +export default PasswordResetModal; \ No newline at end of file diff --git a/frontend/src/components/UsageBar.jsx b/frontend/src/components/UsageBar.jsx index a4590c6..5e515d3 100644 --- a/frontend/src/components/UsageBar.jsx +++ b/frontend/src/components/UsageBar.jsx @@ -30,13 +30,16 @@ const UsageBar = ({ mailbox }) => { ? `${bytes(used)} / ${bytes(limit)} (${fmtPercent(pct)}%)` : `${bytes(used)} / unlimited`; - // Color the bar by usage. No quota -> neutral primary tone. const fillColor = pct === null ? 'bg-primary-500' : pct >= 90 ? 'bg-red-500' : pct >= 75 ? 'bg-amber-500' : 'bg-emerald-500'; + const hasMessageCount = + mailbox.message_count !== null && mailbox.message_count !== undefined; + const messageCount = hasMessageCount ? Number(mailbox.message_count) : null; + return (
Disk Usage: {label}
@@ -46,15 +49,13 @@ const UsageBar = ({ mailbox }) => { style={{ width: `${pct ?? 0}%` }} />
-
- {mailbox.usage_scanned_at - ? `quota checked ${new Date(mailbox.usage_scanned_at).toLocaleString()}` - : 'quota not checked yet'} - {mailbox.message_count !== null && mailbox.message_count !== undefined - ? ` ยท ${Number(mailbox.message_count)} messages` : ''} -
+ {hasMessageCount && ( +
+ {messageCount} {messageCount === 1 ? 'message' : 'messages'} +
+ )} ); }; -export default UsageBar; +export default UsageBar; \ No newline at end of file