fix(enclave): live typeahead for invite member #90

Merged
nrupard merged 2 commits from fix/invite-member-search into main 2026-05-13 22:15:43 +02:00
Owner

Summary

  • Enclave Members panel previously POST'd a free-form username at /enclave/{id}/invite; mistyped names returned a full-page 400 BadRequest and there was no preview of who you were about to invite.
  • Now mirrors the sidebar people-search pattern: 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 that hx-posts the user's id and swaps itself into an inline pill.
  • POST /enclave/{id}/invite now takes user_id and returns a small inline result fragment instead of redirecting. Unique-constraint race resolves into the same "Invited" pill rather than a 500.
  • New db::enclave::pending_invitee_ids_for_enclave bulk-resolves outstanding-invitation state.

Test plan

  • just check clean
  • type a partial username in the Members panel; preview rows render
  • click Invite on a row; row swaps into green "Invited" pill, target user gets EnclaveInvitationCreated WS event
  • click Invite a second time on the same candidate (race); row still shows "Invited" (no 500)
  • try inviting yourself; row shows "(you)" and no Invite button
  • type a username that does not exist; popover collapses (or shows "No matching people.")

🤖 Generated with Claude Code

## Summary - Enclave Members panel previously POST'd a free-form `username` at `/enclave/{id}/invite`; mistyped names returned a full-page 400 BadRequest and there was no preview of who you were about to invite. - Now mirrors the sidebar people-search pattern: `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 that `hx-post`s the user's id and swaps itself into an inline pill. - `POST /enclave/{id}/invite` now takes `user_id` and returns a small inline result fragment instead of redirecting. Unique-constraint race resolves into the same "Invited" pill rather than a 500. - New `db::enclave::pending_invitee_ids_for_enclave` bulk-resolves outstanding-invitation state. ## Test plan - [x] `just check` clean - [ ] type a partial username in the Members panel; preview rows render - [ ] click Invite on a row; row swaps into green "Invited" pill, target user gets `EnclaveInvitationCreated` WS event - [ ] click Invite a second time on the same candidate (race); row still shows "Invited" (no 500) - [ ] try inviting yourself; row shows "(you)" and no Invite button - [ ] type a username that does not exist; popover collapses (or shows "No matching people.") 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(enclave): replace invite-by-username form with live typeahead
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 12s
1fb2c3bd76
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>
fix(enclave): enforce visibility/block/ban gate on invite POST
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 12s
5b6c0c846b
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>
nrupard deleted branch fix/invite-member-search 2026-05-13 22:15:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!90
No description provided.