diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/profile-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/profile-page.js | 28 |
1 files changed, 22 insertions, 6 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js index ed7a2fa..d3d06d7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js @@ -37,6 +37,10 @@ export function ProfilePage({ user, onLogout }) { const [emailVerifyPending, setEmailVerifyPending] = useState(false); const [emailCode, setEmailCode] = useState(''); const [emailVerifying, setEmailVerifying] = useState(false); + // Changing the address on file now needs the passphrase (the hub verifies the + // derived auth_key): a bare access token — which every node this account + // connects to is handed — must not be able to start an account takeover. + const [emailPass, setEmailPass] = useState(''); const [pinCount, setPinCount] = useState( () => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0)); const [delOpen, setDelOpen] = useState(false); @@ -232,12 +236,18 @@ export function ProfilePage({ user, onLogout }) { const saveEmail = useCallback(async () => { const val = emailDraft.trim(); if (!val || val === email) { setEmailEditing(false); return; } + if (!emailPass) return; // the Save button is disabled until it is entered setEmailSaving(true); setEmailStatus(''); try { + // The passphrase is the second factor here, exactly as for a passphrase + // change or an account deletion: the hub gets the derived auth_key, never + // the passphrase itself. + const authKey = await window.MeshBayKeys.deriveAuthKey(emailPass, user.username); const resp = await hubFetch('/v1/users/me', { - method: 'PATCH', token: user.token, body: { email: val }, + method: 'PATCH', token: user.token, body: { email: val, auth_key: authKey }, }); + setEmailPass(''); if (resp.email_verification_required) { setEmailVerifyPending(true); setEmailStatus(t('settings.email_code_sent')); @@ -248,11 +258,13 @@ export function ProfilePage({ user, onLogout }) { setTimeout(() => setEmailStatus(''), 3000); } } catch (e) { - setEmailStatus(e.message); + const msg = /403|does not match/i.test(e.message) + ? t('settings.pw_wrong_current') : e.message; + setEmailStatus(msg); } finally { setEmailSaving(false); } - }, [emailDraft, email, user.token]); + }, [emailDraft, email, emailPass, user.token, user.username]); const verifyEmailChange = useCallback(async () => { if (!emailCode.trim()) return; @@ -311,14 +323,18 @@ export function ProfilePage({ user, onLogout }) { ? html`<span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"> <input type="email" value=${emailDraft} onInput=${e => setEmailDraft(e.target.value)} - onKeyDown=${e => e.key === 'Enter' && saveEmail()} disabled=${emailVerifyPending} style="font-size:0.9em;padding:4px 8px;border:1px solid var(--border);border-radius:4px" /> ${!emailVerifyPending && html` + <input type="password" autocomplete="current-password" + placeholder=${t('login.password')} + value=${emailPass} onInput=${e => setEmailPass(e.target.value)} + onKeyDown=${e => e.key === 'Enter' && saveEmail()} + style="font-size:0.9em;padding:4px 8px;border:1px solid var(--border);border-radius:4px" /> <button class="admin-btn" onClick=${saveEmail} - disabled=${emailSaving}>${t('settings.email_save')}</button> + disabled=${emailSaving || !emailPass}>${t('settings.email_save')}</button> <button class="btn-secondary" onClick=${() => { - setEmailEditing(false); setEmailDraft(email); + setEmailEditing(false); setEmailDraft(email); setEmailPass(''); }}>${t('settings.cancel')}</button> `} </span>` |