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 | 33 |
1 files changed, 30 insertions, 3 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 10bcee7..7e355e7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js @@ -8,6 +8,21 @@ import { _storeBundleKey, _loadBundleKey, _storeRecoveryKey, } from './hub-client.js'; +// A sign-in lockout also refuses the two actions here that re-check the +// passphrase (the hub counts them on the same row). +function lockedText(seconds) { + return t('login.locked', { minutes: Math.max(1, Math.ceil(seconds / 60)) }); +} + +async function lockedMessage(token) { + try { + const me = await hubFetch('/v1/users/me', { token }); + return lockedText(me.passphrase_locked_for || 60); + } catch { + return lockedText(60); + } +} + export function ProfilePage({ user, onLogout }) { const [nodeKey, setNodeKey] = useState(''); const [currentNodeKey, setCurrentNodeKey] = useState(null); @@ -39,7 +54,8 @@ export function ProfilePage({ user, onLogout }) { }); onLogout(); } catch (err) { - setDelError(err.message); + setDelError(err.message === 'account_locked' + ? await lockedMessage(user.token) : err.message); } finally { setDeleting(false); } @@ -76,6 +92,14 @@ export function ProfilePage({ user, onLogout }) { if (cpNew !== cpNew2) { setCpError(t('settings.pw_mismatch')); return; } if (cpNew === cpOld) { setCpError(t('settings.pw_same')); return; } try { + // The next step re-wraps every node's bundle before the hub is asked to + // accept the new passphrase. Started during a lockout, the nodes would + // take the new one and the hub would refuse it — so ask first. + const me = await hubFetch('/v1/users/me', { token: user.token }); + if (me.passphrase_locked_for > 0) { + setCpError(lockedText(me.passphrase_locked_for)); + return; + } const mine = await hubFetch('/v1/groups/mine', { token: user.token }); const groups = mine.groups || []; setCpEstimate({ @@ -118,8 +142,10 @@ export function ProfilePage({ user, onLogout }) { setCpResult(result); setCpPhase('done'); } catch (err) { - const msg = /403|does not match/i.test(err.message) - ? t('settings.pw_wrong_current') : err.message; + const msg = err.message === 'account_locked' + ? await lockedMessage(user.token) + : /403|does not match/i.test(err.message) + ? t('settings.pw_wrong_current') : err.message; setCpError(msg); setCpPhase('confirm'); } @@ -361,6 +387,7 @@ export function ProfilePage({ user, onLogout }) { <input type="password" autocomplete="new-password" placeholder=${t('settings.passphrase_new_repeat')} value=${cpNew2} onInput=${e => setCpNew2(e.target.value)} required /> + ${cpError && html`<p class="error-msg">${cpError}</p>`} <div style="display:flex;gap:8px"> <button class="admin-btn" type="submit">${t('settings.continue')}</button> <button class="btn-secondary" type="button" onClick=${cpReset}> |