diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 70 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/i18n.js | 15 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 13 |
3 files changed, 96 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index d8b83ed..7b1b5fc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -2130,7 +2130,7 @@ function SearchPage() { const THEME_OPTIONS = ['light', 'dark', 'system']; -function SettingsPage({ user, theme, onThemeChange, groups }) { +function SettingsPage({ user, theme, onThemeChange, groups, onLogout }) { const [locale, setLoc] = useState(getLocale); const [muted, setMuted] = useState(() => { try { return JSON.parse(localStorage.getItem('mb_muted') || '{}'); } @@ -2142,6 +2142,29 @@ function SettingsPage({ user, theme, onThemeChange, groups }) { const [nodeKeyLoading, setNodeKeyLoading] = useState(false); const [pinCount, setPinCount] = useState( () => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0)); + const [delOpen, setDelOpen] = useState(false); + const [delPass, setDelPass] = useState(''); + const [delError, setDelError] = useState(''); + const [deleting, setDeleting] = useState(false); + + const deleteAccount = useCallback(async (e) => { + e.preventDefault(); + setDeleting(true); + setDelError(''); + try { + // The passphrase is re-checked by the hub, not merely by this form: an + // open session is not consent to something irreversible. + const authKey = await window.MeshBayKeys.deriveAuthKey(delPass, user.username); + await hubFetch('/v1/users/me', { + method: 'DELETE', token: user.token, body: { auth_key: authKey }, + }); + onLogout(); + } catch (err) { + setDelError(err.message); + } finally { + setDeleting(false); + } + }, [delPass, user]); // 11.5.8: node identity pins are refused strictly on change, so users need a // deliberate way to accept a legitimate rotation (operator reinstalled a node). @@ -2251,6 +2274,33 @@ function SettingsPage({ user, theme, onThemeChange, groups }) { </div> <div class="settings-section"> + <h3 class="settings-heading">${t('settings.danger')}</h3> + <p class="settings-hint">${t('settings.delete_hint')}</p> + ${delError && html`<p class="error-msg">${delError}</p>`} + ${!delOpen + ? html`<button class="btn-danger" onClick=${() => setDelOpen(true)}> + ${t('settings.delete_account')} + </button>` + : html` + <form onSubmit=${deleteAccount}> + <p class="settings-hint">${t('settings.delete_confirm')}</p> + <div style="display:flex;gap:8px;margin-top:8px"> + <input type="password" placeholder=${t('login.password')} + autocomplete="current-password" + value=${delPass} onInput=${e => setDelPass(e.target.value)} required /> + <button class="btn-danger" type="submit" disabled=${deleting}> + ${deleting ? '…' : t('settings.delete_confirm_btn')} + </button> + <button class="btn-secondary" type="button" + onClick=${() => { setDelOpen(false); setDelPass(''); setDelError(''); }}> + ${t('settings.cancel')} + </button> + </div> + </form> + `} + </div> + + <div class="settings-section"> <h3 class="settings-heading">${t('settings.appearance')}</h3> <div class="settings-row"> <span class="settings-label">${t('settings.theme')}</span> @@ -2368,6 +2418,18 @@ function AdminPage({ token }) { else if (tab === 'blocklist') loadBlocklist(); }, [tab]); + const deleteUser = useCallback(async (u) => { + // Suspension is the reversible tool and stays one click away; this one is + // not, so it names the account and says what it cannot reach. + if (!confirm(t('admin.delete_confirm', { user: u.username }))) return; + try { + await hubFetch(`/v1/admin/users/${u.id}`, { method: 'DELETE', token }); + loadUsers(userSearch); + } catch (err) { + alert(err.message); + } + }, [token, userSearch, loadUsers]); + const patchUser = useCallback(async (userId, patch) => { try { await hubFetch(`/v1/admin/users/${userId}`, { method: 'PATCH', body: patch, token }); @@ -2510,6 +2572,10 @@ function AdminPage({ token }) { ? html`<button class="admin-btn" onClick=${() => patchUser(u.id, { status: 'active' })}>${t('admin.btn_unsuspend')}</button>` : null } + ${u.status !== 'deleted' && html` + <button class="admin-btn danger" + onClick=${() => deleteUser(u)}>${t('admin.btn_delete')}</button> + `} </td> </tr> `)} @@ -2806,7 +2872,7 @@ function App() { : html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`; } else if (route === '/settings') { page = html`<${SettingsPage} user=${user} theme=${theme} - onThemeChange=${setTheme} groups=${groups} />`; + onThemeChange=${setTheme} groups=${groups} onLogout=${authCtx.logout} />`; } else { page = html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index c19411b..1b03375 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -132,6 +132,16 @@ const en = { 'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.", 'settings.node_pins_count': '{n} pinned', 'settings.node_pins_clear': 'Clear pinned identities', + 'settings.danger': 'Delete account', + 'settings.delete_hint': 'Removes your account, your group memberships and your ' + + 'notifications from the hub, and frees your username. It cannot reach what ' + + 'lives on nodes: files you uploaded stay where their operator keeps them, ' + + 'and each node keeps the identity it pinned for you until its operator ' + + 'unpins it. Connection logs are kept for a year, as the law requires.', + 'settings.delete_account': 'Delete my account', + 'settings.delete_confirm': 'Enter your passphrase to confirm. This cannot be undone.', + 'settings.delete_confirm_btn': 'Delete for good', + 'settings.cancel': 'Cancel', 'settings.appearance': 'Appearance', 'settings.theme': 'Theme', 'settings.theme_light': 'Light', @@ -190,6 +200,11 @@ const en = { 'admin.btn_suspend': 'Suspend', 'admin.btn_unsuspend': 'Unsuspend', 'admin.btn_details': 'Details', + 'admin.btn_delete': 'Delete', + 'admin.delete_confirm': 'Delete the account "{user}"? This cannot be undone. ' + + 'Files they uploaded stay on the nodes that host them, and each node keeps ' + + 'the identity it pinned until its operator unpins it. Suspending is the ' + + 'reversible option.', 'admin.user_detail': 'User details', 'admin.btn_close': 'Close', 'admin.self_note': '(you)', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 361ff7e..d948dc2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -1420,3 +1420,16 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } border-radius: 8px; background: var(--bg-surface); } + +/* Irreversible actions look like it. */ +.btn-danger { + background: var(--error); + color: #fff; + border: none; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 0.9em; +} +.btn-danger:hover { filter: brightness(1.1); } +.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; } |