aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/auth-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js48
1 files changed, 30 insertions, 18 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index c5374f8..4b8ed7c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -339,6 +339,23 @@ function WelcomePitch() {
`;
}
+/**
+ * A `mailto:` link that has the person's own mail application send the
+ * recovery key to their own address. The hub never sees the key (§3.6): it
+ * used to append it to the verification e-mail, which put it in clear on the
+ * hub's way out. Opened in the browser by the link itself, and by the desktop
+ * and Android shells, which hand `mailto:` to the system.
+ */
+export function recoveryMailto(email, username, mnemonic) {
+ // RFC 6068: the address is percent-encoded like the rest, but '@' stays —
+ // several mail applications do not decode it back.
+ const to = encodeURIComponent(email).replace(/%40/g, '@');
+ const subject = encodeURIComponent(t('register.recovery_mail_subject'));
+ const body = encodeURIComponent(
+ t('register.recovery_mail_body', { user: username, key: mnemonic }));
+ return `mailto:${to}?subject=${subject}&body=${body}`;
+}
+
// The sign-in page's dark gradient backdrop and frosted card, without the
// pitch beside it — so Register (and its verify/recovery/done steps) sits on
// the same background and reads in the same dark theme as Login. A lone
@@ -370,9 +387,6 @@ export function RegisterPage() {
const [recoveryMnemonic, setRecoveryMnemonic] = useState('');
const [recoverySaved, setRecoverySaved] = useState(false);
const [recoveryCopied, setRecoveryCopied] = useState(false);
- // Off by default: mailing the recovery key is opt-in — the key is shown on
- // screen to save, and sending a copy is the user's own choice to make.
- const [emailRecovery, setEmailRecovery] = useState(false);
const captcha = useCaptcha();
const onSubmit = async (e) => {
@@ -393,18 +407,16 @@ export function RegisterPage() {
try {
if (window.MeshBayKeys) {
// The account recovery key (docs/MESHBAY_DESIGN.md §3.6): generated
- // here, shown once on the next screen. When the user leaves "e-mail it"
- // checked, the mnemonic goes in the register body so the hub appends it
- // to the verification e-mail (and stores it nowhere); otherwise it is
- // screen-only. The derived key is kept in `session.recoveryKey` and
- // persisted, so groups joined later — this session or a future one —
- // still leave a recovery-wrapped copy on their node.
+ // here, shown once on the next screen, and never sent to the hub —
+ // the person may mail it to themselves from their own application.
+ // The derived key is kept in `session.recoveryKey` and persisted, so
+ // groups joined later — this session or a future one — still leave a
+ // recovery-wrapped copy on their node.
const rk = window.MeshBayKeys.generateRecoveryKey();
// `name` (trimmed), not the raw field: the hub stores the trimmed
// username and every key derivation must fold in the same string.
const reg = await window.MeshBayKeys.registerUser(
- name, email, password, emailRecovery ? rk.mnemonic : null,
- await captcha.take());
+ name, email, password, await captcha.take());
// An account made in the desktop application starts without browser
// access: its identities stay on this device and nothing of them is
// left on nodes until the person turns it on, here, natively.
@@ -510,8 +522,14 @@ export function RegisterPage() {
<button class="btn-secondary" style="margin-top:8px" onClick=${copyRecovery}>
${recoveryCopied ? t('register.recovery_copied') : t('register.recovery_copy')}
</button>
+ <a class="btn-secondary recovery-mail" role="button"
+ href=${recoveryMailto(email, username.trim(), recoveryMnemonic)}
+ target=${platform.isNative ? '_blank' : undefined} rel="noopener">
+ ${t('register.recovery_mail_self')}
+ </a>
+ <p class="settings-hint" style="margin-top:6px">${t('register.recovery_mail_hint')}</p>
<p style="margin-top:12px; color:var(--text-secondary)">
- ${emailRecovery ? t('register.recovery_emailed') : t('register.recovery_not_emailed')}
+ ${t('register.recovery_shown_once')}
</p>
<p class="error-msg" style="margin-top:8px">${t('register.recovery_warning')}</p>
<label style="display:flex; gap:8px; align-items:flex-start; margin-top:12px">
@@ -589,12 +607,6 @@ export function RegisterPage() {
<${PasswordInput} placeholder=${t('register.confirm')} value=${confirm}
onInput=${e => setConfirm(e.target.value)}
autocomplete="new-password" />
- <label style="display:flex; gap:8px; align-items:flex-start; margin:4px 0 2px;
- font-size:0.88em; color:var(--text-secondary)">
- <input type="checkbox" checked=${emailRecovery}
- onChange=${e => setEmailRecovery(e.target.checked)} />
- <span>${t('register.recovery_email_opt')}</span>
- </label>
${error && html`<div class="error-msg">${error}</div>`}
<button type="submit" disabled=${loading}>
${loading ? t('register.loading') : t('register.submit')}