Files
meshcore-analyzer/docs/user-guide/accounts.md
T
efitenandClaude Opus 5.5 f3ae8ac25a feat: sync a logged-in user's settings across devices (part B) (#2130)
Part B of #2128: a logged-in user's settings follow them across devices.
Log in on a phone and your own nodes, favorites, customizer and filters
are there; a change on one device reaches the others within a minute or
when you return to the tab.

**This PR builds on #2129.** Until that one is merged, the diff here
includes it. The commits for this part start at `docs(specs): settings
sync for optional user management (sub-project B)`.

## The situation

Everything a visitor sets up lives in one browser's `localStorage`
(about 100 keys in `public/`). A second device or a cleared cache starts
from zero (#895).

## What this PR adds

**Storage.** `users.db` schema v2: one JSON document per user in
`user_settings`, with a revision number and a generation id. A write
succeeds only when the client's revision and generation match the stored
ones, so two devices cannot overwrite each other silently.

**Server.** `GET`, `PUT` and `DELETE /api/account/settings`, behind the
same session and CSRF checks as the account routes.
- The server owns the list of synced keys (61 keys,
[`settings_allowlist.go`](https://github.com/efiten/CoreScope/blob/feat/settings-sync/cmd/server/settings_allowlist.go))
and sends it to the client, so the two cannot drift.
- A hard denylist, checked first, refuses `meshcore-api-key`, every
`corescope_channel_*` key and `live-channel-colors` (#725). The colour
map is keyed by channel hash, and for a user-added channel that hash is
`user:<name>`, which would expose hashtag channel names.
- Documents are capped at 256 KiB, measured like `JSON.stringify`. PUT
is limited to 60 requests per hour per user. A stale revision gets 409
with the current document.

**Client**
([`settings-sync.js`](https://github.com/efiten/CoreScope/blob/feat/settings-sync/public/settings-sync.js)).
Inert unless the feature is on and someone is logged in.
- It wraps `localStorage.setItem` and `removeItem` for allowlisted keys
only and pushes 2 seconds after the last change.
- It pulls on login, page load, tab focus and every 60 seconds while the
tab is visible.
- **Merge:** three-way, against a per-device baseline that belongs to
one user and one document generation. Lists (own nodes, favorites, saved
filters) merge per item, so an item added anywhere is kept and an item
removed on one device does not come back from another. Single values:
the profile wins unless only this device changed it.
- Remote changes are written without a push, theme and colour-blind
preset are re-applied, and the current page re-renders (skipped on
account pages and while the geofilter editor is open).

**UI.**
- Logout asks: keep my settings on this device (default), remove them
from this device, or cancel. Channel keys are never removed: no copy
exists anywhere else.
- The account page gets a "Settings sync" section: last synced time,
"Sync now", what is and is not synced, and "Delete synced settings from
my account".

## Not synced

Layout and device state (panel and column widths, collapsed panels, map
positions, geofilter drafts), channel data (#725), the API key, and all
`sessionStorage`. The full list is in the
[spec](https://github.com/efiten/CoreScope/blob/feat/settings-sync/docs/specs/2026-10-06-user-settings-sync-design.md).

## Performance

- One GET per page load, tab focus and minute while visible; one
debounced PUT per burst of changes.
- The `setItem` wrapper costs one Set lookup per write for non-synced
keys. A synced write reads one small revision key, not the stored
document.
- The server reads or writes one row per request.

## Verification

- `internal/users` and `cmd/server`: `go vet` and `go test` pass locally
(22 new Go tests), including a test that every allowlisted key still
occurs in `public/`, and denylist tests.
- `tests/unit/test-settings-sync.js`: 79 passing (vm, real module). The
cases cover the merge table, two tabs sharing one storage, stale answers
after a push, delete while a push is in flight, and logout while the
final push fails.
- `sh test-all.sh` exits 0.
- `tests/e2e/test-user-management-e2e.js` (10 steps, 4 of them new)
passed locally with two browser contexts as two devices: a favorite and
the packet time window travel from device 1 to device 2, a removal does
not come back, and "remove from this device" clears the synced keys
while a channel key stays.
- Checked by hand on a staging instance with a desktop and a phone on
one account.

## Not in this PR

- On a shared browser where the previous user chose "keep", the next
user's first login merges those settings into their own account. The
user guide says to choose "remove" on shared computers.
- Saved filter expressions are synced as typed, including any channel
names written in them. The guide says so.
- Realtime push between devices; the minute pull is the sync interval.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 11:20:44 +02:00

6.1 KiB

Accounts (optional user management)

CoreScope runs without accounts by default: every page is public and settings live in each visitor's browser. Operators can turn on accounts. The dashboard stays public; logging in only adds things.

  • Visitors can register with an email address, a display name and a password, and activate the account through a link mailed to that address.
  • Admins manage users (activate, disable, delete, promote) and can use the operator actions (geofilter save, prune, backup, perf reset) without the API key.

With the feature off (the default) the account routes are not registered: requests to /api/auth/*, /api/account/* and /api/admin/users/* fall through to the normal page, and no login or account control appears in the interface.

For operators

1. Get a Brevo account

  1. Create a free account at brevo.com. The free plan's daily volume is far more than account mails need.
  2. Verify the sender address or domain you will send from (Brevo: Senders, Domains & Dedicated IPs). Mail from an unverified sender is rejected or lands in spam.
  3. Create an API key (SMTP & API, API Keys).

2. Configure

Add to config.json (see config.example.json for every key):

"userManagement": {
  "enabled": true,
  "adminEmails": ["you@example.org"],
  "publicBaseUrl": "https://corescope.example.org",
  "mail": { "fromEmail": "noreply@example.org", "fromName": "My CoreScope" }
}

Provide the API key as mail.brevoApiKey or, better, the environment variable CORESCOPE_BREVO_API_KEY. Restart the server. It refuses to start if the mail setup is incomplete, and the log says what is missing.

Key Meaning
adminEmails Addresses that become admin on activation. They cannot be demoted, disabled or deleted from the UI. Remove an address here first.
publicBaseUrl The address visitors use. Every mail link is built from it, never from the request. It must match the browser origin, because state-changing requests from another origin are refused.
dbPath Where accounts are stored. Default: users.db next to the analyzer database.
sessionDays Login lifetime, extended while in use. Default 30, maximum 365.
trustedProxies CIDRs of your reverse proxy, so the per-IP login limits see real client IPs. Without it, behind a proxy every client shares the proxy's IP for the per-IP limits (they are switched off when that IP is loopback or private). Per-address and per-account limits apply either way.
mail.webhookSecret Enables delivery status (below). At least 16 characters.

3. The first admin

Register at #/account/register with an address from adminEmails, click the activation link and enter the password you chose. You are now admin and can promote others in Users.

4. Delivery status (optional)

To see per user whether mails were delivered, bounced, blocked or marked as spam:

  1. Set mail.webhookSecret (16+ random characters), or CORESCOPE_BREVO_WEBHOOK_SECRET. The webhook route exists only when a secret is set.
  2. In Brevo, create a transactional webhook to https://<publicBaseUrl>/api/mail/brevo/webhook with these events: delivered, opened, clicked, soft bounce, hard bounce, invalid email, deferred, spam, blocked, error. Use bearer authentication with the same secret.

If Brevo cannot reach your instance, use Refresh status in a user's details instead: CoreScope then asks Brevo directly. "Opened" is indicative only. Some mail apps load tracking pixels automatically, and others block them.

When mail fails

  • In Users, Resend mail sends a fresh activation link to a pending account.
  • Activate activates a pending account by hand. The address is then unverified; the row shows "manual" and the audit log records who did it.

Backups

users.db holds password hashes and addresses. Back it up together with the analyzer database, and protect it the same way. Deleting it removes all accounts and nothing else.

For users

  • Register: Log in, Create an account, then click the link in the mail within 48 hours and enter your password to finish. Registering a pending (not yet activated) address again replaces the password and sends a new link. Registering an address that is already activated changes nothing; its owner gets a notice mail instead.
  • Forgot password: Log in, Forgot password? The link works once, for one hour, and logs out all your devices.
  • My account: change your display name, password or address, see your logged-in devices, or delete your account. A new address is confirmed from a link sent to it, and your old address gets a notice. Changing your password logs out your other devices.
  • Settings sync: while you are logged in, your settings follow you: your nodes, favorites, theme and customizer settings, saved packet filters, and the filter, sort and view choices of each page. Log in on another browser or phone and they are restored; later changes reach your other devices within about a minute, or when you return to the tab. A node or favorite added on one device is never dropped by another, and one you removed stays removed. Saved packet filters are stored in your account as you typed them.
  • Never synced: channel keys and decrypted messages, the API key, panel and column sizes, collapsed panels and map positions. They stay in the browser where you set them.
  • Logging out asks whether to keep your settings on this device (the default) or remove them; your account keeps its copy either way, and channel keys are never removed. An automatic logout (expired session) keeps everything on the device. On a shared computer choose Remove my settings from this device: settings kept on the device are added to the account of the next person who logs in there.
  • My account, Settings sync shows when your settings were last synced, has Sync now, and Delete synced settings from my account, which removes the account's copy only. The settings on your devices stay, and your next change starts a new copy.

Admins can see your display name and email address in the Users list.