diff --git a/README.md b/README.md index f3626d3..702fc0f 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ seconds of downtime with nothing lost. **Settings** - **Dates & times**: language/region (every one of the ~620 locales CLDR has data for, each named in its own language and script), date order (locale default, `22.11.2025`, `22/11/2025`, `11/22/2025` or ISO `2025-11-22`) and 12h/24h clock, applied everywhere — message list and headers, calendar, contacts, files, sessions. The default comes from the locale configured for the account in Stalwart (`x:AccountSettings/get`, falling back to `x:Account/get`), and from the browser where the server will not say; POSIX forms are normalised (`de_DE.UTF-8` → `de-DE`) and script modifiers preserved (`sr_RS@latin` → `sr-Latn-RS`). Numerals follow the locale (`٢٢.١١.٢٠٢٥` for `ar-EG`), except under ISO 8601, which pins date *and* clock to Latin digits. Dates are **entered** through custom pickers in the same format (browsers render `` in their own locale and ignore the page's), with a calendar popover, a time list, keyboard navigation, and lenient typing — `22.11.`, `221125`, `6:23pm` and bare ISO all parse -- **Self-service credentials** in Settings › Security: change your password, manage **app passwords** (a separate password per mail app or device, revocable on its own), and turn **two-factor authentication** on or off by scanning a QR code. Enrolment codes are verified before anything is stored, so a mistyped key cannot lock you out, and switching 2FA on moves this browser's session onto a dedicated app password instead of signing you straight back out. Built on the `x:AccountPassword` / `x:AppPassword` registry objects +- **Self-service credentials** in Settings › Security: change your password and manage **app passwords** (a separate password per mail app or device, revocable on its own). Built on the `x:AccountPassword` / `x:AppPassword` registry objects. Turning **two-factor authentication** *on* is not offered here — ihasmail cannot sign in with a code yet, so enrolling would only lock the account out on its next sign-in. An account that already has 2FA on gets one control, to turn it off - **Light and dark** follow the system by default, with a toggle in the top bar for flipping between them and a three-way choice in Settings › Appearance - **Hide identities from the compose picker** — an account with alias domains can have every address twice over while only a handful are ever sent from, which makes the From picker unusable. Hiding is presentation only: the identity still exists, still receives, and stays listed and editable in Settings, the way an unsubscribed folder is still a folder. The identity a draft is already using and the default can never be hidden, and hiding every one of them offers them all again — a sender picker with nothing in it is worse than a cluttered one - Identities & signatures, **Sieve filters** (visual rule builder that round-trips to a Sieve script, plus a raw script editor with server-side validation), out-of-office (`VacationResponse`), folders, labels, templates, notifications, calendar defaults, sessions (sign out other devices), keyboard shortcuts, import/export of settings @@ -307,7 +307,7 @@ works the same way — and dropped where 0.15 was the whole subject. Support for - Snooze (nothing in JMAP or Stalwart supports it, and ihasmail never stores a password, so nothing could act on a mailbox while you are away) - Translations (strings are English-only for now) -- **Two-factor sign-in.** Today an account with 2FA must use an app password (see Quick start). Supporting a TOTP code directly means implementing OAuth: Stalwart offers the authorization-code and device flows and no password grant, so ihasmail would hand sign-in to Stalwart's own login and come back with a token. That is a better security posture than the sealed password it holds now — a refresh token rather than a credential — but it replaces ihasmail's own sign-in page for those users and may need an OAuth client registered. Reported as [#75](https://github.com/LINUXexpert-org/ihasmail/issues/75) +- **Two-factor sign-in.** Today an account with 2FA must use an app password (see Quick start), and Settings › Security offers no way to switch 2FA *on* — only off, for an account that already has it. Supporting a TOTP code directly means implementing OAuth: Stalwart offers the authorization-code and device flows and no password grant, so ihasmail would hand sign-in to Stalwart's own login and come back with a token. That is a better security posture than the sealed password it holds now — a refresh token rather than a credential — but it replaces ihasmail's own sign-in page for those users and may need an OAuth client registered. Reported as [#75](https://github.com/LINUXexpert-org/ihasmail/issues/75) ## License diff --git a/web/src/views/settings/SecuritySettings.tsx b/web/src/views/settings/SecuritySettings.tsx index cf64f38..5a4aa5b 100644 --- a/web/src/views/settings/SecuritySettings.tsx +++ b/web/src/views/settings/SecuritySettings.tsx @@ -5,7 +5,6 @@ import { useSession } from "@/store/session"; import { formatFullDate } from "@/lib/format"; import { toast } from "@/ui/toast"; import { confirmDialog, Dialog } from "@/ui/dialog"; -import { QrCode } from "@/ui/qrcode"; interface SessionRow { id: string; @@ -68,11 +67,11 @@ export function SecuritySettings() { { void load(); }} /> )} -

Two-factor authentication

- {unsupported ? ( -

Two-factor authentication is managed by your mail administrator.

- ) : ( - { await loadSecurity(); await load(); }} /> + {!unsupported && state?.otpEnabled && ( + <> +

Two-factor authentication

+ { await loadSecurity(); await load(); }} /> + )}

App passwords

@@ -168,46 +167,20 @@ function PasswordForm({ otpEnabled, onChanged }: { otpEnabled: boolean; onChange /* ------------------------------------------------------------------ */ -function TwoFactor({ state, reload }: { state: SecurityState | null; reload: () => Promise }) { - const [setup, setSetup] = useState<{ secret: string; url: string } | null>(null); +/** + * Only the way *out*. Setting two-factor authentication up is gone until + * signing in with a code works: Stalwart takes a TOTP code through an OAuth + * flow alone and offers no password grant, so ihasmail has nowhere to send one + * (#75). Turning it on here would lock the account out of webmail on its next + * sign-in. Turning it off is a plain registry write, works today, and has to + * stay — whoever is already enrolled needs a way back. + */ +function TwoFactorOff({ reload }: { reload: () => Promise }) { const [code, setCode] = useState(""); const [password, setPassword] = useState(""); const [busy, setBusy] = useState(false); const [disabling, setDisabling] = useState(false); - if (!state) return

Loading…

; - - const begin = async () => { - try { - setSetup(await apiFetch<{ secret: string; url: string }>("/api/account/2fa/begin", { method: "POST", body: "{}" })); - setCode(""); setPassword(""); - } catch (err) { - toast.error((err as Error).message); - } - }; - - const enable = async () => { - if (!setup) return; - setBusy(true); - try { - const res = await apiFetch<{ sessionKept: boolean }>("/api/account/2fa/enable", { - method: "POST", - body: JSON.stringify({ url: setup.url, code, current: password }), - }); - setSetup(null); - await reload(); - if (res.sessionKept) { - toast.success("Two-factor authentication is on. This browser stays signed in."); - } else { - toast.success("Two-factor authentication is on. You'll need to sign in again with a code."); - } - } catch (err) { - toast.error((err as Error).message); - } finally { - setBusy(false); - } - }; - const disable = async () => { setBusy(true); try { @@ -225,51 +198,15 @@ function TwoFactor({ state, reload }: { state: SecurityState | null; reload: () return (

- {state.otpEnabled - ? "Signing in requires a code from your authenticator app as well as your password." - : "Add a one-time code from an authenticator app to your sign-in, so a stolen password isn't enough on its own."} + This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another + device needs an app password — or you can turn two-factor authentication off here.

- - {state.otpEnabled ? "Enabled" : "Not enabled"} - {state.otpEnabled - ? - : } + + Enabled +
- setSetup(null)} title="Set up two-factor authentication" size="md" - footer={<> - - - }> - {setup && ( -
-
    -
  1. Scan this with your authenticator app.
  2. -
  3. Enter the six-digit code it shows, and your password.
  4. -
-
- -
-
- - -
-
- - setCode(e.target.value)} placeholder="123456" /> -
-
- - setPassword(e.target.value)} /> -
-
-
-

Codes are checked before anything is saved, so a mistyped key can't lock you out.

-
- )} -
- setDisabling(false)} title="Turn off two-factor authentication" size="sm" footer={<>