Skip to content

Host features#2

Open
xossroads wants to merge 5 commits intomainfrom
host-features
Open

Host features#2
xossroads wants to merge 5 commits intomainfrom
host-features

Conversation

@xossroads
Copy link
Copy Markdown
Owner

No description provided.

Plumbing for per-room host: at most one host per room, set by whoever
claims it first with a password. After claim, accessing the host role
for that room requires the password.

"Host" deliberately over "admin" — this is per-room session control,
not site-wide superuser. "Admin" carries the wrong mental model and
would collide with any future site-level admin concept.

Schema: two new tables. room_hosts(room_id PK, password_hash,
claimed_at) holds one row per claimed room; host_sessions(token_hash
PK, room_id FK, expires_at, created_at) holds outstanding session
tokens with cascade delete on the parent host row. Password hashes
are scrypt with a per-row salt, encoded as scrypt:<salt>:<hash>.
Tokens are 32 random bytes; only their SHA-256 lives in the DB so a
DB leak does not hand out live host sessions.

Endpoints (all under /api/rooms/:id/...):
- GET host: public, returns {claimed, available}. available=false
  when the database is down so the client can hide the feature.
- POST claim body {password}: atomic INSERT ... ON CONFLICT DO NOTHING
  decides the join-race; first wins, others get 409. Password 8-72
  chars. On success returns a fresh 30-day session token.
- POST host-login body {password}: verifies and issues a session
  token. Always runs exactly one scrypt against either the row's
  hash or a precomputed dummy so timing doesn't leak whether the
  room has been claimed.

Cleanup job extended to purge expired host sessions on every tick
(no TTL knob — expiration is per-row).

No host actions are wired up yet: this slice only stands up the
identity layer. UI and host-only endpoints are next.
Drives the host claim and login modals against the new server-side
identity endpoints. A small client module (host.ts) handles the API
calls and a localStorage cache of the issued token, keyed per room
under collabjs_host_token_<roomId>.

Initial state on room load:
- DB unavailable → host feature is dark, button hidden, no modal.
- Room not yet claimed → claim modal pops automatically with two
  password fields (entry + confirm). Min 8, max 72 chars. The Skip
  button closes without claiming.
- Room already claimed, no stored token → "🔒 Host" button in the
  header opens the login modal on click.
- Room already claimed, stored token present → "👑 Host" button
  (gold accent) shows we're authenticated. Click logs out (clears
  the token) and the button drops back to the login state so we
  can re-authenticate later.

Race UX: if a concurrent claim wins (server returns 409), the claim
modal closes and the login modal opens with an explanatory message
so the loser can authenticate if they know the password.

Stale tokens fall through silently for now: the assumption is that
once we add host endpoints they'll surface a re-auth prompt on a
401. This slice does not add any host actions yet.

Modal width is capped (max-width 420px) since the new modals carry
explanation text that previously stretched the unbounded layout.
Builds on the claim/login plumbing to give the room host two real
actions — "Log out everywhere" and "Nuke room" — and keeps every
open tab in sync with the server's view of host state via Hocuspocus
stateless broadcasts.

Server
- requireHost middleware: validates Bearer token, looks up the
  session, and confirms the session matches the URL's room.
- POST /api/rooms/:id/host/logout-all: revokes every session for
  the room, including the caller's. Room and password are untouched.
- DELETE /api/rooms/:id: nukes the room. Wipes documents,
  activity_logs, and the host record (cascade-clears sessions) in
  a single transaction. Before the delete, broadcasts a stateless
  "room-nuked" message so other tabs reload and drop their local
  Y.Doc — without that, CRDT auto-sync would push pre-nuke content
  back into the freshly-empty server doc on reconnect.
- roomsBeingNuked guard: short-circuits Database.fetch/store and
  onConnect for any room currently being deleted, so the final
  unloadDocument save can't recreate the row we just deleted and
  reconnects can't grab the doomed room mid-tear-down.
- Single-session policy: claim and host-login revoke every prior
  session for the room before issuing a fresh token. Latest login
  wins; older tabs/devices get 401 the next time they act.
- broadcastHostStateChanged on claim, login, and logout-all so
  other clients reconcile their host UI live.
- GET /api/rooms/:id/host now accepts an optional Bearer token and
  returns `authed`, used by the client to detect tokens that have
  been revoked elsewhere (newer login, logout-everywhere, nuke).
- New helpers: revokeAllHostSessions and a transactional deleteRoom.

Client
- Editor: optional onStateless passthrough so main.ts can listen
  for room-level events (room-nuked, host-state-changed).
- host.ts: nukeRoom, logoutAllHostSessions, parseHostStatelessMessage,
  and an updated fetchHostStatus(roomId, token?) that returns authed.
- main.ts: host menu modal with Log out / Log out everywhere / Nuke
  buttons, plus separate confirm modals for the destructive paths.
  refreshHostState reconciles UI on initial load and on every
  host-state-changed push: drops stale local tokens, swaps the
  claim modal for a login modal when someone else just claimed,
  and closes host-only modals when we lose auth.
- room-nuked handler: clear local token + reload, since the local
  Y.Doc must not be allowed to re-sync into the empty server doc.
- Three new modals in index.html (menu, logout-all confirm, nuke
  confirm) and a .danger button variant in styles.css.

Bundled small fixes
- Race fix: the tab that just won a claim/login could be left
  showing 🔒. The server's own host-state-changed broadcast could
  reach the originating tab before its claim/login HTTP response,
  kicking off a refreshHostState with a null token; when that fetch
  resolved late it clobbered the correct authed state.
  refreshHostState now bails if the local token changed during the
  fetch.
- Stack the host menu buttons vertically (four labels read better
  as a list than a row), and swap the host-state emoji from 👑 to
  🎛️ — control knobs read more like host actions and less like a
  coronation.
Dashboard shell
- Host menu modal becomes a 720px-wide layout: left sidebar with a live
  user list from awareness and the existing actions docked at the
  bottom, right detail pane with a per-user placeholder. Per-user
  stats and contributions land in the next slice.
- Selected user is cleared automatically when they disconnect. The
  awareness listener is only subscribed while the modal is open, and
  torn down on close (including the auth-loss auto-close path).

Destroy lands the group together in a fresh room
- Server picks the successor room ID (UUID), broadcasts it in the
  room-nuked stateless message and returns it in the DELETE response.
  Both paths carry the same value, so the destroyer can navigate even
  if its own broadcast never reaches it and every other tab ends up at
  the same destination.
- Client validates nextRoomId against the room-id regex at every parse
  boundary (response body and stateless payload) before navigating, so
  a malformed value can't redirect us off-app.
- Result types in host.ts split: LogoutAllResult stays {ok:true}|fail;
  NukeResult is {ok:true, nextRoomId}|fail.
- Destroy warning copy updated to call out the redirect, that the host
  moves with everyone else, and that the old URL stops working.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant