aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
diff options
context:
space:
mode:
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.js28
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>`