aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-14 01:53:04 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-14 01:53:04 +0200
commit392b5e4a53aace725794c7bbabf9e95fb4e1b9c5 (patch)
tree0d31021b8558833a822bb906ec59d95abeb3f860 /packages/meshbay-hub/src/meshbay_hub/static
parent413837a0845240241ed7e9d9ac1f3b1dc45a2f40 (diff)
downloadmeshbay-392b5e4a53aace725794c7bbabf9e95fb4e1b9c5.tar.gz
fix(hub): a per-account sign-in lockout, and a reviewed unauthenticated surface
Passphrase sign-in locks per username: after `login.max_failures` wrong passphrases (default 4) the name is refused with `429 account_locked` and a `Retry-After` for `login.lockout_minutes` (default 60), without the passphrase being checked. Both numbers are instance policy an admin sets from the panel; zero failures turns it off. The per-IP limit bounds one address, and IPv6 gives every subscriber a /64 of them — an online guess targets an account, so the account is what is counted. - Counted by the name as typed, existing or not, so `login` stays uniform (M1). The key is a hash: people type passphrases into the username field. - The attempt is taken before the check in one `INSERT … ON CONFLICT DO UPDATE … WHERE … RETURNING`, so a concurrent burst gets no more than the limit. - Sign-in, passphrase change and account deletion count on the same row; the last had no rate limit at all. - A lockout refuses passphrase sign-in and nothing else: sessions, renewal and device sign-in continue, and a reset code clears it (AV26). A session learns its own lockout from `/v1/users/me`, and the passphrase change checks it before re-wrapping any node's bundle — the hub accepts the new passphrase only after the nodes have it. The SPA now shows what the hub said. `loginAndRecover` threw "Login failed: {json}", so `email_verification_required` never matched and was never shown; the passphrase-change form rendered no error at all in its first phase. The unauthenticated surface, reviewed route by route: - No `/docs`, `/redoc` or `/openapi.json`, in the code. The Caddyfile hid them on meshbay.org only; a packaged hub behind any other proxy published all three. - The node socket's first message must arrive within ten seconds. It is accepted before anyone is known, and an unbounded read is a connection any stranger holds for free. - `/v1/relays` answers 503 behind `relay.RELAYS_ENABLED`, as federation does: nothing in the tree calls it and two of its routes take no account. - `test_unauthenticated_surface.py` walks every route and fails on one without an authentication dependency that is not listed with its reason. Verified in Chrome against a local hub: the lockout and wrong-passphrase messages, the admin section saving both lockout and mail limits, and the passphrase change refused while locked. Not verified in Firefox (a running instance blocks the headless one), nor the upsert's concurrency on PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcF3QKWii7uQ2kSyXErzCt
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/admin-page.js51
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/profile-page.js33
14 files changed, 167 insertions, 11 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
index 8800615..c40d240 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
@@ -16,6 +16,7 @@ export function AdminPage({ token, role }) {
// Edited values live here until Save, so a half-typed number is never sent
// and a rejected one never looks applied.
const [mailDraft, setMailDraft] = useState(null);
+ const [loginDraft, setLoginDraft] = useState(null);
const [users, setUsers] = useState([]);
const [usersTotal, setUsersTotal] = useState(0);
const [userSearch, setUserSearch] = useState('');
@@ -43,6 +44,7 @@ export function AdminPage({ token, role }) {
const data = await hubFetch('/v1/admin/settings', { token });
setSettings(data);
setMailDraft({ ...data.mail });
+ setLoginDraft({ ...data.login });
} catch (e) { setError(e.message); }
try {
setMailStatus(await hubFetch('/v1/admin/mail', { token }));
@@ -61,6 +63,7 @@ export function AdminPage({ token, role }) {
// The hub clamps what it was given, so the draft is reset from the
// answer rather than left showing a number that was not stored.
setMailDraft({ ...data.mail });
+ setLoginDraft({ ...data.login });
if (patch.mail) {
try {
setMailStatus(await hubFetch('/v1/admin/mail', { token }));
@@ -193,6 +196,14 @@ const MAIL_FIELDS = [
'email_change_cooldown',
];
+const LOGIN_FIELDS = ['max_failures', 'lockout_minutes'];
+
+// Only what changed, and only what is a number: an empty field is someone
+// mid-edit, not a request to set zero.
+const changedNumbers = (fields, draft, stored) => Object.fromEntries(fields
+ .filter(k => draft[k] !== '' && draft[k] !== null && Number(draft[k]) !== stored[k])
+ .map(k => [k, Number(draft[k])]));
+
const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist'];
const canEditSettings = role === 'admin';
@@ -247,12 +258,7 @@ const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist
<div class="settings-row">
<button class="btn" disabled=${settingsSaving}
onClick=${() => saveSettings({
- mail: Object.fromEntries(MAIL_FIELDS
- // Only what changed, and only what is a number: an empty
- // field is someone mid-edit, not a request to set zero.
- .filter(k => mailDraft[k] !== '' && mailDraft[k] !== null
- && Number(mailDraft[k]) !== settings.mail[k])
- .map(k => [k, Number(mailDraft[k])])),
+ mail: changedNumbers(MAIL_FIELDS, mailDraft, settings.mail),
})}>${t('admin.mail_save')}</button>
<button class="btn btn-secondary" disabled=${settingsSaving}
onClick=${() => setMailDraft({ ...settings.mail_defaults })}
@@ -260,6 +266,37 @@ const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist
</div>
`}
</div>
+
+ ${settings.login && html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('admin.login_heading')}</h3>
+ <p class="settings-hint">${t('admin.login_hint')}</p>
+
+ ${loginDraft && LOGIN_FIELDS.map(key => html`
+ <div class="settings-row" key=${key}>
+ <span class="settings-label">${t('admin.login_' + key)}</span>
+ <input type="number" class="settings-number"
+ min=${(settings.login_bounds?.[key] || [0])[0]}
+ max=${(settings.login_bounds?.[key] || [0, 0])[1]}
+ value=${loginDraft[key]}
+ disabled=${!canEditSettings || settingsSaving}
+ onInput=${e => setLoginDraft(d => ({ ...d, [key]: e.target.value }))} />
+ </div>
+ `)}
+
+ ${canEditSettings && loginDraft && html`
+ <div class="settings-row">
+ <button class="btn" disabled=${settingsSaving}
+ onClick=${() => saveSettings({
+ login: changedNumbers(LOGIN_FIELDS, loginDraft, settings.login),
+ })}>${t('admin.login_save')}</button>
+ <button class="btn btn-secondary" disabled=${settingsSaving}
+ onClick=${() => setLoginDraft({ ...settings.login_defaults })}
+ >${t('admin.mail_reset_defaults')}</button>
+ </div>
+ `}
+ </div>
+ `}
`}
${tab === 'stats' && stats && html`
@@ -436,7 +473,7 @@ const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist
loadLogs(e.target.value, 0);
}}>
<option value="">${t('admin.filter_all')}</option>
- ${['login', 'login_fail', 'account_create', 'token_refresh', 'group_create',
+ ${['login', 'login_fail', 'login_locked', 'account_create', 'token_refresh', 'group_create',
'group_join', 'group_leave', 'node_announce', 'revoke_user', 'revoke_group',
'admin_user_update', 'admin_group_update'].map(ev => html`
<option key=${ev} value=${ev}>${ev}</option>
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 9ded87b..d4dc5a4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -157,6 +157,12 @@ export function LoginPage({ onLogin }) {
} catch (err) {
if (err.message === 'email_verification_required') {
setPendingVerif(true);
+ } else if (err.message === 'account_locked') {
+ setError(t('login.locked', {
+ minutes: Math.max(1, Math.ceil((err.retryAfter || 60) / 60)),
+ }));
+ } else if (err.message === 'Invalid credentials') {
+ setError(t('login.invalid'));
} else {
setError(err.message);
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
index a540a94..918ade3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
@@ -368,7 +368,23 @@ async function loginAndRecover(username, password) {
body: JSON.stringify({ username, auth_key: authKey }),
});
- if (!resp.ok) throw new Error(`Login failed: ${await resp.text()}`);
+ if (!resp.ok) {
+ // The hub's `detail`, not the raw body: the sign-in page matches on it
+ // (`email_verification_required`, `account_locked`), and a message wrapped
+ // as "Login failed: {json}" matched nothing, so neither was ever shown.
+ const body = await resp.text();
+ let detail = body;
+ // `error` is the per-IP rate limiter's field (slowapi), `detail` everyone else's.
+ try {
+ const j = JSON.parse(body);
+ detail = j.detail || j.error || body;
+ } catch { /* not JSON */ }
+ const err = new Error(String(detail));
+ err.status = resp.status;
+ err.retryAfter = Number(resp.headers && resp.headers.get
+ ? resp.headers.get('Retry-After') : 0) || 0;
+ throw err;
+ }
const data = await resp.json();
const result = {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index f460255..9d6fc61 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -87,6 +87,8 @@ export default {
'register.resend_sent': 'Ein neuer Code wurde gesendet.',
'login.pending_verification': 'Ihre E-Mail-Adresse ist noch nicht bestätigt. Bitte prüfen Sie Ihr Postfach auf den Bestätigungscode.',
'login.verify_link': 'E-Mail bestätigen',
+ 'login.invalid': "Benutzername oder Passphrase falsch.",
+ 'login.locked': "Zu viele falsche Passphrasen für dieses Konto. Versuchen Sie es in {minutes} Min. erneut oder setzen Sie Ihre Passphrase zurück.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'Das Internet, wie es gedacht war.',
'welcome.lead': 'MeshBay ist Open-Source-Software, mit der Sie aus der Ferne auf Ihre persönlichen Dateien zugreifen und Anwendungen direkt auf dem Speicher Ihres eigenen Computers betreiben:',
@@ -499,6 +501,11 @@ export default {
'admin.mail_email_change_cooldown': "Sekunden, bevor ein Konto eine andere Adresse vorschlagen darf",
'admin.mail_save': "Mail-Grenzen speichern",
'admin.mail_reset_defaults': "Standardwerte wiederherstellen",
+ 'admin.login_heading': "Anmeldung",
+ 'admin.login_hint': "Nach so vielen falschen Passphrasen für einen Benutzernamen wird die Anmeldung mit Passphrase für die angegebene Dauer verweigert. Bereits offene Sitzungen und registrierte Geräte funktionieren weiter, und ein Zurücksetzen der Passphrase hebt die Sperre auf. 0 schaltet sie ab.",
+ 'admin.login_max_failures': "Falsche Passphrasen bis zur Sperre",
+ 'admin.login_lockout_minutes': "Dauer der Sperre (Minuten)",
+ 'admin.login_save': "Anmeldegrenzen speichern",
'admin.mail_state_is_in_stats': "Der Verbrauch der aktuellen Stunde steht unter Statistik.",
'admin.mail_state_hint': "Rücksetzungen und Einladungen dürfen das ganze Budget nutzen; Registrierungen und Adressänderungen nicht den dafür reservierten Anteil. Administratoren werden einmal pro Stunde benachrichtigt, wenn eine der beiden Grenzen erreicht ist.",
'admin.mail_left_signups': "Rest für Registrierungen",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index e7ffa64..a5dd5d4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -90,6 +90,8 @@ export default {
'register.resend_sent': 'A new code has been sent.',
'login.pending_verification': 'Your email is not yet verified. Please check your inbox for the verification code.',
'login.verify_link': 'Verify email',
+ 'login.invalid': "Wrong username or passphrase.",
+ 'login.locked': "Too many wrong passphrases for this account. Try again in {minutes} min, or reset your passphrase.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'The Internet as it was meant to be.',
'welcome.lead': 'MeshBay is open-source software that gives you remote access to your personal files, and runs applications on top of the storage on your own computer:',
@@ -489,6 +491,11 @@ export default {
'admin.mail_email_change_cooldown': "Seconds before an account may propose another address",
'admin.mail_save': "Save mail limits",
'admin.mail_reset_defaults': "Restore defaults",
+ 'admin.login_heading': "Sign-in",
+ 'admin.login_hint': "After this many wrong passphrases for one username, signing in with a passphrase is refused for the set duration. Sessions already open and registered devices keep working, and a passphrase reset ends the lockout. 0 turns it off.",
+ 'admin.login_max_failures': "Wrong passphrases before a lockout",
+ 'admin.login_lockout_minutes': "Lockout duration (minutes)",
+ 'admin.login_save': "Save sign-in limits",
'admin.mail_state_is_in_stats': "The current hour's usage is shown under Statistics.",
'admin.mail_state_hint': "Resets and invitations may spend the whole budget; sign-ups and address changes may not spend the share reserved for them. Administrators are notified once per hour when either ceiling is reached.",
'admin.mail_left_signups': "Left for sign-ups",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 2641708..6e8e191 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -86,6 +86,8 @@ export default {
'register.resend_sent': 'Se ha enviado un nuevo código.',
'login.pending_verification': 'Su correo electrónico aún no ha sido verificado. Revise su bandeja de entrada para obtener el código de verificación.',
'login.verify_link': 'Verificar correo',
+ 'login.invalid': "Nombre de usuario o frase de contraseña incorrectos.",
+ 'login.locked': "Demasiadas frases de contraseña incorrectas para esta cuenta. Vuelva a intentarlo en {minutes} min o restablezca su frase de contraseña.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'Internet como debió ser.',
'welcome.lead': 'MeshBay es un software de código abierto que le da acceso remoto a sus archivos personales y ejecuta aplicaciones sobre el almacenamiento de su propio ordenador:',
@@ -495,6 +497,11 @@ export default {
'admin.mail_email_change_cooldown': "Segundos antes de que una cuenta pueda proponer otra dirección",
'admin.mail_save': "Guardar límites de correo",
'admin.mail_reset_defaults': "Restaurar valores por defecto",
+ 'admin.login_heading': "Inicio de sesión",
+ 'admin.login_hint': "Tras este número de frases de contraseña incorrectas para un mismo nombre de usuario, se rechaza el inicio de sesión con frase de contraseña durante el tiempo indicado. Las sesiones ya abiertas y los dispositivos registrados siguen funcionando, y restablecer la frase de contraseña levanta el bloqueo. 0 lo desactiva.",
+ 'admin.login_max_failures': "Frases incorrectas antes del bloqueo",
+ 'admin.login_lockout_minutes': "Duración del bloqueo (minutos)",
+ 'admin.login_save': "Guardar límites de inicio de sesión",
'admin.mail_state_is_in_stats': "El uso de la hora actual se muestra en Estadísticas.",
'admin.mail_state_hint': "Los restablecimientos y las invitaciones pueden gastar todo el presupuesto; los registros y cambios de dirección no pueden tocar la parte reservada. Se avisa a los administradores una vez por hora cuando se alcanza cualquiera de los dos límites.",
'admin.mail_left_signups': "Restante para registros",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 0c639b7..47de45f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -86,6 +86,8 @@ export default {
'register.resend_sent': 'Un nouveau code a été envoyé.',
'login.pending_verification': 'Votre e-mail n\'est pas encore vérifié. Consultez votre boîte de réception pour le code de vérification.',
'login.verify_link': 'Vérifier l\'e-mail',
+ 'login.invalid': "Nom d'utilisateur ou phrase secrète incorrect.",
+ 'login.locked': "Trop de phrases secrètes erronées pour ce compte. Réessayez dans {minutes} min, ou réinitialisez votre phrase secrète.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'L’Internet tel qu’il aurait dû être.',
'welcome.lead': 'MeshBay est un logiciel open source qui vous donne accès à distance à vos fichiers personnels et fait tourner des applications sur le stockage de votre ordinateur :',
@@ -498,6 +500,11 @@ export default {
'admin.mail_email_change_cooldown': "Secondes avant qu'un compte puisse proposer une autre adresse",
'admin.mail_save': "Enregistrer les limites",
'admin.mail_reset_defaults': "Rétablir les valeurs par défaut",
+ 'admin.login_heading': "Connexion",
+ 'admin.login_hint': "Après ce nombre de phrases secrètes erronées pour un même nom d'utilisateur, la connexion par phrase secrète est refusée pendant la durée indiquée. Les sessions déjà ouvertes et les appareils enregistrés continuent de fonctionner, et une réinitialisation de la phrase secrète lève le blocage. 0 le désactive.",
+ 'admin.login_max_failures': "Phrases secrètes erronées avant blocage",
+ 'admin.login_lockout_minutes': "Durée du blocage (minutes)",
+ 'admin.login_save': "Enregistrer les limites de connexion",
'admin.mail_state_is_in_stats': "La consommation de l'heure en cours est affichée dans Statistiques.",
'admin.mail_state_hint': "Les réinitialisations et invitations peuvent dépenser tout le budget ; les inscriptions et changements d'adresse ne peuvent pas entamer la part qui leur est réservée. Les administrateurs sont prévenus une fois par heure lorsqu'un des deux plafonds est atteint.",
'admin.mail_left_signups': "Restant pour les inscriptions",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index efda136..660298e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -87,6 +87,8 @@ export default {
'register.resend_sent': 'Un nuovo codice è stato inviato.',
'login.pending_verification': 'Il suo indirizzo e-mail non è ancora verificato. Controlli la sua casella di posta per il codice di verifica.',
'login.verify_link': 'Verifica e-mail',
+ 'login.invalid': "Nome utente o passphrase errati.",
+ 'login.locked': "Troppe passphrase errate per questo account. Riprovi tra {minutes} min oppure reimposti la passphrase.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'Internet come doveva essere.',
'welcome.lead': 'MeshBay è un software open source che le permette di accedere da remoto ai suoi file personali e di usare applicazioni basate sullo spazio di archiviazione del suo computer:',
@@ -498,6 +500,11 @@ export default {
'admin.mail_email_change_cooldown': "Secondi prima che un account possa proporre un altro indirizzo",
'admin.mail_save': "Salva i limiti di posta",
'admin.mail_reset_defaults': "Ripristina i valori predefiniti",
+ 'admin.login_heading': "Accesso",
+ 'admin.login_hint': "Dopo questo numero di passphrase errate per uno stesso nome utente, l'accesso con passphrase viene rifiutato per la durata indicata. Le sessioni già aperte e i dispositivi registrati continuano a funzionare, e la reimpostazione della passphrase rimuove il blocco. 0 lo disattiva.",
+ 'admin.login_max_failures': "Passphrase errate prima del blocco",
+ 'admin.login_lockout_minutes': "Durata del blocco (minuti)",
+ 'admin.login_save': "Salva i limiti di accesso",
'admin.mail_state_is_in_stats': "Il consumo dell'ora corrente è mostrato in Statistiche.",
'admin.mail_state_hint': "Reimpostazioni e inviti possono spendere l'intero budget; registrazioni e cambi di indirizzo non possono intaccare la quota riservata. Gli amministratori vengono avvisati una volta all'ora quando uno dei due limiti viene raggiunto.",
'admin.mail_left_signups': "Rimanente per le registrazioni",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 22f5d6b..ff5b693 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -87,6 +87,8 @@ export default {
'register.resend_sent': '新しいコードを送信しました。',
'login.pending_verification': 'メールアドレスがまだ確認されていません。受信トレイで確認コードをご確認ください。',
'login.verify_link': 'メールを確認',
+ 'login.invalid': "ユーザー名またはパスフレーズが正しくありません。",
+ 'login.locked': "このアカウントでパスフレーズの誤りが多すぎます。{minutes} 分後にもう一度お試しいただくか、パスフレーズをリセットしてください。",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': '本来あるべき姿のインターネット。',
'welcome.lead': 'MeshBay は、個人のファイルにどこからでもアクセスでき、自分のパソコンのストレージ上でアプリを動かせるオープンソースソフトウェアです。',
@@ -491,6 +493,11 @@ export default {
'admin.mail_email_change_cooldown': "別のアドレスを申請できるようになるまでの秒数",
'admin.mail_save': "メール制限を保存",
'admin.mail_reset_defaults': "既定値に戻す",
+ 'admin.login_heading': "サインイン",
+ 'admin.login_hint': "1つのユーザー名に対してこの回数パスフレーズを誤ると、設定した時間のあいだパスフレーズによるサインインが拒否されます。すでに開いているセッションと登録済みの端末は引き続き使え、パスフレーズをリセットするとロックは解除されます。0 で無効になります。",
+ 'admin.login_max_failures': "ロックまでの誤りの回数",
+ 'admin.login_lockout_minutes': "ロック時間(分)",
+ 'admin.login_save': "サインインの制限を保存",
'admin.mail_state_is_in_stats': "現在の 1 時間の使用状況は「統計」に表示されます。",
'admin.mail_state_hint': "再設定と招待は上限全体を使えます。登録とアドレス変更は、確保された分には手を付けられません。いずれかの上限に達すると、管理者に 1 時間に 1 回通知されます。",
'admin.mail_left_signups': "登録に残っている数",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 88aebfa..3c07b3e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -87,6 +87,8 @@ export default {
'register.resend_sent': 'Er is een nieuwe code verzonden.',
'login.pending_verification': 'Uw e-mailadres is nog niet geverifieerd. Controleer uw inbox voor de verificatiecode.',
'login.verify_link': 'E-mail verifiëren',
+ 'login.invalid': "Onjuiste gebruikersnaam of wachtwoordzin.",
+ 'login.locked': "Te veel onjuiste wachtwoordzinnen voor dit account. Probeer het over {minutes} min opnieuw of stel uw wachtwoordzin opnieuw in.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'Het internet zoals het bedoeld was.',
'welcome.lead': 'MeshBay is opensourcesoftware waarmee u op afstand bij uw persoonlijke bestanden kunt, en die toepassingen draait bovenop de opslag van uw eigen computer:',
@@ -499,6 +501,11 @@ export default {
'admin.mail_email_change_cooldown': "Seconden voordat een account een ander adres mag voorstellen",
'admin.mail_save': "E-maillimieten opslaan",
'admin.mail_reset_defaults': "Standaardwaarden herstellen",
+ 'admin.login_heading': "Aanmelden",
+ 'admin.login_hint': "Na dit aantal onjuiste wachtwoordzinnen voor één gebruikersnaam wordt aanmelden met een wachtwoordzin geweigerd gedurende de ingestelde tijd. Al geopende sessies en geregistreerde apparaten blijven werken, en het opnieuw instellen van de wachtwoordzin heft de blokkade op. 0 schakelt dit uit.",
+ 'admin.login_max_failures': "Onjuiste wachtwoordzinnen vóór blokkade",
+ 'admin.login_lockout_minutes': "Duur van de blokkade (minuten)",
+ 'admin.login_save': "Aanmeldlimieten opslaan",
'admin.mail_state_is_in_stats': "Het verbruik van dit uur staat onder Statistieken.",
'admin.mail_state_hint': "Herstel en uitnodigingen mogen het hele budget gebruiken; registraties en adreswijzigingen niet het gereserveerde deel. Beheerders krijgen één keer per uur bericht wanneer een van beide grenzen is bereikt.",
'admin.mail_left_signups': "Resterend voor registraties",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index bed39d2..2ff9b84 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -90,6 +90,8 @@ export default {
'register.resend_sent': 'Nowy kod został wysłany.',
'login.pending_verification': 'Twój adres e-mail nie został jeszcze zweryfikowany. Sprawdź skrzynkę odbiorczą.',
'login.verify_link': 'Zweryfikuj e-mail',
+ 'login.invalid': "Nieprawidłowa nazwa użytkownika lub hasło-fraza.",
+ 'login.locked': "Zbyt wiele błędnych haseł-fraz dla tego konta. Spróbuj ponownie za {minutes} min lub zresetuj hasło-frazę.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'Internet taki, jaki miał być.',
'welcome.lead': 'MeshBay to oprogramowanie open source, które daje zdalny dostęp do Twoich plików osobistych i uruchamia aplikacje korzystające z pamięci Twojego własnego komputera:',
@@ -511,6 +513,11 @@ export default {
'admin.mail_email_change_cooldown': "Sekundy, zanim konto może zaproponować inny adres",
'admin.mail_save': "Zapisz limity poczty",
'admin.mail_reset_defaults': "Przywróć domyślne",
+ 'admin.login_heading': "Logowanie",
+ 'admin.login_hint': "Po tylu błędnych hasłach-frazach dla jednej nazwy użytkownika logowanie hasłem-frazą jest odrzucane przez ustawiony czas. Otwarte już sesje i zarejestrowane urządzenia działają dalej, a zresetowanie hasła-frazy znosi blokadę. 0 ją wyłącza.",
+ 'admin.login_max_failures': "Błędne hasła-frazy przed blokadą",
+ 'admin.login_lockout_minutes': "Czas blokady (minuty)",
+ 'admin.login_save': "Zapisz limity logowania",
'admin.mail_state_is_in_stats': "Zużycie w bieżącej godzinie pokazano w Statystykach.",
'admin.mail_state_hint': "Resety i zaproszenia mogą wykorzystać cały budżet; rejestracje i zmiany adresu nie mogą naruszyć zarezerwowanej części. Administratorzy są powiadamiani raz na godzinę, gdy któryś z limitów zostanie osiągnięty.",
'admin.mail_left_signups': "Pozostało na rejestracje",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index cd5da7d..33fa2be 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -88,6 +88,8 @@ export default {
'register.resend_sent': 'Um novo código foi enviado.',
'login.pending_verification': 'Seu e-mail ainda não foi verificado. Verifique sua caixa de entrada.',
'login.verify_link': 'Verificar e-mail',
+ 'login.invalid': "Nome de usuário ou frase secreta incorretos.",
+ 'login.locked': "Muitas frases secretas incorretas para esta conta. Tente novamente em {minutes} min ou redefina sua frase secreta.",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': 'A internet como deveria ser.',
'welcome.lead': 'O MeshBay é um software de código aberto que dá acesso remoto aos seus arquivos pessoais e executa aplicativos sobre o armazenamento do seu próprio computador:',
@@ -497,6 +499,11 @@ export default {
'admin.mail_email_change_cooldown': "Segundos até uma conta poder propor outro endereço",
'admin.mail_save': "Salvar limites de correio",
'admin.mail_reset_defaults': "Restaurar padrões",
+ 'admin.login_heading': "Login",
+ 'admin.login_hint': "Após este número de frases secretas incorretas para um mesmo nome de usuário, o login com frase secreta é recusado pelo tempo definido. Sessões já abertas e dispositivos registrados continuam funcionando, e redefinir a frase secreta encerra o bloqueio. 0 desativa.",
+ 'admin.login_max_failures': "Frases incorretas antes do bloqueio",
+ 'admin.login_lockout_minutes': "Duração do bloqueio (minutos)",
+ 'admin.login_save': "Salvar limites de login",
'admin.mail_state_is_in_stats': "O consumo da hora atual aparece em Estatísticas.",
'admin.mail_state_hint': "Redefinições e convites podem gastar todo o orçamento; cadastros e trocas de endereço não podem usar a parte reservada. Os administradores são avisados uma vez por hora quando qualquer um dos limites é atingido.",
'admin.mail_left_signups': "Restante para cadastros",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 06aa011..b58ba64 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -87,6 +87,8 @@ export default {
'register.resend_sent': '新验证码已发送。',
'login.pending_verification': '您的邮箱尚未验证。请查看收件箱中的验证码。',
'login.verify_link': '验证邮箱',
+ 'login.invalid': "用户名或密码短语错误。",
+ 'login.locked': "此账户输错密码短语的次数过多。请在 {minutes} 分钟后重试,或重置密码短语。",
// Sign-in page: what MeshBay is (browser only)
'welcome.title': '互联网本该有的样子。',
'welcome.lead': 'MeshBay 是一款开源软件,让您远程访问个人文件,并在您自己电脑的存储之上运行应用:',
@@ -483,6 +485,11 @@ export default {
'admin.mail_email_change_cooldown': "账号可再次申请其他地址前的秒数",
'admin.mail_save': "保存邮件限制",
'admin.mail_reset_defaults': "恢复默认值",
+ 'admin.login_heading': "登录",
+ 'admin.login_hint': "同一用户名输错密码短语达到此次数后,将在设定时长内拒绝使用密码短语登录。已打开的会话和已注册的设备不受影响,重置密码短语即可解除锁定。设为 0 则关闭。",
+ 'admin.login_max_failures': "锁定前允许的错误次数",
+ 'admin.login_lockout_minutes': "锁定时长(分钟)",
+ 'admin.login_save': "保存登录限制",
'admin.mail_state_is_in_stats': "本小时的用量显示在「统计」中。",
'admin.mail_state_hint': "重置与邀请可动用全部额度;注册与更换地址不得占用为前者保留的份额。任一上限达到时,每小时通知管理员一次。",
'admin.mail_left_signups': "注册剩余",
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}>