Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.