aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js70
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css13
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; }