Setup API Key';
array_unshift( $links, $setup_link );
return $links;
}
// ── Menu Registration ─────────────────────────────────────────────────
public function register_menus(): void {
add_menu_page(
__( 'AICOM', 'aicom' ),
__( 'AICOM', 'aicom' ),
self::CAPABILITY,
self::MENU_SLUG,
[ $this, 'page_dashboard' ],
'dashicons-rest-api',
80
);
add_submenu_page( self::MENU_SLUG, __( 'Dashboard', 'aicom' ), __( 'Dashboard', 'aicom' ), self::CAPABILITY, self::MENU_SLUG, [ $this, 'page_dashboard' ] );
add_submenu_page( self::MENU_SLUG, __( 'API Keys', 'aicom' ), __( 'API Keys', 'aicom' ), self::CAPABILITY, 'aicom-api-keys', [ $this, 'page_api_keys' ] );
add_submenu_page( self::MENU_SLUG, __( 'Audit Logs', 'aicom' ), __( 'Audit Logs', 'aicom' ), self::CAPABILITY, 'aicom-audit-logs', [ $this, 'page_audit_logs' ] );
add_submenu_page( self::MENU_SLUG, __( 'Safety', 'aicom' ), __( 'Safety', 'aicom' ), self::CAPABILITY, 'aicom-safety', [ $this, 'page_safety' ] );
add_submenu_page( self::MENU_SLUG, __( 'Modules', 'aicom' ), __( 'Modules', 'aicom' ), self::CAPABILITY, 'aicom-modules', [ $this, 'page_modules' ] );
add_submenu_page( self::MENU_SLUG, __( 'Backups', 'aicom' ), __( 'Backups', 'aicom' ), self::CAPABILITY, 'aicom-backups', [ $this, 'page_backups' ] );
}
// ── Asset Enqueuing ───────────────────────────────────────────────────
public function enqueue_assets( string $hook ): void {
$plugin_url = AICOM_URL;
$version = AICOM_VERSION;
$on_aicom = strpos( $hook, 'aicom' ) !== false;
// CSS enqueued on every admin page (admin bar styles needed everywhere)
wp_enqueue_style(
'aicom-admin',
$plugin_url . 'assets/admin.css',
[],
$version
);
// JS only needed on AICOM pages OR when admin bar is showing (toolbar toggle)
wp_enqueue_script(
'aicom-admin',
$plugin_url . 'assets/admin.js',
[ 'jquery' ],
$version,
true
);
wp_localize_script( 'aicom-admin', 'AICOM_MCP', [
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
] );
}
// ── Page Renderers ────────────────────────────────────────────────────
public function page_dashboard(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/dashboard.php';
}
public function page_api_keys(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/api-keys.php';
}
public function page_audit_logs(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/audit-logs.php';
}
public function page_safety(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/safety.php';
}
public function page_modules(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/modules.php';
}
public function page_backups(): void {
$this->require_cap();
require AICOM_DIR . 'admin/pages/backups.php';
}
// ── POST Handler ──────────────────────────────────────────────────────
public function handle_post(): void {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'aicom' ), 403 );
}
check_admin_referer( self::NONCE_ACTION );
// All $_POST reads happen here, after nonce verification, then passed as typed params.
$action = sanitize_key( wp_unslash( $_POST['aicom_action'] ?? '' ) );
switch ( $action ) {
case 'set_lock':
$this->handle_set_lock(
! empty( $_POST['soft_lock'] ),
! empty( $_POST['hard_lock'] )
);
break;
case 'create_key':
$raw_scopes = filter_input( INPUT_POST, 'scopes', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ) ?? [];
$scopes = array_map( 'sanitize_text_field', wp_unslash( $raw_scopes ) );
$this->handle_create_key(
sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) ),
$scopes,
! empty( $_POST['dry_run_only'] ),
sanitize_textarea_field( wp_unslash( $_POST['ip_allowlist'] ?? '' ) )
);
break;
case 'rotate_key':
$this->handle_rotate_key( absint( wp_unslash( $_POST['key_id'] ?? 0 ) ) );
break;
case 'revoke_key':
$this->handle_revoke_key( absint( wp_unslash( $_POST['key_id'] ?? 0 ) ) );
break;
case 'suspend_key':
$this->handle_suspend_key( absint( wp_unslash( $_POST['key_id'] ?? 0 ) ) );
break;
case 'unsuspend_key':
$this->handle_unsuspend_key( absint( wp_unslash( $_POST['key_id'] ?? 0 ) ) );
break;
case 'delete_backup':
$this->handle_delete_backup( absint( wp_unslash( $_POST['backup_id'] ?? 0 ) ) );
break;
default:
wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&error=unknown_action' ) );
exit;
}
}
// ── Action Handlers ───────────────────────────────────────────────────
private function handle_set_lock( bool $soft_lock, bool $hard_lock ): void {
AICOM_Lock_Manager::set_soft_lock( $soft_lock );
AICOM_Lock_Manager::set_hard_lock( $hard_lock );
AICOM_Audit_Logger::log( [
'remote_ip' => AICOM_Tool_Router::remote_ip(),
'tool_name' => 'admin.set_lock',
'module' => 'admin',
'status' => 'success',
'http_status' => 200,
'api_key_label' => wp_get_current_user()->user_login,
'result_summary_json' => wp_json_encode( [
'soft_lock' => $soft_lock,
'hard_lock' => $hard_lock,
] ),
] );
wp_safe_redirect( admin_url( 'admin.php?page=aicom-safety&updated=lock' ) );
exit;
}
private function handle_create_key( string $label, array $scopes, bool $dry_run_only, string $ip_allowlist_raw ): void {
if ( ! $label ) {
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&error=missing_label' ) );
exit;
}
$restrictions = [];
if ( $dry_run_only ) {
$restrictions['dry_run_only'] = true;
}
if ( $ip_allowlist_raw ) {
$restrictions['ip_allowlist'] = array_filter( array_map( 'trim', explode( "\n", $ip_allowlist_raw ) ) );
}
$result = AICOM_Auth::create_key( $label, $scopes, $restrictions );
// Store the plain key in a short-lived transient for one-time display
set_transient( 'aicom_new_key_' . $result['id'], $result['plain_key'], 60 );
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&created=' . $result['id'] ) );
exit;
}
private function handle_rotate_key( int $key_id ): void {
$new_key = AICOM_Auth::rotate_key( $key_id );
if ( $new_key ) {
set_transient( 'aicom_new_key_' . $key_id, $new_key, 60 );
}
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&rotated=' . $key_id ) );
exit;
}
private function handle_revoke_key( int $key_id ): void {
AICOM_Auth::revoke_key( $key_id );
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&revoked=' . $key_id ) );
exit;
}
private function handle_suspend_key( int $key_id ): void {
AICOM_Auth::suspend_key( $key_id );
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&suspended=' . $key_id ) );
exit;
}
private function handle_unsuspend_key( int $key_id ): void {
AICOM_Auth::unsuspend_key( $key_id );
wp_safe_redirect( admin_url( 'admin.php?page=aicom-api-keys&unsuspended=' . $key_id ) );
exit;
}
private function handle_delete_backup( int $id ): void {
global $wpdb;
if ( $id ) {
$wpdb->delete( $wpdb->prefix . 'aicom_backups', [ 'id' => $id ] );
}
wp_safe_redirect( admin_url( 'admin.php?page=aicom-backups&deleted=' . $id ) );
exit;
}
// ── Admin Bar ─────────────────────────────────────────────────────────
public static function register_admin_bar( WP_Admin_Bar $bar ): void {
if ( ! current_user_can( self::CAPABILITY ) ) {
return;
}
global $wpdb;
$keys = $wpdb->get_results(
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
"SELECT id, label, status FROM {$wpdb->prefix}aicom_api_keys
WHERE status IN ('active','suspended')
ORDER BY created_at DESC LIMIT 20",
ARRAY_A
);
$manage_url = admin_url( 'admin.php?page=aicom-api-keys' );
$active_count = count( array_filter( $keys, fn( $k ) => $k['status'] === 'active' ) );
$count_html = $active_count > 0
? ' ' . $active_count . ''
: '';
$bar->add_node( [
'id' => 'aicom-toolbar',
'title' => ''
. 'AICOM Keys'
. $count_html,
'href' => $manage_url,
'meta' => [ 'class' => 'aicom-tb-parent' ],
] );
foreach ( $keys as $key ) {
$is_active = $key['status'] === 'active';
$dot = $is_active ? '🟢' : '🟡';
$action = $is_active ? 'suspend_key' : 'unsuspend_key';
$btn_label = $is_active ? 'Suspend' : 'Unsuspend';
$btn_class = $is_active ? 'aicom-tb-btn-suspend' : 'aicom-tb-btn-unsuspend';
$bar->add_node( [
'id' => 'aicom-key-' . (int) $key['id'],
'parent' => 'aicom-toolbar',
'title' => '' . $dot . ''
. '' . esc_html( $key['label'] ) . ''
. '',
'href' => false,
'meta' => [ 'class' => 'aicom-tb-key' ],
] );
}
$bar->add_node( [
'id' => 'aicom-toolbar-manage',
'parent' => 'aicom-toolbar',
'title' => 'Manage API Keys',
'href' => $manage_url,
'meta' => [ 'class' => 'aicom-tb-manage' ],
] );
}
public static function ajax_toolbar_toggle(): void {
$key_id = absint( $_POST['key_id'] ?? 0 );
$action = sanitize_key( wp_unslash( $_POST['aicom_action'] ?? '' ) );
if ( ! $key_id || ! in_array( $action, [ 'suspend_key', 'unsuspend_key' ], true ) ) {
wp_send_json_error( 'invalid_params', 400 );
}
if ( ! check_ajax_referer( 'aicom_toolbar_' . $key_id, 'nonce', false ) ) {
wp_send_json_error( 'bad_nonce', 403 );
}
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_send_json_error( 'unauthorized', 403 );
}
$ok = $action === 'suspend_key'
? AICOM_Auth::suspend_key( $key_id )
: AICOM_Auth::unsuspend_key( $key_id );
if ( ! $ok ) {
wp_send_json_error( 'no_change', 409 );
}
$new_status = $action === 'suspend_key' ? 'suspended' : 'active';
$new_dot = $new_status === 'active' ? '🟢' : '🟡';
$new_action = $new_status === 'active' ? 'suspend_key' : 'unsuspend_key';
$new_btn_label = $new_status === 'active' ? 'Suspend' : 'Unsuspend';
$new_btn_class = $new_status === 'active' ? 'aicom-tb-btn-suspend' : 'aicom-tb-btn-unsuspend';
$new_nonce = wp_create_nonce( 'aicom_toolbar_' . $key_id );
wp_send_json_success( [
'new_status' => $new_status,
'new_dot' => $new_dot,
'new_action' => $new_action,
'new_btn_label' => $new_btn_label,
'new_btn_class' => $new_btn_class,
'new_nonce' => $new_nonce,
] );
}
// ── Private Helpers ───────────────────────────────────────────────────
private function require_cap(): void {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'aicom' ) );
}
}
}