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
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Enterprise identity management for WordPress powered by WorkOS. SSO, directory sync, MFA, and user management.

- **Version:** 1.0.4
- **Version:** 1.0.5
- **Namespace:** `WorkOS\`
- **PHP Requirement:** 7.4+
- **WordPress Requirement:** 5.9+
Expand Down Expand Up @@ -80,6 +80,11 @@ Per-environment constants (take priority over generic):
| `src/WorkOS/Admin/DiagnosticsPage.php` | System diagnostics page |
| `src/WorkOS/Admin/OnboardingPage.php` | Onboarding wizard UI |
| `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 |
| `src/WorkOS/Admin/Users/RestApi.php` | `GET /wp-json/workos/v1/admin/users` — proxies `Api\Client::list_users()` with sanitized pagination + filters and a server-computed `dashboard_url` per row |
| `src/js/admin-users/index.tsx` | React user list (search + cursor pagination + Open in WorkOS deep-link) |
| **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 Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [1.0.5] - 2026-05-18

### Added

- **WorkOS → Users admin page** ([CONS-273](https://linear.app/nexcess/issue/CONS-273/re-enable-workos-emails-for-affected-portal-users))
— new submenu under WorkOS that mounts a paginated, searchable React
list of WorkOS users for the active environment. Each row exposes an
"Open in WorkOS" deep-link that takes the admin straight to the user's
Dashboard page (`https://dashboard.workos.com/{env}/users/{id}/details`),
where the per-user "Re-enable email" action lives. Gated by
`manage_options`. Backed by `GET /wp-json/workos/v1/admin/users`, which
proxies `Api\Client::list_users()` with sanitized `limit` (1..100),
cursor (`after`/`before`), `email` substring, and `organization_id`
pass-through, and enriches each user record with a server-computed
`dashboard_url` so the React side doesn't reconstruct it.

**Why list-only:** WorkOS exposes the "Re-enable email" action only
through the Dashboard — there is no public REST endpoint or webhook
event for email suppression / bounce state as of this release
(verified against the WorkOS API reference, the User schema, the
Events catalogue, and the public workos-node / -python / -ruby / -go
SDK sources). This page builds the foundation; once WorkOS ships an
API, a row + bulk action can be wired in without reworking the UI.

## [1.0.4] - 2026-05-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion integration-workos.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Integration with WorkOS
* Plugin URI: https://github.com/bordoni/integration-workos
* Description: Enterprise identity management for WordPress powered by WorkOS. SSO, directory sync, MFA, and user management.
* Version: 1.0.4
* Version: 1.0.5
* Author: Gustavo Bordoni
* Author URI: https://github.com/bordoni
* License: GPL-2.0-or-later
Expand Down
9 changes: 8 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags: sso, identity, workos, authentication, directory-sync
Requires at least: 6.2
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.0.4
Stable tag: 1.0.5
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -175,6 +175,10 @@ WorkOS is provided by WorkOS, Inc.

== Changelog ==

= 1.0.5 - 2026-05-18 =

* New: WorkOS → Users admin page. Paginated, searchable React list of WorkOS users for the active environment, with a per-row "Open in WorkOS" deep-link straight to the user's Dashboard page. Lets admins triage WorkOS users (including re-enabling a suppressed email under the Dashboard's Emails tab) without bouncing through the Dashboard's own user picker. Requires `manage_options`. No bulk re-enable yet — WorkOS does not expose a public REST endpoint for the "Re-enable email" action. ([CONS-273](https://linear.app/nexcess/issue/CONS-273/re-enable-workos-emails-for-affected-portal-users))

= 1.0.4 - 2026-05-14 =

* Fix: `wp-login.php?loggedout=true` is now claimed by the AuthKit takeover instead of rendering native wp-login. The "you have been logged out" screen advertised the wp-login username/password field, which legacy customers misread as a still-working classic sign-in. The URL now 302s to `/login/?loggedout=true` (or the configured custom path) so the React form handles it. `?fallback=1`, `?workos=0`, and `action=logout|lostpassword|rp|...` bypasses are unchanged. (#18)
Expand Down Expand Up @@ -245,6 +249,9 @@ Base platform:

== Upgrade Notice ==

= 1.0.5 =
Adds a new WorkOS → Users admin page (read-only, paginated, searchable) with deep-links into the WorkOS Dashboard so admins can re-enable a user's suppressed email faster. No bulk re-enable yet — WorkOS does not expose a public API for that action.

= 1.0.4 =
Fixes the "you have been logged out" screen leaking the native wp-login form, password-reset emails arriving with HTML-encoded `&` in the link, an infinite redirect loop caused by cached redirect responses, and a Login Profile editor bug where unchecking an auth method or MFA factor did not persist on save.

Expand Down
126 changes: 126 additions & 0 deletions src/WorkOS/Admin/Users/AdminPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php
/**
* WorkOS Users admin page.
*
* @package WorkOS\Admin\Users
*/

namespace WorkOS\Admin\Users;

use WorkOS\Config;

defined( 'ABSPATH' ) || exit;

/**
* Submenu under "WorkOS" that mounts the React Users list.
*
* Read-only triage UI: paginated list of WorkOS users with search and a
* per-row "Open in WorkOS" deep-link that takes admins straight to the
* user's Dashboard page (where the "Re-enable email" action lives — there
* is no public REST endpoint for it as of this release).
*/
class AdminPage {

public const MENU_SLUG = 'workos-users';
public const SCRIPT_HANDLE = 'workos-admin-users';
public const STYLE_HANDLE = 'workos-admin-users';

/**
* Register hooks.
*
* @return void
*/
public function register(): void {
add_action( 'admin_menu', [ $this, 'register_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'maybe_enqueue_assets' ] );
}

/**
* Register the submenu under the main WorkOS menu.
*
* @return void
*/
public function register_menu(): void {
add_submenu_page(
'workos',
__( 'WorkOS Users', 'integration-workos' ),
__( 'Users', 'integration-workos' ),
'manage_options',
self::MENU_SLUG,
[ $this, 'render' ]
);
}

/**
* Render the React mount shell.
*
* @return void
*/
public function render(): void {
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e( 'WorkOS Users', 'integration-workos' ); ?></h1>
<hr class="wp-header-end">
<p class="description">
<?php esc_html_e( 'Browse users stored in WorkOS for the active environment. Use the Open in WorkOS action to manage a user in the WorkOS Dashboard — including re-enabling email after a deliverability suppression.', 'integration-workos' ); ?>
</p>
<div id="workos-users-admin-root"></div>
</div>
<?php
}

/**
* Enqueue the React admin bundle on our page only.
*
* @param string $hook Current admin page hook.
*
* @return void
*/
public function maybe_enqueue_assets( string $hook ): void {
if ( false === strpos( $hook, self::MENU_SLUG ) ) {
return;
}

$assets_url = trailingslashit( WORKOS_URL . 'build' );
$assets_dir = trailingslashit( WORKOS_DIR . 'build' );

$asset_file = $assets_dir . 'admin-users.asset.php';
$asset = file_exists( $asset_file )
? require $asset_file
: [
'dependencies' => [ 'wp-element', 'wp-i18n' ],
'version' => WORKOS_VERSION,
];

wp_enqueue_script(
self::SCRIPT_HANDLE,
$assets_url . 'admin-users.js',
$asset['dependencies'] ?? [ 'wp-element', 'wp-i18n' ],
$asset['version'] ?? WORKOS_VERSION,
true
);

wp_set_script_translations( self::SCRIPT_HANDLE, 'integration-workos' );

wp_enqueue_style(
self::STYLE_HANDLE,
$assets_url . 'admin-users.css',
[],
$asset['version'] ?? WORKOS_VERSION
);

wp_localize_script(
self::SCRIPT_HANDLE,
'workosUsersAdmin',
[
'restUrl' => esc_url_raw( rest_url( RestApi::NAMESPACE . RestApi::BASE ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'environment' => Config::get_active_environment(),
'environmentId' => Config::get_environment_id(),
'dashboardBaseUrl' => 'https://dashboard.workos.com',
'defaultLimit' => 25,
'pluginEnabled' => workos()->is_enabled(),
]
);
}
}
41 changes: 41 additions & 0 deletions src/WorkOS/Admin/Users/Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Users admin controller.
*
* @package WorkOS\Admin\Users
*/

namespace WorkOS\Admin\Users;

use WorkOS\Contracts\Controller as BaseController;

/**
* Wires the WorkOS Users admin page (React list of WorkOS users) and its
* supporting REST endpoint. Always active — the REST routes need to be
* registered during `rest_api_init` regardless of admin context, and the
* AdminPage is gated internally via the `admin_menu` / `admin_enqueue_scripts`
* hooks plus a `manage_options` capability check.
*/
class Controller extends BaseController {

/**
* Register the components.
*
* @return void
*/
protected function doRegister(): void {
$this->container->singleton( RestApi::class );
$this->container->get( RestApi::class );

$this->container->singleton( AdminPage::class );
$this->container->get( AdminPage::class )->register();
}

/**
* Unregister.
*
* @return void
*/
protected function doUnregister(): void {
}
}
Loading