diff options
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.js | 92 |
1 files changed, 91 insertions, 1 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 8cbfb5a..9c32475 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js @@ -28,6 +28,8 @@ export function AdminPage({ token, role }) { const [logEvent, setLogEvent] = useState(''); const [logOffset, setLogOffset] = useState(0); const [blocklist, setBlocklist] = useState([]); + const [reports, setReports] = useState([]); + const [reportsDraft, setReportsDraft] = useState(null); const [nodes, setNodes] = useState([]); const [detailUser, setDetailUser] = useState(null); const [error, setError] = useState(''); @@ -48,6 +50,7 @@ export function AdminPage({ token, role }) { setMailDraft({ ...data.mail }); setLoginDraft({ ...data.login }); setSessionDraft({ ...data.session }); + setReportsDraft({ ...data.reports }); } catch (e) { setError(e.message); } try { setMailStatus(await hubFetch('/v1/admin/mail', { token })); @@ -68,6 +71,7 @@ export function AdminPage({ token, role }) { setMailDraft({ ...data.mail }); setLoginDraft({ ...data.login }); setSessionDraft({ ...data.session }); + setReportsDraft({ ...data.reports }); if (patch.mail) { try { setMailStatus(await hubFetch('/v1/admin/mail', { token })); @@ -102,6 +106,23 @@ export function AdminPage({ token, role }) { } catch (e) { setError(e.message); } }, [token]); + const loadReports = useCallback(async () => { + try { + const data = await hubFetch('/v1/admin/reports', { token }); + setReports(data.reports); + } catch (e) { setError(e.message); } + }, [token]); + + // Block or dismiss one reported hash. Blocking names it on the list every + // node hosting a public group applies; dismissing closes it for good. + const decideReport = useCallback(async (hash, verdict) => { + if (verdict === 'block' && !await ask(t('admin.report_block_confirm'))) return; + try { + await hubFetch(`/v1/admin/reports/${hash}/${verdict}`, { method: 'POST', token }); + loadReports(); + } catch (e) { setError(e.message); } + }, [token]); + const loadBlocklist = useCallback(async () => { try { const data = await hubFetch('/v1/admin/blocklist', { token }); @@ -124,6 +145,7 @@ export function AdminPage({ token, role }) { .then(d => setNodes(d.nodes || [])).catch(e => setError(e.message)); } else if (tab === 'logs') { setLogOffset(0); loadLogs(logEvent, 0); } + else if (tab === 'reports') loadReports(); else if (tab === 'blocklist') loadBlocklist(); }, [tab]); @@ -205,13 +227,16 @@ const LOGIN_FIELDS = ['max_failures', 'lockout_minutes']; const SESSION_FIELDS = ['browser_idle_hours', 'refresh_idle_hours', 'max_hours']; +const REPORT_FIELDS = ['min_account_age_hours', 'daily_per_account', 'review_threshold', + 'auto_block']; + // 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 TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'reports', 'blocklist']; const canEditSettings = role === 'admin'; return html` @@ -304,6 +329,37 @@ const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist `} </div> + ${settings.reports && html` + <div class="settings-section"> + <h3 class="settings-heading">${t('admin.reports_heading')}</h3> + <p class="settings-hint">${t('admin.reports_hint')}</p> + + ${reportsDraft && REPORT_FIELDS.map(key => html` + <div class="settings-row" key=${key}> + <span class="settings-label">${t('admin.reports_' + key)}</span> + <input type="number" class="settings-number" + min=${(settings.reports_bounds?.[key] || [0])[0]} + max=${(settings.reports_bounds?.[key] || [0, 0])[1]} + value=${reportsDraft[key]} + disabled=${!canEditSettings || settingsSaving} + onInput=${e => setReportsDraft(d => ({ ...d, [key]: e.target.value }))} /> + </div> + `)} + + ${canEditSettings && reportsDraft && html` + <div class="settings-row"> + <button class="btn" disabled=${settingsSaving} + onClick=${() => saveSettings({ + reports: changedNumbers(REPORT_FIELDS, reportsDraft, settings.reports), + })}>${t('admin.reports_save')}</button> + <button class="btn btn-secondary" disabled=${settingsSaving} + onClick=${() => setReportsDraft({ ...settings.reports_defaults })} + >${t('admin.mail_reset_defaults')}</button> + </div> + `} + </div> + `} + <div class="settings-section"> <h3 class="settings-heading">${t('admin.session_heading')}</h3> <p class="settings-hint">${t('admin.session_hint')}</p> @@ -546,6 +602,40 @@ const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist `} `} + ${tab === 'reports' && html` + <table class="admin-table"> + <thead><tr> + <th>${t('admin.col_hash')}</th> + <th>${t('admin.col_reporters')}</th> + <th>${t('admin.col_reason')}</th> + <th>${t('admin.col_groups')}</th> + <th>${t('admin.col_date')}</th> + <th>${t('admin.col_actions')}</th> + </tr></thead> + <tbody> + ${reports.length === 0 && html`<tr><td colspan="6" class="admin-empty">${t('admin.no_reports')}</td></tr>`} + ${reports.map(r => html` + <tr key=${r.hash}> + <td style="font-family:monospace;font-size:0.8em">${r.hash.slice(0, 16)}...</td> + <td>${r.reporters}</td> + <td> + ${Object.entries(r.reasons).map(([k, n]) => `${t('report.reason_' + k)} (${n})`).join(', ')} + ${r.details.map(d => html`<div class="settings-hint">${d}</div>`)} + </td> + <td>${r.groups.map(g => g.name || g.id.slice(0, 8)).join(', ')}</td> + <td>${new Date(r.opened_at).toLocaleDateString()}</td> + <td> + ${role === 'admin' && html` + <button class="admin-btn" onClick=${() => decideReport(r.hash, 'block')}>${t('admin.btn_block')}</button> + <button class="admin-btn" onClick=${() => decideReport(r.hash, 'dismiss')}>${t('admin.btn_dismiss')}</button> + `} + </td> + </tr> + `)} + </tbody> + </table> + `} + ${tab === 'blocklist' && html` <${BlocklistForm} onAdd=${addToBlocklist} /> <table class="admin-table"> |