feat: add Central API clients, bot rate limiting, and admin API UI
- Add ITK_HP_API and ITK_Bot_API static classes with queue/flush/cron - Add WP-Cron (5 min) + shutdown flush for both API queues - Bot Blocker and Honeypot now queue events to their respective APIs - Admin: Bot Blocker tab gains Central Bot API settings panel (enable, URL, token, test connection, flush queue, historical sync) - Admin: Honeypot tab gains Central Honeypot API settings panel - Admin JS: AJAX handlers for Test Connection and Flush Now buttons - Admin CSS: API card styles (status badge, notices, footer controls) - Add .gitignore (excludes bot-api/ which lives in CloudHost/bot-api) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,10 @@ class ITK_Admin {
|
||||
add_action('admin_menu', [$this, 'add_menu']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||
add_action('admin_init', [$this, 'handle_actions']);
|
||||
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
|
||||
add_action('wp_ajax_itk_save_config_file',[$this, 'ajax_save_config_file']);
|
||||
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
|
||||
add_action('wp_ajax_itk_save_config_file', [$this, 'ajax_save_config_file']);
|
||||
add_action('wp_ajax_itk_test_api', [$this, 'ajax_test_api']);
|
||||
add_action('wp_ajax_itk_flush_api_queue', [$this, 'ajax_flush_api_queue']);
|
||||
}
|
||||
|
||||
public function add_menu(): void {
|
||||
@@ -65,6 +67,60 @@ class ITK_Admin {
|
||||
]);
|
||||
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
|
||||
break;
|
||||
case 'save_bot_api':
|
||||
$this->save_api_settings(ITK_Bot_API::OPT_SETTINGS, 'itk_bot_api_settings');
|
||||
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
|
||||
break;
|
||||
case 'save_hp_api':
|
||||
$this->save_api_settings(ITK_HP_API::OPT_SETTINGS, 'itk_hp_api_settings');
|
||||
$this->redirect(['tab' => 'honeypot', 'saved' => 1]);
|
||||
break;
|
||||
case 'test_bot_api':
|
||||
$result = ITK_Bot_API::test_connection();
|
||||
$s = ITK_Bot_API::settings();
|
||||
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
|
||||
$s['last_error'] = $result['ok'] ? '' : $result['message'];
|
||||
update_option(ITK_Bot_API::OPT_SETTINGS, $s);
|
||||
set_transient('itk_bot_api_test_result', $result, 60);
|
||||
$this->redirect(['tab' => 'bot-blocker', 'api_tested' => 1]);
|
||||
break;
|
||||
case 'test_hp_api':
|
||||
$result = ITK_HP_API::test_connection();
|
||||
$s = ITK_HP_API::settings();
|
||||
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
|
||||
$s['last_error'] = $result['ok'] ? '' : $result['message'];
|
||||
update_option(ITK_HP_API::OPT_SETTINGS, $s);
|
||||
set_transient('itk_hp_api_test_result', $result, 60);
|
||||
$this->redirect(['tab' => 'honeypot', 'api_tested' => 1]);
|
||||
break;
|
||||
case 'flush_bot_api':
|
||||
ITK_Bot_API::flush();
|
||||
$this->redirect(['tab' => 'bot-blocker', 'api_flushed' => 1]);
|
||||
break;
|
||||
case 'flush_hp_api':
|
||||
ITK_HP_API::flush();
|
||||
$this->redirect(['tab' => 'honeypot', 'api_flushed' => 1]);
|
||||
break;
|
||||
case 'send_bot_history':
|
||||
$result = ITK_Bot_API::send_history_batch();
|
||||
set_transient('itk_bot_history_result', $result, 60);
|
||||
$this->redirect(['tab' => 'bot-blocker', 'history_sent' => 1]);
|
||||
break;
|
||||
case 'send_hp_history':
|
||||
$result = ITK_HP_API::send_history_batch();
|
||||
set_transient('itk_hp_history_result', $result, 60);
|
||||
$this->redirect(['tab' => 'honeypot', 'history_sent' => 1]);
|
||||
break;
|
||||
case 'reset_bot_history':
|
||||
delete_option('itk_bot_history_last_id');
|
||||
delete_option('itk_bot_history_sent');
|
||||
$this->redirect(['tab' => 'bot-blocker']);
|
||||
break;
|
||||
case 'reset_hp_history':
|
||||
delete_option('itk_hp_history_last_id');
|
||||
delete_option('itk_hp_history_sent');
|
||||
$this->redirect(['tab' => 'honeypot']);
|
||||
break;
|
||||
case 'save_settings_login':
|
||||
$this->save_settings_form('itk_security', [
|
||||
'custom_login_slug',
|
||||
@@ -102,6 +158,41 @@ class ITK_Admin {
|
||||
update_option($option, $opts);
|
||||
}
|
||||
|
||||
private function save_api_settings(string $option_key, string $post_key): void {
|
||||
$cur = get_option($option_key, []);
|
||||
$posted = $_POST[$post_key] ?? [];
|
||||
$new_url = esc_url_raw(trim($posted['api_url'] ?? ''));
|
||||
$changed = $new_url !== ($cur['api_url'] ?? '') || (!empty($posted['api_token']) && $posted['api_token'] !== ($cur['api_token'] ?? ''));
|
||||
update_option($option_key, array_merge($cur, [
|
||||
'enabled' => !empty($posted['enabled']),
|
||||
'api_url' => $new_url,
|
||||
'api_token' => sanitize_text_field($posted['api_token'] ?? ($cur['api_token'] ?? '')),
|
||||
'connection_ok' => $changed ? null : ($cur['connection_ok'] ?? null),
|
||||
'last_verified' => $changed ? 0 : ($cur['last_verified'] ?? 0),
|
||||
'last_error' => $changed ? '' : ($cur['last_error'] ?? ''),
|
||||
]));
|
||||
}
|
||||
|
||||
/* ── AJAX: test API connection ─────────────────────────────── */
|
||||
|
||||
public function ajax_test_api(): void {
|
||||
check_ajax_referer(self::NONCE_ACTION, 'nonce');
|
||||
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
|
||||
$which = sanitize_key($_POST['api'] ?? '');
|
||||
$result = $which === 'bot' ? ITK_Bot_API::test_connection() : ITK_HP_API::test_connection();
|
||||
wp_send_json($result);
|
||||
}
|
||||
|
||||
/* ── AJAX: flush API queue ─────────────────────────────────── */
|
||||
|
||||
public function ajax_flush_api_queue(): void {
|
||||
check_ajax_referer(self::NONCE_ACTION, 'nonce');
|
||||
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
|
||||
$which = sanitize_key($_POST['api'] ?? '');
|
||||
if ($which === 'bot') ITK_Bot_API::flush(); else ITK_HP_API::flush();
|
||||
wp_send_json_success('Queue flushed.');
|
||||
}
|
||||
|
||||
private function redirect(array $args): void {
|
||||
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
|
||||
exit;
|
||||
@@ -423,6 +514,98 @@ class ITK_Admin {
|
||||
<?php submit_button('Save Response Settings'); ?>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
/* ── Central Bot API card ─────────────────────── */
|
||||
$bot_api = ITK_Bot_API::settings();
|
||||
$bot_queue = count((array) get_option(ITK_Bot_API::OPT_QUEUE, []));
|
||||
$bot_total = ITK_Database::count_bot_rows();
|
||||
$bot_sent = (int) get_option('itk_bot_history_sent', 0);
|
||||
$bot_rem = max(0, $bot_total - $bot_sent);
|
||||
$bot_ok = $bot_api['connection_ok'];
|
||||
$bot_cls = is_null($bot_ok) ? 'itk-api-unknown' : ($bot_ok ? 'itk-api-ok' : 'itk-api-err');
|
||||
$bot_lbl = is_null($bot_ok) ? 'Not tested' : ($bot_ok ? 'Connected' : 'Connection failed');
|
||||
$bot_test_r = get_transient('itk_bot_api_test_result'); if ($bot_test_r) delete_transient('itk_bot_api_test_result');
|
||||
$bot_hist_r = get_transient('itk_bot_history_result'); if ($bot_hist_r) delete_transient('itk_bot_history_result');
|
||||
?>
|
||||
<section class="itk-card itk-api-card">
|
||||
<h2>Central Bot API</h2>
|
||||
<p class="description itk-api-desc">Send blocked-bot events to your self-hosted Bot Intelligence Docker stack (port 3001).</p>
|
||||
|
||||
<div class="itk-api-status-bar">
|
||||
<span class="itk-api-badge <?= esc_attr($bot_cls) ?>"><?= esc_html($bot_lbl) ?></span>
|
||||
<?php if ($bot_api['last_verified'] > 0): ?>
|
||||
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$bot_api['last_verified'])) ?> ago</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bot_ok && !is_null($bot_ok) && !empty($bot_api['last_error'])): ?>
|
||||
<span class="itk-api-err-msg"><?= esc_html($bot_api['last_error']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($bot_test_r): ?>
|
||||
<div class="itk-api-notice <?= $bot_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_test_r['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($bot_hist_r): ?>
|
||||
<div class="itk-api-notice <?= $bot_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_hist_r['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="save_bot_api">
|
||||
<table class="form-table itk-api-table">
|
||||
<tr>
|
||||
<th>Enable</th>
|
||||
<td><label><input type="checkbox" name="itk_bot_api_settings[enabled]" value="1" <?= checked(!empty($bot_api['enabled'])) ?>> Send events to Central API</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API URL</th>
|
||||
<td>
|
||||
<input type="url" name="itk_bot_api_settings[api_url]" value="<?= esc_attr($bot_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3001">
|
||||
<p class="description">Base URL of your Bot API stack (e.g. <code>http://localhost:3001</code>)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API Token</th>
|
||||
<td>
|
||||
<input type="password" name="itk_bot_api_settings[api_token]" value="" class="regular-text"
|
||||
placeholder="<?= !empty($bot_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
|
||||
autocomplete="new-password">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="itk-api-form-actions">
|
||||
<?php submit_button('Save Bot API Settings', 'primary', 'submit', false); ?>
|
||||
<button type="button" class="button itk-btn-test-api" data-api="bot" style="margin-left:8px">Test Connection</button>
|
||||
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="itk-api-footer">
|
||||
<div class="itk-api-queue-row">
|
||||
<strong><?= (int) $bot_queue ?></strong> event(s) pending in queue
|
||||
<button type="button" class="button button-small itk-btn-flush-api" data-api="bot" style="margin-left:8px">Flush Now</button>
|
||||
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
|
||||
</div>
|
||||
|
||||
<div class="itk-api-history-row">
|
||||
<strong>Historical sync:</strong>
|
||||
<?= number_format($bot_sent) ?> / <?= number_format($bot_total) ?> records sent
|
||||
<?php if ($bot_rem > 0): ?><em class="itk-api-rem">(<?= number_format($bot_rem) ?> remaining)</em><?php endif; ?>
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:10px">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="send_bot_history">
|
||||
<input type="submit" class="button button-small" value="Send Next 50">
|
||||
</form>
|
||||
<?php if ($bot_sent > 0): ?>
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="reset_bot_history">
|
||||
<input type="submit" class="button button-small" value="Reset Progress">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -607,6 +790,98 @@ class ITK_Admin {
|
||||
<?php submit_button('Save Honeypot Settings'); ?>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
/* ── Central Honeypot API card ────────────────── */
|
||||
$hp_api = ITK_HP_API::settings();
|
||||
$hp_queue = count((array) get_option(ITK_HP_API::OPT_QUEUE, []));
|
||||
$hp_total = ITK_Database::count_honeypot_rows();
|
||||
$hp_sent = (int) get_option('itk_hp_history_sent', 0);
|
||||
$hp_rem = max(0, $hp_total - $hp_sent);
|
||||
$hp_ok = $hp_api['connection_ok'];
|
||||
$hp_cls = is_null($hp_ok) ? 'itk-api-unknown' : ($hp_ok ? 'itk-api-ok' : 'itk-api-err');
|
||||
$hp_lbl = is_null($hp_ok) ? 'Not tested' : ($hp_ok ? 'Connected' : 'Connection failed');
|
||||
$hp_test_r = get_transient('itk_hp_api_test_result'); if ($hp_test_r) delete_transient('itk_hp_api_test_result');
|
||||
$hp_hist_r = get_transient('itk_hp_history_result'); if ($hp_hist_r) delete_transient('itk_hp_history_result');
|
||||
?>
|
||||
<section class="itk-card itk-api-card">
|
||||
<h2>Central Honeypot API</h2>
|
||||
<p class="description itk-api-desc">Send honeypot catch events to your self-hosted Honeypot Intelligence Docker stack (port 3000).</p>
|
||||
|
||||
<div class="itk-api-status-bar">
|
||||
<span class="itk-api-badge <?= esc_attr($hp_cls) ?>"><?= esc_html($hp_lbl) ?></span>
|
||||
<?php if ($hp_api['last_verified'] > 0): ?>
|
||||
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$hp_api['last_verified'])) ?> ago</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!$hp_ok && !is_null($hp_ok) && !empty($hp_api['last_error'])): ?>
|
||||
<span class="itk-api-err-msg"><?= esc_html($hp_api['last_error']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($hp_test_r): ?>
|
||||
<div class="itk-api-notice <?= $hp_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_test_r['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($hp_hist_r): ?>
|
||||
<div class="itk-api-notice <?= $hp_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_hist_r['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="save_hp_api">
|
||||
<table class="form-table itk-api-table">
|
||||
<tr>
|
||||
<th>Enable</th>
|
||||
<td><label><input type="checkbox" name="itk_hp_api_settings[enabled]" value="1" <?= checked(!empty($hp_api['enabled'])) ?>> Send events to Central API</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API URL</th>
|
||||
<td>
|
||||
<input type="url" name="itk_hp_api_settings[api_url]" value="<?= esc_attr($hp_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3000">
|
||||
<p class="description">Base URL of your Honeypot API stack (e.g. <code>http://localhost:3000</code>)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API Token</th>
|
||||
<td>
|
||||
<input type="password" name="itk_hp_api_settings[api_token]" value="" class="regular-text"
|
||||
placeholder="<?= !empty($hp_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
|
||||
autocomplete="new-password">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="itk-api-form-actions">
|
||||
<?php submit_button('Save Honeypot API Settings', 'primary', 'submit', false); ?>
|
||||
<button type="button" class="button itk-btn-test-api" data-api="hp" style="margin-left:8px">Test Connection</button>
|
||||
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="itk-api-footer">
|
||||
<div class="itk-api-queue-row">
|
||||
<strong><?= (int) $hp_queue ?></strong> event(s) pending in queue
|
||||
<button type="button" class="button button-small itk-btn-flush-api" data-api="hp" style="margin-left:8px">Flush Now</button>
|
||||
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
|
||||
</div>
|
||||
|
||||
<div class="itk-api-history-row">
|
||||
<strong>Historical sync:</strong>
|
||||
<?= number_format($hp_sent) ?> / <?= number_format($hp_total) ?> records sent
|
||||
<?php if ($hp_rem > 0): ?><em class="itk-api-rem">(<?= number_format($hp_rem) ?> remaining)</em><?php endif; ?>
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:10px">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="send_hp_history">
|
||||
<input type="submit" class="button button-small" value="Send Next 50">
|
||||
</form>
|
||||
<?php if ($hp_sent > 0): ?>
|
||||
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="itk_action" value="reset_hp_history">
|
||||
<input type="submit" class="button button-small" value="Reset Progress">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user