mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
A refused login used to delete the account's data, and with it everyone's S/MIME and OpenPGP keys, also with offline off. The usual cause is a password changed on another device, which Stalwart 0.16 and 1.0 both answer with `invalid_grant`, so that lost unsent mail for little protection. - A refused login signs out at once and hides the data. A new password brings it back, unsent mail included. Otherwise it goes after 30 days. The sign-in page gives a count and a date. The 24-hour timer is gone. - `organisation.devices.wipeOnRefusedLogin`, off by default, asks for deletion at once, of that account's own data only. - The session limit no longer counts as a refusal on any route. It hides the data and says nothing. - A wipe runs under a Web Lock, tells other windows when it starts and ends, and queues its report before deleting anything. It reports only once the deletion worked and no plugin database is left. - More goes with a wipe: Pro tabs, search history, Files and WebDAV state, templates, pop-out drafts, shown notifications, push state, and Web Push with the last account. - The unsent count is per account, and unknown rather than 0 when the store can't say. A new row is checked against the account that asked, and a sign-out drops its row. Existing tests changed because the rule did: `auth-store-keep-local.test.ts` expected a wipe on `invalid_grant` and at the session limit, and `device-wipe.test.ts` expected shared keys to go on a refused login.
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import { wipeWholeDevice } from '@/lib/offline/device-wipe';
|
|
import { unsentForWipe } from '@/lib/offline/offline-mode';
|
|
|
|
/** The question doesn't wait as long for the count as the wipe itself does. */
|
|
const QUESTION_WAIT_MS = 2_000;
|
|
|
|
/**
|
|
* "Remove everything from this device", for someone giving a computer away or
|
|
* selling it. It signs out, so the question says exactly what goes.
|
|
*/
|
|
export function WipeDeviceSection() {
|
|
const t = useTranslations('offlineWipe');
|
|
const [asking, setAsking] = useState(false);
|
|
// Null: the store couldn't say, which isn't the same as none.
|
|
const [unsent, setUnsent] = useState<number | null>(0);
|
|
const [wiping, setWiping] = useState(false);
|
|
|
|
const ask = async () => {
|
|
setUnsent(await unsentForWipe('all', QUESTION_WAIT_MS).catch(() => null));
|
|
setAsking(true);
|
|
};
|
|
|
|
const confirmed = () => {
|
|
setAsking(false);
|
|
setWiping(true);
|
|
void wipeWholeDevice('user').finally(() => setWiping(false));
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border py-3" data-testid="wipe-device-section">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium text-foreground">{t('settings_title')}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">{t('settings_description')}</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-destructive"
|
|
disabled={wiping}
|
|
onClick={() => { void ask(); }}
|
|
data-testid="wipe-device-button"
|
|
>
|
|
{wiping ? t('settings_wiping') : t('settings_button')}
|
|
</Button>
|
|
<ConfirmDialog
|
|
isOpen={asking}
|
|
onClose={() => setAsking(false)}
|
|
onConfirm={confirmed}
|
|
title={t('confirm_title')}
|
|
message={t('confirm_message')}
|
|
details={(
|
|
<ul className="list-disc space-y-1 ps-5 text-sm text-muted-foreground" data-testid="wipe-device-details">
|
|
<li>{t('confirm_mail')}</li>
|
|
{unsent === null && <li className="font-medium text-foreground">{t('confirm_unsent_unknown')}</li>}
|
|
{unsent !== null && unsent > 0 && <li className="font-medium text-foreground">{t('confirm_unsent', { count: unsent })}</li>}
|
|
<li>{t('confirm_keys')}</li>
|
|
<li>{t('confirm_plugins')}</li>
|
|
<li>{t('confirm_files')}</li>
|
|
<li>{t('confirm_templates')}</li>
|
|
<li>{t('confirm_accounts')}</li>
|
|
</ul>
|
|
)}
|
|
confirmText={t('confirm_button')}
|
|
variant="destructive"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|