fixes
This commit is contained in:
@@ -71,6 +71,8 @@ async function refreshQuotaForDomain(req: any, domain: string): Promise<number>
|
|||||||
mailboxesRouter.get('/', async (req, res) => {
|
mailboxesRouter.get('/', async (req, res) => {
|
||||||
const domain = String(req.query.domain ?? '').toLowerCase();
|
const domain = String(req.query.domain ?? '').toLowerCase();
|
||||||
const refreshQuota = String(req.query.refreshQuota ?? '').toLowerCase() === 'true';
|
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) {
|
if (domain) {
|
||||||
ensureDomain(req, domain);
|
ensureDomain(req, domain);
|
||||||
@@ -82,6 +84,7 @@ mailboxesRouter.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const params: unknown[] = [config.nodeName];
|
const params: unknown[] = [config.nodeName];
|
||||||
let where = 'WHERE node_name=$1';
|
let where = 'WHERE node_name=$1';
|
||||||
|
if (!includeDeleted) where += ` AND status <> 'deleted'`;
|
||||||
if (domain) { params.push(domain); where += ` AND domain=$${params.length}`; }
|
if (domain) { params.push(domain); where += ` AND domain=$${params.length}`; }
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
|
|||||||
@@ -4,15 +4,25 @@ import { mailboxesAPI } from '../services/api';
|
|||||||
|
|
||||||
const PasswordResetModal = ({ open, email, onClose, onToast }) => {
|
const PasswordResetModal = ({ open, email, onClose, onToast }) => {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirm, setConfirm] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) { setPassword(''); setError(''); }
|
if (open) { setPassword(''); setConfirm(''); setError(''); }
|
||||||
}, [open, email]);
|
}, [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 () => {
|
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('');
|
setBusy(true); setError('');
|
||||||
try {
|
try {
|
||||||
await mailboxesAPI.setPassword(email, password);
|
await mailboxesAPI.setPassword(email, password);
|
||||||
@@ -34,16 +44,51 @@ const PasswordResetModal = ({ open, email, onClose, onToast }) => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }}
|
onKeyDown={(e) => {
|
||||||
className="input-field"
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('pw-confirm')?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`input-field ${tooShort ? 'border-red-500 focus:ring-red-500' : ''}`}
|
||||||
minLength={8}
|
minLength={8}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
{tooShort && (
|
||||||
|
<p className="mt-1 text-xs text-red-600">Minimum 8 characters.</p>
|
||||||
|
)}
|
||||||
</div>
|
</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>}
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2 border-t border-gray-200">
|
<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={onClose} className="btn-secondary px-4 py-2" disabled={busy}>
|
||||||
<button onClick={submit} disabled={busy} className="btn-primary">
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={submit}
|
||||||
|
disabled={busy || !canSubmit}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
{busy ? 'Updating...' : 'Update password'}
|
{busy ? 'Updating...' : 'Update password'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,13 +30,16 @@ const UsageBar = ({ mailbox }) => {
|
|||||||
? `${bytes(used)} / ${bytes(limit)} (${fmtPercent(pct)}%)`
|
? `${bytes(used)} / ${bytes(limit)} (${fmtPercent(pct)}%)`
|
||||||
: `${bytes(used)} / unlimited`;
|
: `${bytes(used)} / unlimited`;
|
||||||
|
|
||||||
// Color the bar by usage. No quota -> neutral primary tone.
|
|
||||||
const fillColor = pct === null
|
const fillColor = pct === null
|
||||||
? 'bg-primary-500'
|
? 'bg-primary-500'
|
||||||
: pct >= 90 ? 'bg-red-500'
|
: pct >= 90 ? 'bg-red-500'
|
||||||
: pct >= 75 ? 'bg-amber-500'
|
: pct >= 75 ? 'bg-amber-500'
|
||||||
: 'bg-emerald-500';
|
: 'bg-emerald-500';
|
||||||
|
|
||||||
|
const hasMessageCount =
|
||||||
|
mailbox.message_count !== null && mailbox.message_count !== undefined;
|
||||||
|
const messageCount = hasMessageCount ? Number(mailbox.message_count) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-[200px]">
|
<div className="min-w-[200px]">
|
||||||
<div className="text-xs font-semibold text-gray-700 mb-1.5">Disk Usage: {label}</div>
|
<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}%` }}
|
style={{ width: `${pct ?? 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
{hasMessageCount && (
|
||||||
{mailbox.usage_scanned_at
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
? `quota checked ${new Date(mailbox.usage_scanned_at).toLocaleString()}`
|
{messageCount} {messageCount === 1 ? 'message' : 'messages'}
|
||||||
: 'quota not checked yet'}
|
</div>
|
||||||
{mailbox.message_count !== null && mailbox.message_count !== undefined
|
)}
|
||||||
? ` · ${Number(mailbox.message_count)} messages` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user