feat: admin settings page UI/UX pass with WordPress-native patterns
- Header: h1 + logo in a wrapping flex row, hr.wp-header-end before the tabs, aria-current on the active tab; own CSS/JS enqueued only on this screen. - Verbindung: ordered "So verbindest du Claude" steps, copy button (core clipboard.js + wp.a11y.speak), Site-Health-style status list, German details. - Zugriff: roles as form-table fieldset, connections with site date format, empty-state row, confirm on revoke; ConnectionUsage records last use via WP_Application_Passwords::record_application_password_usage (once a day). - Tools: plain-language legend, counter as inline notice, groups as core postboxes (postbox.js toggles, per-user closed state, summary when closed), fixed column widths with new "Art" column, submit above and below. - Protokoll: site-timezone times with UTC tooltip, result icons, filters with labels in the list table nav, retention link to Limits. - Limits: short labels with units and descriptions; snapshot count hidden but preserved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Admin;
|
||||
|
||||
use LIT\MCP\Server\ServerRegistrar;
|
||||
use Throwable;
|
||||
use WP_REST_Request;
|
||||
use WPMedia\MCP\OAuth\Auth\JWT;
|
||||
use WPMedia\MCP\OAuth\Auth\SecretManager;
|
||||
|
||||
/**
|
||||
* Records when an OAuth connection was last used, for "Zuletzt genutzt" in the tab "Zugriff".
|
||||
*
|
||||
* mcp-oauth authenticates with a JWT bound to an application password but never records
|
||||
* its usage (WordPress only does that for Basic-Auth application passwords), so last_used
|
||||
* stayed empty. After an authenticated request to our MCP route this calls WordPress' own
|
||||
* WP_Application_Passwords::record_application_password_usage(), which writes at most once
|
||||
* a day per password – the same data and throttle as in the user's profile.
|
||||
*/
|
||||
final class ConnectionUsage
|
||||
{
|
||||
/** @var callable(string): ?array<string, mixed> */
|
||||
private $decode;
|
||||
|
||||
/** @var callable(int, string): mixed */
|
||||
private $recordUsage;
|
||||
|
||||
/**
|
||||
* @param (callable(string): ?array<string, mixed>)|null $decode JWT → claims, null if invalid.
|
||||
* @param (callable(int, string): mixed)|null $recordUsage user id, application password uuid.
|
||||
*/
|
||||
public function __construct(?callable $decode = null, ?callable $recordUsage = null)
|
||||
{
|
||||
$this->decode = $decode ?? static function (string $token): ?array {
|
||||
$claims = JWT::decode($token, SecretManager::get_secret());
|
||||
|
||||
return is_array($claims) ? $claims : null;
|
||||
};
|
||||
$this->recordUsage = $recordUsage ?? static function (int $userId, string $uuid): void {
|
||||
\WP_Application_Passwords::record_application_password_usage($userId, $uuid);
|
||||
};
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_filter('rest_request_after_callbacks', [$this, 'record'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $handler
|
||||
* @return mixed The response, unchanged.
|
||||
*/
|
||||
public function record(mixed $response, array $handler, WP_REST_Request $request): mixed
|
||||
{
|
||||
$route = '/' . ServerRegistrar::NAMESPACE . '/' . ServerRegistrar::ROUTE;
|
||||
if ($request->get_route() !== $route || is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$header = $request->get_header('authorization');
|
||||
$userId = get_current_user_id();
|
||||
if (! is_string($header) || stripos($header, 'Bearer ') !== 0 || $userId <= 0) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
try {
|
||||
$claims = ($this->decode)(substr($header, 7));
|
||||
$uuid = is_array($claims) ? (string) ($claims['app_pass_id'] ?? '') : '';
|
||||
// Only the password of the user this request actually authenticated as.
|
||||
if ($uuid !== '' && (int) ($claims['sub'] ?? 0) === $userId) {
|
||||
($this->recordUsage)($userId, $uuid);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Bookkeeping only – never break the MCP response.
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user