Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ Per-environment constants (take priority over generic):
| `src/WorkOS/Admin/OnboardingAjax.php` | Onboarding wizard AJAX handlers |
| **Admin — Users** | |
| `src/WorkOS/Admin/Users/Controller.php` | Wires the WorkOS Users admin submenu + REST endpoint |
| `src/WorkOS/Admin/Users/AdminPage.php` | Admin submenu (WorkOS → Users) that mounts the React user list and enqueues the shared `workos-admin-password-reset` JS/CSS so the per-row trigger button works. |
| `src/WorkOS/Admin/Users/AdminPage.php` | Admin submenu (WorkOS → Users) that mounts the React user list and enqueues the shared `workos-admin-password-reset` JS/CSS so the per-row trigger button works. Also localizes `changeEmailUrl` + `changeEmailEnabled` for the React-native "Change email" action (handled in-bundle, so it does *not* enqueue the shared change-email handler). |
| `src/WorkOS/Admin/Users/RestApi.php` | `GET /wp-json/workos/v1/admin/users` — proxies `Api\Client::list_users()` with sanitized pagination + filters, a server-computed `dashboard_url` per row, and a `wp_user_id` resolved via `_workos_user_id` meta so the React side can show the "Send password reset" trigger only for linked rows. |
| `src/js/admin-users/index.tsx` | React user list (search + cursor pagination + Open in WorkOS deep-link + per-row Send-password-reset button when `wp_user_id > 0`) |
| `src/js/admin-users/index.tsx` | React user list (search + cursor pagination + Open in WorkOS deep-link + per-row Send-password-reset button when `wp_user_id > 0` + native "Change email" button with its own modal, immediate POST, and in-place row refresh) |
| **Admin — Login Profiles (Custom AuthKit)** | |
| `src/WorkOS/Admin/LoginProfiles/Controller.php` | Wires Login Profile admin page + CRUD REST |
| `src/WorkOS/Admin/LoginProfiles/AdminPage.php` | Admin submenu that mounts the React editor |
Expand All @@ -106,7 +106,7 @@ Per-environment constants (take priority over generic):
| `src/js/admin-password-reset/index.ts` | Delegated `.workos-pwreset-trigger` click handler — POSTs to the admin endpoint and surfaces a transient admin notice. Same handler powers every trigger surface (WP Users row, user-edit panel, shortcode, WorkOS Users admin page row). |
| **Auth — Change Email** ([`docs/change-email.md`](docs/change-email.md)) | |
| `src/WorkOS/Auth/ChangeEmail/Controller.php` | DI controller — wires REST endpoints, JS/CSS, row action, profile panel, shortcode, and the frontend confirm route. Honors the `change_email_enabled` setting + `workos_change_email_enabled` filter. |
| `src/WorkOS/Auth/ChangeEmail/RestApi.php` | Three endpoints: `POST /workos/v1/users/{id}/email-change` (initiate, `edit_user` cap, per-IP + per-user rate limits, enumeration-safe conflict response), `…/email-change/confirm` (token-gated, race re-check, transient-guarded WorkOS + WP commit), `…/email-change/cancel` (cancel-token *or* `edit_user` cap). Logs `email_change.*` events. |
| `src/WorkOS/Auth/ChangeEmail/RestApi.php` | Three endpoints: `POST /workos/v1/users/{id}/email-change` (initiate, `edit_user` cap), `…/email-change/confirm` (token-gated, race re-check), `…/email-change/cancel` (cancel-token *or* `edit_user` cap). Initiate branches on `is_admin_action()` (`edit_users` + initiator ≠ target): self-service is rate-limited, enumeration-safe, and verified via emailed token; admin-of-other commits immediately (no rate limit, no notification, WP core email-change notice suppressed), gets a real `409` conflict, and logs `email_change.admin_changed`. Shared `commit_change()` (WorkOS `update_user` → `wp_update_user` → rollback → transient → sync-hash refresh) backs both the confirm and admin-direct paths. Logs `email_change.*` events. |
| `src/WorkOS/Auth/ChangeEmail/PendingChange.php` | Stores the pending record as `_workos_pending_email_change` user_meta — only hashes (HMAC-SHA256 + `wp_salt('auth')`), never plaintext tokens. Single-use: cleared on confirm/cancel/expiry. |
| `src/WorkOS/Auth/ChangeEmail/TokenFactory.php` | `wp_generate_password()`-backed token generator + `hash_hmac()` storage + `hash_equals()` verify. |
| `src/WorkOS/Auth/ChangeEmail/ConflictResolver.php` | Enforces `change_email_conflict_policy`: `block` (default), `allow_orphan` (gated by `_workos_user_id`, posts, comments, last-login window — defaults to 90 days, filterable via `workos_change_email_orphan_max_inactive_days`), `merge_request` (rejects + fires `workos_change_email_merge_requested` for Issue 2). Fires `workos_change_email_conflict_detected` on any collision. |
Expand All @@ -116,7 +116,7 @@ Per-environment constants (take priority over generic):
| `src/WorkOS/Auth/ChangeEmail/Shortcode.php` | `[workos:change-email]` — self-service form when no `user` attribute; admin-of-other form when `user="id-or-email"`. |
| `src/WorkOS/Auth/ChangeEmail/FrontendConfirmRoute.php` | Rewrite rule for the configurable confirm path (default `/workos/change-email/`, settable via `change_email_confirm_path`). `template_redirect` renders `templates/change-email/confirm-page.php`, which enqueues the JS bundle that POSTs to the confirm/cancel endpoint. |
| `src/WorkOS/Auth/ChangeEmail/Assets.php` | Registers `workos-admin-change-email` (admin row/panel/shortcode trigger) + `workos-change-email-confirm` (frontend confirm page) JS/CSS handles. |
| `src/js/admin-change-email/index.ts` | Delegated `.workos-change-email-trigger` click handler — prompts for the new email (standalone button) or reads from `.workos-change-email-input` (shortcode form), POSTs to the initiate endpoint. |
| `src/js/admin-change-email/index.ts` | Delegated `.workos-change-email-trigger` click handler — prompts for the new email (standalone button) or reads from `.workos-change-email-input` (shortcode form), POSTs to the initiate endpoint. Success messaging is response-driven: an immediate admin commit (`committed: true`) shows "Email changed", a self-service initiate shows "verification sent". |
| `src/js/change-email-confirm/index.ts` | Frontend confirm-page logic: reads token + user_id from URL, picks confirm vs cancel based on `?action=cancel`, POSTs, renders success / error. |
| `src/WorkOS/Email/Mailer.php` | Small `wp_mail()` wrapper shared by the change-email Notifier — locates templates (with theme override at `wp-content/themes/{theme}/integration-workos/{template}.php`), sets HTML headers + a filterable from-address, and exposes `workos_email_body`, `workos_email_subject`, `workos_email_headers` filters. |
| `templates/change-email/*.php` | Email + confirm-page templates (verification, old-address notice, confirmation notice, confirm page). |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [1.0.8] - 2026-06-30

### Added

- **Change email from the WorkOS Users page** (#33) — adds a "Change email" action to the WorkOS → Users admin page, alongside "Open in WorkOS" and "Send password reset". When a privileged user (`edit_users`) changes *another* account's email it now commits immediately, skipping the emailed verification step that self-service still uses; the admin path also skips rate limiting, sends no notification, surfaces real conflicts, and logs `email_change.admin_changed`. Removes the now-unused `change_email_admin_bypass_verification` option.

### Fixed

- **500 in admin password-reset when `profile` is empty** (#33) — an empty `profile` (the common case) fataled the endpoint instead of resolving to the default login profile. The `profile` sanitize callback no longer hands the request object back as the sanitized value.

## [1.0.7] - 2026-06-23

### Fixed
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Enterprise identity management for WordPress powered by [WorkOS](https://workos.
- **Sign-in methods**: email + password, magic code, social OAuth (Google, Microsoft, GitHub, Apple), passkey
- **In-app flows**: self-serve sign-up with email verification, invitation acceptance, password reset (two-field new-password form with `wp.passwordStrength.meter` gating, optional auto-login on success, post-reset `redirect_url` validated same-host)
- **Admin-triggered password reset** — send a WorkOS reset email on behalf of any linked user via `POST /wp-json/workos/v1/admin/users/{id}/password-reset` (gated by `edit_user($id)` so the same route covers self-service). Surfaced as a row action under the **WorkOS column** on `wp-admin/users.php`, a button on the user-edit screen, a per-row button on the WorkOS Users admin page, and a `[workos:password-reset]` shortcode. Successful sends are audited as `password_reset.admin_sent` in the activity log; emails point at the in-site React shell (`/workos/login/{slug}?token=…&redirect_to=…`) and the new password is mirrored to the linked WP user so `?fallback=1` / `wp_authenticate` / REST app passwords stay in sync. See [`docs/password-reset.md`](docs/password-reset.md).
- **Change email** — WorkOS-verified, conflict-guarded email-change flow. Self-service via `[workos:change-email]` shortcode; admin via a row action under the **WorkOS column** on `wp-admin/users.php` and a panel on the user-edit screen. WP-side hashed token sent to the new address; cancel-link notice sent to the old address; configurable conflict policy (`block`, `allow_orphan`, `merge_request`) keeps the new email from silently overwriting another local WP user. Confirm commits to WorkOS first (via `update_user`) and then mirrors into WP, with a 60-second in-progress transient that short-circuits the webhook fan-back. See [`docs/change-email.md`](docs/change-email.md).
- **Change email** — WorkOS-verified, conflict-guarded email-change flow. Self-service via `[workos:change-email]` shortcode confirms a WP-side hashed token sent to the new address (with a cancel-link notice to the old address) before committing; an admin acting on *another* account commits immediately (gated by `edit_users`, no verification round-trip). Admin surfaces are a row action under the **WorkOS column** on `wp-admin/users.php`, a panel on the user-edit screen, and a "Change email" action on the **WorkOS → Users** admin page. A configurable conflict policy (`block`, `allow_orphan`, `merge_request`) keeps the new email from silently overwriting another local WP user. Commits to WorkOS first (via `update_user`) then mirrors into WP, with a 60-second in-progress transient that short-circuits the webhook fan-back. See [`docs/change-email.md`](docs/change-email.md).
- **Skeleton placeholders** on every AuthKit surface (wp-login.php takeover, `/workos/login/{profile}`, shortcode) — pre-hydration markup from PHP plus a React `FlowSkeleton` during bootstrap, mirroring the real card heights so swap-in is a flicker not a jump.
- **MFA** — TOTP, SMS, WebAuthn/passkey with full enrollment + challenge UI; profile-level `mfa.enforce` (`never` / `if_required` / `always`) and factor allowlist
- **Profile routing rules** — ordered `redirect_to` glob / `referrer_host` / `user_role` matchers pick the right profile per request
Expand Down Expand Up @@ -179,9 +179,10 @@ per-profile CSS/JS, and the available PHP filters. For password-reset
integrations (admin-triggered, self-service, shortcode, redirect_url
policy, what-not-to-do), see
[`docs/password-reset.md`](docs/password-reset.md). For the
WorkOS-verified change-email flow (self-service + admin row action +
configurable conflict policy + WP-side token verification + cancel
link to old address), see
WorkOS-verified change-email flow (self-service token verification +
admin-direct immediate commits + the WorkOS → Users / row-action /
user-edit surfaces + configurable conflict policy + cancel link to old
address), see
[`docs/change-email.md`](docs/change-email.md).

### Custom paths
Expand Down
Loading