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.js33
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}>