Skip to content

Admin → Users: accept an invited user's invitation with a password the administrator sets - #716

Merged
schmitch merged 1 commit into
mainfrom
schmitch/admin-accept-invitation-653
Oct 31, 2026
Merged

schmitch merged 1 commit into
mainfrom
schmitch/admin-accept-invitation-653

Conversation

@schmitch

Copy link
Copy Markdown
Member

Fixes #653

An invited local account (#322) could sign in only after its user followed the invitation link and chose a password. An administrator who set up an account for someone else could not finish the invitation for them, for example on an install without email or for a user without a mailbox. The account stayed invited, or became invitation expired.

The change

  • POST /admin/users/{id}/invitation/accept with {"password": "…"} accepts the invitation on the user's behalf. One transaction (invitation_dao::accept_for_user) does all of this:
    • the password hash becomes the account's;
    • invited is cleared, so the status is active;
    • the account's invitation, live or expired, is marked accepted, so its link answers as invalid.
  • It works with or without a live invitation: invited, invitation_expired, and a revoked invitation (no row).
  • Password rules: the same as the invitation page (invitations::check_password): at least 12 characters and at most 1024 bytes. Otherwise 400 with the reason.
  • 409 for an account that is not an active invited local one: its user has already chosen a password, an account of another realm, a service account, or a deactivated account. 404 when there is no account. The handler checks eligibility before the argon2 work, and the update checks it again inside its transaction (… AND invited AND active AND auth_source = 'local' AND kind = 'user').
  • Audit: the admin gate's admin change line records the request. A forge::audit line also names the account and the administrator who accepted for it.

Web UI

  • Admin → Users → <user>: the Invitation panel of an invited account gets Accept invitation.
  • The dialog asks for the password twice and checks it against the invitation page's rules before sending anything. Those rules are now shared in invitationPasswordProblem in web/src/api/auth.ts, which /invite uses too.
  • A refusal from the server (for example a 409 because the user chose a password in the meantime) shows inside the dialog, and the dialog stays open.
  • After success, the panel goes away and a notice says the user signs in with the password you set and that the link no longer works. The header badge shows active immediately: the answer replaces the user in the cached list.

Docs

  • docs/deployment.md → Inviting users: the intro now says an administrator sets another person's password only this way. A new bullet describes Accept invitation, and the API paragraph lists the endpoint with its 400/409/404.
  • Changelog fragment: docs/changes/2.2.0/653.feature.md.

Tests

  • crates/server/tests/invitations.rs → an_administrator_accepts_an_invitation_with_a_password_they_set:
    • a too-short and a too-long password get 400, and the account stays invited with its link live;
    • accepting answers the active view. The user signs in with the password, accepted_at is set, and the old link fails both /auth/invitations/lookup and /auth/invitations/accept (and the password sent there does not sign in);
    • a second accept gets 409;
    • accepting works for an expired invitation and for a revoked one;
    • a deactivated invited account gets 409 and is still invited after reactivation;
    • a credential-forwarding account, a service account and the bootstrap admin each get 409;
    • a missing account gets 404.
  • web/src/routes/admin/users/users.test.tsx, two new tests:
    • the dialog's client-side rules (too short, the two entries differ, nothing sent), then a successful accept (body {password}, dialog closed, notice shown, invitation actions gone, status active);
    • a 409 whose reason is shown in the dialog, which stays open.

Checks run

  • cargo test -p forge-server --test invitations: 6 passed (all 5 existing ones plus the new one), against Postgres 18, versitygw and mailpit testcontainers.
  • cargo clippy -p forge-db -p forge-server --lib --test invitations -- -D warnings and cargo fmt --all --check.
  • cargo xtask changelog --check and cargo xtask docs --check.
  • pnpm run tsc; pnpm run test: 536 passed, 40 files.
  • Not run here: the e2e suites and Playwright, and the dialog was not rendered in a real browser. CI runs them.

Notes

  • No migration and no .sqlx change: the new queries use the runtime sqlx::query API, like the rest of invitation_dao.
  • The dialog's Set password button sits in the dialog footer and is tied to the form by form= id. Browsers then submit on Enter in either field. user-event does not simulate that, so the test clicks the button.

Generated by Claude Code

…e administrator sets

An invited local account signed in only once its user had followed the
invitation link and chosen a password, so an administrator who set an
account up for someone without email or a mailbox could not hand it over
ready to use: it stayed invited, or its invitation expired.

POST /admin/users/{id}/invitation/accept with {"password"} now accepts the
invitation on the user's behalf, in one transaction: the password becomes
the account's, the account is no longer invited (status active), and its
invitation, live or expired, is marked accepted, so its link answers as
invalid. It works whether a live invitation waits or not. The password
follows the invitation page's rules (12 characters to 1024 bytes, 400
otherwise); an account that is not an active invited local one (its user
chose a password already, an account of another realm, a service account,
a deactivated account) is refused with 409, and no account is 404. The
change is in the audit trail with the administrator who made it.

Admin → Users → <user> offers Accept invitation on an invited account: a
dialog asks for the password twice, checks it by the invitation page's
rules (now shared with that page), shows the server's refusal, and says
afterwards that the user signs in with the password set. docs/deployment.md
(Inviting users) describes it as the only way an administrator sets
another person's password.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbbDh51swmocfFvXPe
@schmitch
schmitch added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 20bedc6 Oct 31, 2026
19 checks passed
@schmitch
schmitch deleted the schmitch/admin-accept-invitation-653 branch October 31, 2026 12:26
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.

Admin → Users: accept an invited user's invitation for them, with a password the administrator sets

2 participants