Skip to content

feat: change a user's email from the WorkOS Users admin page#33

Merged
redscar merged 10 commits into
mainfrom
feature/workos-users-change-email-action
Jun 30, 2026
Merged

feat: change a user's email from the WorkOS Users admin page#33
redscar merged 10 commits into
mainfrom
feature/workos-users-change-email-action

Conversation

@redscar

@redscar redscar commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

[CONS-553]

Adds a Change email action to the WorkOS Users admin page (admin.php?page=workos-users), alongside the existing "Open in WorkOS" and "Send password reset". An admin can change a user's email and have it flow to WorkOS and mirror into WordPress.

The behavior depends on who's acting: an admin acting on another account commits the change immediately (no verification round-trip), while self-service email changes keep the existing emailed-token verification flow. Since all admin surfaces (users.php row action and the profile panel) hit the same endpoint, they get the immediate behavior too.

Artifact

https://www.loom.com/share/40d99f675e3c4ed198e517fe34951eb6

image

What changed

Backend

  • RestApi.php: extracted the commit logic (WorkOS update → wp_update_user → rollback → webhook race-guard → sync-hash refresh) out of confirm() into a shared commit_change(). ~Half of this file's diff is that extraction (moved, not new).
  • RestApi.php: initiate() branches on is_admin_action() (edit_users && initiator !== target) — admins commit immediately via commit_admin_change(); self-service runs the verified flow. Admins see the real 409 "already in use" conflict; self-service stays enumeration-safe. Forced changes log a distinct email_change.admin_changed event and send no user notification.
  • AdminPage.php: localizes changeEmailUrl + changeEmailEnabled to the React page.
  • Assets.php: flow-agnostic modal strings + a successImmediate string for the shared handler.

Frontend

  • admin-users/index.tsx: native "Change email" button — own modal, immediate POST, in-place row refresh, success/error notice, per-row busy state.
  • admin-change-email/index.ts: response-driven success messaging (immediate vs. pending verification) for the users.php/profile surfaces.

Tests

  • ChangeEmailRestApiTest.php: repointed two initiate tests to self-service actors; added an admin-immediate-commit test and the admin-reveal vs. self-service-enumeration-safe conflict pair.

🤖 Generated with Claude Code

redscar and others added 2 commits June 19, 2026 08:18
The `profile` REST arg registered `sanitize_title` directly as its
sanitize_callback. WordPress invokes sanitize callbacks as
`call_user_func( $cb, $value, $request, $key )`, so the request object
landed in sanitize_title's `$fallback_title` parameter. When the input
sanitizes to an empty string — as it does for the common `profile: ""`
payload — sanitize_title returns the fallback, handing back the
WP_REST_Request object. The subsequent `(string)` cast in send_reset()
then fataled with "Object of class WP_REST_Request could not be
converted to string", surfacing as a 500.

Wrap the sanitizer in a closure so only the value reaches sanitize_title.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Change email" action to admin.php?page=workos-users, alongside
"Open in WorkOS" and "Send password reset".

The change-email REST flow now branches on who is acting. A privileged
admin acting on another account (edit_users && initiator !== target)
commits the change immediately to WorkOS + WordPress, with no token and
no verification email; self-service callers keep the emailed-token
verified flow unchanged. The shared commit logic (WorkOS update, local
mirror, rollback, webhook race-guard, sync-hash refresh) is extracted
from confirm() into commit_change() so both paths share one
implementation.

Admins acting on another account also see the real "already in use"
conflict instead of the enumeration-safe response, since they can
already enumerate accounts; self-service stays enumeration-safe. Forced
admin changes are recorded as a distinct email_change.admin_changed
audit event and send no user notification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@redscar redscar self-assigned this Jun 19, 2026
redscar and others added 3 commits June 19, 2026 13:28
The admin Users page assumed the change-email response was always an
immediate commit, but an admin acting on their *own* account falls into
the verified (emailed-token) flow and returns no `committed` flag. The
page would rewrite the row and report "Email changed" for a change that
hadn't landed yet. Branch on `committed`/`no_op` instead, and only mutate
the row on an actual commit; otherwise report that verification was sent.

Also announce error notices assertively (role="alert") so screen readers
catch a failed action, and add wpunit coverage for the admin-acting-on-
self verified path and the admin-direct commit on a WorkOS-unlinked user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wp_update_user() mails WordPress core's "Notice of Email Change" to the
old address whenever the email changes. The admin-direct path skipped our
own notifier but not this one, so the "silent" admin change wasn't silent
— and the test asserting no mail failed. Suppress it via the
send_email_change_email filter, scoped to the admin commit so the
self-service confirm path keeps its core notice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the initiate() intro comment that restated is_admin_action()'s own
docblock, and tighten the commit_admin_change docblock and the React
discriminator comment. Keep the comments that carry non-obvious why —
the commit_change invariants, the rate-limit bypass, and the WP-core
email-notice suppression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@redscar redscar marked this pull request as ready for review June 22, 2026 17:44
@redscar redscar requested review from bordoni and johnhooks June 22, 2026 17:44

@bordoni bordoni left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I get this to include some documentation and readme updates based on the changes?

Also changelog

@bordoni bordoni added this to the 1.0.8 milestone Jun 30, 2026
redscar and others added 3 commits June 30, 2026 10:12
….com:bordoni/integration-workos into feature/workos-users-change-email-action
Add documentation, README/AGENTS, and changelog coverage for the
WorkOS-Users-page change-email action and the admin-direct
immediate-commit behavior, and bump to 1.0.8.

- change-email.md: new "Admin-direct vs. self-service" matrix; admin
  response shape and 409 on initiate; Users-page surface; admin_changed
  event; drop the removed admin_bypass setting; test count 13 -> 19.
- README.md / AGENTS.md: reflect the new surface, admin-direct path, and
  admin_changed event.
- CHANGELOG.md / readme.txt: 1.0.8 entries for the change-email work
  (#33) and the password-reset empty-profile 500 fix (#32).
- Remove the now-unused change_email_admin_bypass_verification option.
- Bump Version / Stable tag / package.json to 1.0.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
redscar and others added 2 commits June 30, 2026 10:30
PR #32 is folded into #33, so #33 is the PR of record for the
password-reset empty-profile fix in this release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim the change-email and password-reset entries to a lead line plus a
sentence or two, matching the surrounding changelog style; the PR
carries the full detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@redscar redscar requested a review from bordoni June 30, 2026 14:34
@redscar redscar merged commit e2c6028 into main Jun 30, 2026
7 checks passed
@redscar redscar deleted the feature/workos-users-change-email-action branch June 30, 2026 17:37
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.

2 participants