fixes
This commit is contained in:
@@ -71,6 +71,8 @@ async function refreshQuotaForDomain(req: any, domain: string): Promise<number>
|
||||
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(
|
||||
|
||||
@@ -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 && (
|
||||
<p className="mt-1 text-xs text-red-600">Minimum 8 characters.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Confirm password</label>
|
||||
<input
|
||||
id="pw-confirm"
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={(e) => 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 && (
|
||||
<p className="mt-1 text-xs text-red-600">Passwords do not match.</p>
|
||||
)}
|
||||
{!mismatch && confirm.length > 0 && password === confirm && password.length >= 8 && (
|
||||
<p className="mt-1 text-xs text-green-600">Passwords match.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-gray-200">
|
||||
<button onClick={onClose} className="btn-secondary px-4 py-2">Cancel</button>
|
||||
<button onClick={submit} disabled={busy} className="btn-primary">
|
||||
<button onClick={onClose} className="btn-secondary px-4 py-2" disabled={busy}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={busy || !canSubmit}
|
||||
className="btn-primary"
|
||||
>
|
||||
{busy ? 'Updating...' : 'Update password'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-w-[200px]">
|
||||
<div className="text-xs font-semibold text-gray-700 mb-1.5">Disk Usage: {label}</div>
|
||||
@@ -46,13 +49,11 @@ const UsageBar = ({ mailbox }) => {
|
||||
style={{ width: `${pct ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{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` : ''}
|
||||
</div>
|
||||
{hasMessageCount && (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{messageCount} {messageCount === 1 ? 'message' : 'messages'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user