fix(enclave): live typeahead for invite member #90
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/invite-member-search"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
usernameat/enclave/{id}/invite; mistyped names returned a full-page 400 BadRequest and there was no preview of who you were about to invite.GET /enclave/{id}/invite/search?q=...returns a popover of matching users with per-row state (Invitable / Member / Invited / you). Each row has an Invite button thathx-posts the user's id and swaps itself into an inline pill.POST /enclave/{id}/invitenow takesuser_idand returns a small inline result fragment instead of redirecting. Unique-constraint race resolves into the same "Invited" pill rather than a 500.db::enclave::pending_invitee_ids_for_enclavebulk-resolves outstanding-invitation state.Test plan
just checkcleanEnclaveInvitationCreatedWS event🤖 Generated with Claude Code
The enclave Members panel used a bare HTML form that POST'd a typed-in username at /enclave/{id}/invite; a mistyped name returned 400 BadRequest as a full HTML page, and there was no preview of who you were about to invite. Mirrors the people-search pattern wired into the sidebar: - GET /enclave/{id}/invite/search?q=... returns a popover fragment of matching users (substring on username + display_name, debounced 200ms). Each row carries per-row state - Invitable, AlreadyMember, AlreadyInvited, Self - so the template renders an Invite button, a "Member" pill, an "Invited" pill, or "(you)" without per-row queries. - POST /enclave/{id}/invite now takes a `user_id` (not free-form `username`) and replies with a small inline outerHTML fragment that hx-swaps the candidate row into a green "Invited" pill on success or a red error on failure. The unique-constraint race on (enclave_id, invitee_id) is resolved into the same "Invited" pill rather than bubbling 500. - The Members panel's old `<form method="post">` is replaced with the typeahead input + result popover; the typeahead reuses the same `hx-trigger="input changed delay:200ms, keyup[key=='Enter']"` cadence the sidebar's people search uses. - New db::enclave::pending_invitee_ids_for_enclave bulk-resolves outstanding-invitation state for the candidate list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Review of the previous commit caught that `post_invite` accepted any user_id and consulted only `find_user_by_id`, while the search step ran the visibility/block-aware `search_users`. A hand-crafted POST could therefore invite a private-profile, banned, or mutually-blocked user that the typeahead would never have surfaced. Mirror the same filters inside `post_invite` and return a generic "User not found." for all three so the response cannot be used to probe block state or enumerate private profiles. Also: - Replace the `hx-vals='{"user_id":"{{ r.id }}"}'` inline JSON literal with a real `<form>` + hidden input. Askama auto-escapes attribute values, which protects this regardless of how `r.id` is shaped in the future. UUID ids are fine today, but the form pattern removes the latent risk of an id ever containing a single quote, double quote, backslash, or angle bracket. - Add `idx_enclave_invitations_enclave` so the typeahead's debounced 200 ms keystroke poll of `pending_invitee_ids_for_enclave` is O(pending invitations for that enclave) instead of a full-table scan of `enclave_invitations`. The existing index in migration 0009 is on `invitee_id` only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>