From eb27371f0dfe6f269812296ace4604c8002bbd9e Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 21:02:49 +0300 Subject: [PATCH] feat(settings): harden 2fa recovery code UX with warning copy and download --- docs/core-checklist-status.md | 2 +- web/src/components/SettingsPanel.tsx | 54 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index e366f04..9f5241e 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -38,7 +38,7 @@ Legend: 29. Archive - `DONE` 30. Blacklist - `DONE` 31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; group-invite `nobody` is available in API and web settings; integration tests cover PM policy matrix (`everyone/contacts/nobody`), group-invite policy matrix (`everyone/contacts/nobody`), and private chat counterpart visibility for `nobody/contacts`, remaining UX/matrix hardening) -32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; integration tests cover single-session revoke and revoke-all invalidation/force-disconnect; 2FA setup now blocked after enable to prevent secret re-issuance; one-time recovery codes added and covered for normalization/lifecycle (`remaining_codes` decrement + one-time usage); web settings now has safer revoke UX with confirmation/loading/error feedback; web auth panel supports recovery-code login) +32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; integration tests cover single-session revoke and revoke-all invalidation/force-disconnect; 2FA setup now blocked after enable to prevent secret re-issuance; one-time recovery codes added and covered for normalization/lifecycle (`remaining_codes` decrement + one-time usage); web auth panel supports recovery-code login; settings now warns when recovery codes are empty and provides copy/download actions for freshly generated codes) 33. Realtime Events - `DONE` (connect/disconnect/send/receive/typing/read/delivered/online/offline + chat/message updates + chat_deleted) 34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages, chat-info panel hot-refreshes on `chat_updated`, delete/leave updates realtime subscriptions, full-chat delete emits `chat_deleted`) 35. Additional - `PARTIAL` (drafts/link preview partial/autoload media basic) diff --git a/web/src/components/SettingsPanel.tsx b/web/src/components/SettingsPanel.tsx index 9b30d44..e87b9cb 100644 --- a/web/src/components/SettingsPanel.tsx +++ b/web/src/components/SettingsPanel.tsx @@ -240,6 +240,37 @@ export function SettingsPanel({ open, onClose }: Props) { } } + async function copyRecoveryCodes(codes: string[]) { + if (!codes.length) { + showToast("No recovery codes to copy"); + return; + } + try { + await navigator.clipboard.writeText(codes.join("\n")); + showToast("Recovery codes copied"); + } catch { + showToast("Failed to copy recovery codes"); + } + } + + function downloadRecoveryCodes(codes: string[]) { + if (!codes.length) { + showToast("No recovery codes to download"); + return; + } + const content = `BenyaMessenger 2FA Recovery Codes\n\n${codes.join("\n")}\n`; + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "benya-recovery-codes.txt"; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + showToast("Recovery codes downloaded"); + } + return createPortal(