aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/admin-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/admin-page.js51
1 files changed, 44 insertions, 7 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>