From a08a7605294b4698a0c07691ba9cf7fd1d24bd9b Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 14:44:20 +0100 Subject: [PATCH 01/47] Implement Security Tab and moved Secuirty related stuff to it Moved from Profile to Secruity - Password Change - 2FA Profile page also show the latest connections at the bottom The most recent 20 connections Security tab will show up to 20 and you can have view to sell all history --- .../src/Components/AdminPanel/Pages/Tools.php | 12 + .../UserPanel/Pages/SecurityPage.php | 243 ++++++++++++++++++ .../Components/UserPanel/UserController.php | 68 +++++ .../src/Components/UserPanel/UserMenu.php | 11 +- .../src/Components/UserPanel/UserView.php | 7 + .../src/Hooks/User/ConnectionLogger.php | 129 ++++++++++ .../src/Hooks/User/Include.php | 1 + src/acore-wp-plugin/src/Hooks/User/User.php | 98 +++++++ .../src/Hooks/Various/tgmplugin_activator.php | 7 +- src/acore-wp-plugin/src/Manager/Opts.php | 1 + 10 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php create mode 100644 src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index 86058a205..03497dab8 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -53,6 +53,18 @@ + + + + + + +

When enabled, login IPs and timestamps are recorded for the Security page.

+ + acore_resurrection_scroll != '1') echo 'style="display:none;"'?>> diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php new file mode 100644 index 000000000..8e94d5b23 --- /dev/null +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php @@ -0,0 +1,243 @@ + + +
+

+ +
+
+

+
+
+ + +
+

+
+ + +

+ + +

+ + + +
+
+ + + + + + + + + + + + + + + +

+ + +

+
+
+ +
+
+ + +
+
+

+
+
+ + + + + + + + + + + +

+ + + +

+
+
+ + +
+
+

+ 50): ?> + + +
+
+ +

+ + + + + + + + + + + $row): ?> + = 50 ? 'class="acore-conn-hidden" style="display:none;"' : '' ?>> + + + + + + +
ip_address) ?>country) ?>login_at)) ?>
+ +
+
+
+ + + + diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php index 1199fb418..63bd0493c 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php @@ -4,6 +4,7 @@ use ACore\Manager\Opts; use ACore\Manager\ACoreServices; +use ACore\Manager\UserValidator; use ACore\Components\UserPanel\UserView; class UserController { @@ -156,6 +157,73 @@ public function showItemRestorationPage() { echo $this->getView()->getItemRestorationRender($queryResult->fetchAllAssociative()); } + public function showSecurityPage() { + $user = wp_get_current_user(); + $passwordMessage = null; + + if (isset($_POST['acore_change_password'])) { + $nonce = $_POST['acore_pw_nonce'] ?? ''; + if (!wp_verify_nonce($nonce, 'acore_security_change_password')) { + $passwordMessage = ['type' => 'error', 'text' => __('Security check failed.', 'acore-wp-plugin')]; + } else { + $oldPass = $_POST['acore_old_pass'] ?? ''; + $newPass = $_POST['acore_new_pass'] ?? ''; + $confirmPass = $_POST['acore_confirm_pass'] ?? ''; + + if (!wp_check_password($oldPass, $user->user_pass, $user->ID)) { + $passwordMessage = ['type' => 'error', 'text' => __('Current password is incorrect.', 'acore-wp-plugin')]; + } elseif ($newPass !== $confirmPass) { + $passwordMessage = ['type' => 'error', 'text' => __('New passwords do not match.', 'acore-wp-plugin')]; + } else { + $validation = UserValidator::validatePassword($newPass); + if ($validation !== true) { + $passwordMessage = ['type' => 'error', 'text' => $validation]; + } else { + wp_set_password($newPass, $user->ID); + update_user_meta($user->ID, 'acore_password_changed_at', current_time('mysql')); + + try { + $soap = ACoreServices::I()->getAccountSoap(); + $soap->setAccountPassword($user->user_login, $newPass); + } catch (\Exception $e) { + } + + $passwordMessage = ['type' => 'success', 'text' => __('Password updated successfully.', 'acore-wp-plugin')]; + } + } + } + } + + $passwordChangedAt = get_user_meta($user->ID, 'acore_password_changed_at', true); + $twoFaData = $this->getTwoFaData($user); + $connections = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); + + echo $this->getView()->getSecurityRender($connections, $passwordChangedAt, $twoFaData, $passwordMessage); + } + + private function getTwoFaData(\WP_User $user) { + $active = class_exists('\WP2FA\WP2FA') || function_exists('wp2fa_get_enabled_providers_for_user'); + + if (!$active) { + return ['plugin_active' => false]; + } + + $primaryMethods = get_user_meta($user->ID, 'wp_2fa_enabled_methods', true); + $backupMethods = get_user_meta($user->ID, 'wp_2fa_backup_methods_enabled', true); + $totpEnabled = !empty(get_user_meta($user->ID, 'wp_2fa_totp_key', true)); + $emailEnabled = (is_array($primaryMethods) && in_array('email', $primaryMethods)) + || $primaryMethods === 'email'; + + return [ + 'plugin_active' => true, + 'primary_methods' => $primaryMethods ?: [], + 'backup_methods' => $backupMethods ?: [], + 'totp_enabled' => $totpEnabled, + 'email_enabled' => $emailEnabled, + 'setup_url' => admin_url('profile.php?page=wp-2fa-setup'), + ]; + } + /** * * @return UserView diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php b/src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php index 8d70c09b8..3bd8b7366 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php @@ -26,9 +26,10 @@ public static function I() return self::$instance; } - // action function for above hook function acore_user_menu() { + add_submenu_page('profile.php', 'Security', 'Security', 'read', ACORE_SLUG . '-security', array($this, 'security_page')); + if (Opts::I()->eluna_recruit_a_friend == '1') { add_submenu_page('profile.php', 'Recruit a Friend', 'Recruit a Friend', 'read', ACORE_SLUG . '-eluna-raf-progress', array($this, 'eluna_raf_progress_page')); } @@ -38,6 +39,13 @@ function acore_user_menu() } } + // action function for above hook + function security_page() + { + $ctrl = new UserController(); + $ctrl->showSecurityPage(); + } + // action function for above hook function eluna_raf_progress_page() { @@ -48,6 +56,7 @@ function eluna_raf_progress_page() } } + // action function for above hook function item_restoration_page() { $SettingsCtrl = new UserController(); diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserView.php b/src/acore-wp-plugin/src/Components/UserPanel/UserView.php index 97b1b8528..1794d9476 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserView.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserView.php @@ -14,6 +14,13 @@ public function getRafProgressRender($rafPersonalInfo, $rafPersonalProgress, $ra return ob_get_clean(); } + /* Page: Security */ + function getSecurityRender($connections, $passwordChangedAt, $twoFaData, $passwordMessage = null) { + ob_start(); + include(__DIR__ . '/Pages/SecurityPage.php'); + return ob_get_clean(); + } + /* Page: Item Restoration */ function getItemRestorationRender($characters) { extract([$characters]); diff --git a/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php b/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php new file mode 100644 index 000000000..c05af8d42 --- /dev/null +++ b/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php @@ -0,0 +1,129 @@ +prefix . 'acore_login_history'; + + if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) { + return; + } + + $collate = $wpdb->get_charset_collate(); + $sql = "CREATE TABLE $table ( + id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + user_id bigint(20) UNSIGNED NOT NULL, + ip_address varchar(45) NOT NULL, + country varchar(10) NOT NULL DEFAULT 'Unknown', + login_at datetime NOT NULL, + PRIMARY KEY (id), + KEY user_id (user_id), + KEY login_at (login_at) + ) $collate;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); +} + +add_action('wp_login', __NAMESPACE__ . '\\acore_log_login', 10, 2); + +function acore_log_login($user_login, $user) { + if (get_option('acore_security_logging', '0') !== '1') { + return; + } + + global $wpdb; + + $ip = acore_resolve_client_ip(); + $country = acore_lookup_country($ip); + + $wpdb->insert( + $wpdb->prefix . 'acore_login_history', + [ + 'user_id' => $user->ID, + 'ip_address' => $ip, + 'country' => $country, + 'login_at' => current_time('mysql'), + ], + ['%d', '%s', '%s', '%s'] + ); +} + +function acore_resolve_client_ip() { + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $ip = trim($parts[0]); + } else { + $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + } + return sanitize_text_field($ip); +} + +function acore_lookup_country($ip) { + $private_ranges = ['127.', '10.', '192.168.', '172.16.', '172.17.', '172.18.', + '172.19.', '172.20.', '172.21.', '172.22.', '172.23.', '172.24.', + '172.25.', '172.26.', '172.27.', '172.28.', '172.29.', '172.30.', + '172.31.', '::1']; + + foreach ($private_ranges as $range) { + if (strpos($ip, $range) === 0) { + return 'Local'; + } + } + + $response = wp_remote_get("http://ip-api.com/json/{$ip}?fields=status,countryCode", [ + 'timeout' => 3, + 'sslverify' => false, + ]); + + if (is_wp_error($response)) { + return 'Unknown'; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if (isset($data['status'], $data['countryCode']) && $data['status'] === 'success') { + return $data['countryCode']; + } + + return 'Unknown'; +} + +function acore_get_login_history($user_id, $limit = 50) { + global $wpdb; + $table = $wpdb->prefix . 'acore_login_history'; + return $wpdb->get_results($wpdb->prepare( + "SELECT ip_address, country, login_at FROM $table WHERE user_id = %d ORDER BY login_at DESC LIMIT %d", + $user_id, + $limit + )); +} + +function acore_format_connection_date($datetime_str) { + $dt = new \DateTime($datetime_str); + $day = (int) $dt->format('j'); + + if (in_array($day % 100, [11, 12, 13])) { + $suffix = 'th'; + } else { + switch ($day % 10) { + case 1: $suffix = 'st'; break; + case 2: $suffix = 'nd'; break; + case 3: $suffix = 'rd'; break; + default: $suffix = 'th'; + } + } + + return sprintf('%d%s of %s, %s at %s', + $day, + $suffix, + $dt->format('F'), + $dt->format('Y'), + $dt->format('H:i') + ); +} diff --git a/src/acore-wp-plugin/src/Hooks/User/Include.php b/src/acore-wp-plugin/src/Hooks/User/Include.php index 17c75d439..bf6260e27 100644 --- a/src/acore-wp-plugin/src/Hooks/User/Include.php +++ b/src/acore-wp-plugin/src/Hooks/User/Include.php @@ -2,4 +2,5 @@ namespace ACore\Hooks\User; +require_once __DIR__ . "/ConnectionLogger.php"; require_once __DIR__ . "/User.php"; diff --git a/src/acore-wp-plugin/src/Hooks/User/User.php b/src/acore-wp-plugin/src/Hooks/User/User.php index 8148212b2..e63b772f2 100644 --- a/src/acore-wp-plugin/src/Hooks/User/User.php +++ b/src/acore-wp-plugin/src/Hooks/User/User.php @@ -108,6 +108,7 @@ function user_profile_update($user_id, $old_user_data) if ($result instanceof \Exception) { die(sprintf(__("#2 ACore Error: Game server error: %s", 'acore-wp-plugin'), $result->getMessage())); } + update_user_meta($user_id, 'acore_password_changed_at', current_time('mysql')); } } @@ -132,6 +133,7 @@ function user_password_reset($user, $new_pass) if ($result instanceof \Exception) { die(sprintf(__("#1 ACore Error: Game server error: %s", 'acore-wp-plugin'), $result->getMessage())); } + update_user_meta($user->ID, 'acore_password_changed_at', current_time('mysql')); } add_action('password_reset', __NAMESPACE__ . '\user_password_reset', 10, 2); @@ -564,3 +566,99 @@ function validPasswordChars(password) { } add_action('login_enqueue_scripts', __NAMESPACE__ . '\login_checks'); + +add_action('show_user_profile', __NAMESPACE__ . '\acore_profile_recent_connections'); + +function acore_profile_recent_connections($user) { + $security_url = admin_url('profile.php?page=' . ACORE_SLUG . '-security'); + $rows = \ACore\Hooks\User\acore_get_login_history($user->ID, 20); + ?> +

+ +

+ + + + + + + + + + + + + + + + + + +
ip_address) ?>country) ?>login_at)) ?>
+

+ + + +

+ callbacks as $priority => $callbacks) { + foreach ($callbacks as $key => $callback) { + $func = $callback['function']; + $identifier = ''; + + if (is_array($func) && is_object($func[0])) { + $identifier = get_class($func[0]); + } elseif (is_array($func) && is_string($func[0])) { + $identifier = $func[0]; + } elseif (is_string($func)) { + $identifier = $func; + } + + if ($identifier && ( + stripos($identifier, 'WP2FA') !== false || + stripos($identifier, 'wp_2fa') !== false || + stripos($identifier, 'Two_Factor') !== false + )) { + unset($wp_filter[$hook]->callbacks[$priority][$key]); + } + } + } + } +} + +add_action('admin_head-profile.php', __NAMESPACE__ . '\acore_hide_wp_password_section'); + +function acore_hide_wp_password_section() { + $security_url = admin_url('profile.php?page=' . ACORE_SLUG . '-security'); + ?> + + + 'mycred', 'required' => true, ), - + array( + 'name' => 'WP 2FA', + 'slug' => 'wp-2fa', + 'required' => true, + ), + // ); diff --git a/src/acore-wp-plugin/src/Manager/Opts.php b/src/acore-wp-plugin/src/Manager/Opts.php index d94880216..6e2f256e3 100644 --- a/src/acore-wp-plugin/src/Manager/Opts.php +++ b/src/acore-wp-plugin/src/Manager/Opts.php @@ -47,6 +47,7 @@ class Opts { [81, 360], // else, 360 days ]; public $acore_name_unlock_allowed_banned_names_table=""; + public $acore_security_logging="0"; public function __get($property) { if (property_exists($this, $property)) { From ebdcf41593e535e684f5c4083f7faf234c5c82e2 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 15:19:47 +0100 Subject: [PATCH 02/47] Config for Allow previous used passwords --- src/acore-wp-plugin/src/Manager/Opts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/acore-wp-plugin/src/Manager/Opts.php b/src/acore-wp-plugin/src/Manager/Opts.php index 6e2f256e3..718707e43 100644 --- a/src/acore-wp-plugin/src/Manager/Opts.php +++ b/src/acore-wp-plugin/src/Manager/Opts.php @@ -48,6 +48,7 @@ class Opts { ]; public $acore_name_unlock_allowed_banned_names_table=""; public $acore_security_logging="0"; + public $acore_allow_old_passwords="0"; public function __get($property) { if (property_exists($this, $property)) { From 0f2a45d17fef54a61f6120a0795ae934993fa5e0 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 15:20:03 +0100 Subject: [PATCH 03/47] Clear the previous description --- .../src/Components/AdminPanel/Pages/Tools.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index 03497dab8..1bd5188a9 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -62,7 +62,17 @@ -

When enabled, login IPs and timestamps are recorded for the Security page.

+ + + + + + + + acore_resurrection_scroll != '1') echo 'style="display:none;"'?>> From 82433215b063f8f8945f849525b5c241ee91b065 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 15:22:46 +0100 Subject: [PATCH 04/47] PasswordHandler creation In the scenarios where the user tried to change their current password to older one (for the first time) it would log them out, never was an issue before as we didn't prevent it, the previous implementation has been reworked to make the entire check happens before the page load --- .../Components/UserPanel/UserController.php | 40 +------- .../src/Hooks/User/Include.php | 1 + .../src/Hooks/User/PasswordHandler.php | 95 +++++++++++++++++++ 3 files changed, 99 insertions(+), 37 deletions(-) create mode 100644 src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php index 63bd0493c..2841d4d84 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php @@ -4,7 +4,6 @@ use ACore\Manager\Opts; use ACore\Manager\ACoreServices; -use ACore\Manager\UserValidator; use ACore\Components\UserPanel\UserView; class UserController { @@ -159,44 +158,11 @@ public function showItemRestorationPage() { public function showSecurityPage() { $user = wp_get_current_user(); - $passwordMessage = null; - - if (isset($_POST['acore_change_password'])) { - $nonce = $_POST['acore_pw_nonce'] ?? ''; - if (!wp_verify_nonce($nonce, 'acore_security_change_password')) { - $passwordMessage = ['type' => 'error', 'text' => __('Security check failed.', 'acore-wp-plugin')]; - } else { - $oldPass = $_POST['acore_old_pass'] ?? ''; - $newPass = $_POST['acore_new_pass'] ?? ''; - $confirmPass = $_POST['acore_confirm_pass'] ?? ''; - - if (!wp_check_password($oldPass, $user->user_pass, $user->ID)) { - $passwordMessage = ['type' => 'error', 'text' => __('Current password is incorrect.', 'acore-wp-plugin')]; - } elseif ($newPass !== $confirmPass) { - $passwordMessage = ['type' => 'error', 'text' => __('New passwords do not match.', 'acore-wp-plugin')]; - } else { - $validation = UserValidator::validatePassword($newPass); - if ($validation !== true) { - $passwordMessage = ['type' => 'error', 'text' => $validation]; - } else { - wp_set_password($newPass, $user->ID); - update_user_meta($user->ID, 'acore_password_changed_at', current_time('mysql')); - - try { - $soap = ACoreServices::I()->getAccountSoap(); - $soap->setAccountPassword($user->user_login, $newPass); - } catch (\Exception $e) { - } - - $passwordMessage = ['type' => 'success', 'text' => __('Password updated successfully.', 'acore-wp-plugin')]; - } - } - } - } + $passwordMessage = \ACore\Hooks\User\acore_pw_get_message($user->ID); $passwordChangedAt = get_user_meta($user->ID, 'acore_password_changed_at', true); - $twoFaData = $this->getTwoFaData($user); - $connections = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); + $twoFaData = $this->getTwoFaData($user); + $connections = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); echo $this->getView()->getSecurityRender($connections, $passwordChangedAt, $twoFaData, $passwordMessage); } diff --git a/src/acore-wp-plugin/src/Hooks/User/Include.php b/src/acore-wp-plugin/src/Hooks/User/Include.php index bf6260e27..06f7143dc 100644 --- a/src/acore-wp-plugin/src/Hooks/User/Include.php +++ b/src/acore-wp-plugin/src/Hooks/User/Include.php @@ -3,4 +3,5 @@ namespace ACore\Hooks\User; require_once __DIR__ . "/ConnectionLogger.php"; +require_once __DIR__ . "/PasswordHandler.php"; require_once __DIR__ . "/User.php"; diff --git a/src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php b/src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php new file mode 100644 index 000000000..cac38e033 --- /dev/null +++ b/src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php @@ -0,0 +1,95 @@ +ID, 'error', __('Security check failed.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; + } + + $oldPass = $_POST['acore_old_pass'] ?? ''; + $newPass = $_POST['acore_new_pass'] ?? ''; + $confirmPass = $_POST['acore_confirm_pass'] ?? ''; + + if (!wp_check_password($oldPass, $user->user_pass, $user->ID)) { + acore_pw_set_message($user->ID, 'error', __('Current password is incorrect.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; + } + + if ($newPass !== $confirmPass) { + acore_pw_set_message($user->ID, 'error', __('New passwords do not match.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; + } + + if (wp_check_password($newPass, $user->user_pass, $user->ID)) { + acore_pw_set_message($user->ID, 'error', __('New password must be different from your current password.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; + } + + $validation = UserValidator::validatePassword($newPass); + if ($validation !== true) { + acore_pw_set_message($user->ID, 'error', $validation); + wp_redirect($security_url); + exit; + } + + if (get_option('acore_allow_old_passwords', '0') !== '1') { + $history = get_user_meta($user->ID, 'acore_password_history', true) ?: []; + foreach ($history as $oldHash) { + if (wp_check_password($newPass, $oldHash, $user->ID)) { + acore_pw_set_message($user->ID, 'error', __('This password has been used before. Please choose a different one.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; + } + } + } + + $history = get_user_meta($user->ID, 'acore_password_history', true) ?: []; + array_unshift($history, $user->user_pass); + update_user_meta($user->ID, 'acore_password_history', array_slice($history, 0, 10)); + + wp_set_password($newPass, $user->ID); + update_user_meta($user->ID, 'acore_password_changed_at', current_time('mysql')); + + wp_set_current_user($user->ID); + wp_set_auth_cookie($user->ID, true); + + try { + $soap = ACoreServices::I()->getAccountSoap(); + $soap->setAccountPassword($user->user_login, $newPass); + } catch (\Exception $e) { + } + + acore_pw_set_message($user->ID, 'success', __('Password updated successfully.', 'acore-wp-plugin')); + wp_redirect($security_url); + exit; +} + +function acore_pw_set_message($user_id, $type, $text) { + set_transient('acore_pw_msg_' . $user_id, ['type' => $type, 'text' => $text], 60); +} + +function acore_pw_get_message($user_id) { + $msg = get_transient('acore_pw_msg_' . $user_id); + if ($msg) { + delete_transient('acore_pw_msg_' . $user_id); + } + return $msg ?: null; +} From 7d022302ff4fab47a137b3aa112f8b44477aa881 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 15:24:43 +0100 Subject: [PATCH 05/47] Show the user before submit if the new pw matches or not --- .../UserPanel/Pages/SecurityPage.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php index 8e94d5b23..c170ca20a 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php @@ -229,6 +229,34 @@ class="regular-text" autocomplete="new-password" /> if (btn) btn.style.display = 'none'; + var newPass = document.getElementById('acore_new_pass'); + var confirmPass = document.getElementById('acore_confirm_pass'); + var saveBtn = document.querySelector('#acore-password-form .button-primary'); + var matchHint = document.createElement('p'); + matchHint.className = 'description'; + matchHint.style.marginTop = '4px'; + if (confirmPass) confirmPass.closest('td').appendChild(matchHint); + + function checkMatch() { + if (!confirmPass.value) { + matchHint.textContent = ''; + saveBtn.disabled = false; + return; + } + if (newPass.value === confirmPass.value) { + matchHint.style.color = '#00a32a'; + matchHint.textContent = ''; + saveBtn.disabled = false; + } else { + matchHint.style.color = '#d63638'; + matchHint.textContent = ''; + saveBtn.disabled = true; + } + } + + if (newPass) newPass.addEventListener('input', checkMatch); + if (confirmPass) confirmPass.addEventListener('input', checkMatch); + var expandBtn = document.getElementById('acore-expand-connections'); if (expandBtn) { expandBtn.addEventListener('click', function() { From 4eaa629da5dc04d3b30c69dd00b65e8a6d1068f2 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 17:14:03 +0100 Subject: [PATCH 06/47] Added DarkMode.php require. --- src/acore-wp-plugin/src/boot.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/acore-wp-plugin/src/boot.php b/src/acore-wp-plugin/src/boot.php index 7a280f60a..6eb5cfc10 100644 --- a/src/acore-wp-plugin/src/boot.php +++ b/src/acore-wp-plugin/src/boot.php @@ -18,6 +18,7 @@ require_once ACORE_PATH_PLG . 'src/Hooks/Subscriptions/sync_subscription.php'; require_once ACORE_PATH_PLG . 'src/Hooks/Various/tgmplugin_activator.php'; +require_once ACORE_PATH_PLG . 'src/Hooks/Various/DarkMode.php'; require_once ACORE_PATH_PLG . 'src/Hooks/User/Include.php'; From 4f531cc04b5102c82959e9c80c98de7d7c829a8c Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 17:14:20 +0100 Subject: [PATCH 07/47] Shared Styles for Each Profile Page --- src/acore-wp-plugin/web/assets/css/main.css | 94 +++++++++++++-------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/src/acore-wp-plugin/web/assets/css/main.css b/src/acore-wp-plugin/web/assets/css/main.css index f36f2ab4e..8df9bc5ef 100644 --- a/src/acore-wp-plugin/web/assets/css/main.css +++ b/src/acore-wp-plugin/web/assets/css/main.css @@ -17,62 +17,90 @@ body { background-color: var(--bs-teal) !important; } -/* start char-order */ -#acore-characters-order .menu-item-handle { +/* start acore-char-list — shared character row layout */ +.acore-char-list { + list-style: none; + padding: 0; + margin: 0; +} + +.acore-char-list li { + margin-bottom: 4px; +} + +.acore-char-list input[type="hidden"], +.acore-char-list input[name="characterorder[]"] { + display: none; +} + +.acore-char-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 46px; + padding: 6px 12px; + box-sizing: border-box; + background: #fff; + border: 1px solid #dcdcde; + border-radius: 2px; +} + +.acore-char-name { + flex: 1; + font-weight: 500; } -#acore-characters-order .item-type { - padding: 0px; - margin-top: 5px; +.acore-char-meta { + display: flex; + align-items: center; + gap: 6px; + color: #646970; } -#acore-characters-order .item-type img { +.acore-char-meta img { display: inline-block; vertical-align: middle; height: 32px; + width: 32px; transform: translateZ(0); /* prevent blurry image on chrome */ } -#acore-characters-order input { - display: none; +/* sortable drag cursor on order list */ +#acore-characters-order .acore-char-row { + cursor: grab; +} + +#acore-characters-order .acore-char-row:active { + cursor: grabbing; } -/* end char order */ +/* click cursor on mail return */ +#acore-characters-mail .acore-char-row { + cursor: pointer; +} -/* start char-unstuck */ -#acore-characters-unstuck .menu-item-handle { - cursor: default; +#acore-characters-mail .acore-char-row.active { + background: #e8f0fe; + border-color: #1f6feb; } +/* end acore-char-list */ +/* start unstuck button */ .unstuck-button { background-size: cover; border: none; color: transparent; cursor: pointer; + background: none; + padding: 0; } .unstuck-button:disabled { - opacity: 0.5; /* Adjust the opacity for disabled state */ + opacity: 0.5; cursor: not-allowed; } - -#acore-characters-unstuck .item-type { - padding: 0px; - margin-top: 5px; -} - -#acore-characters-unstuck .item-type img { - display: inline-block; - vertical-align: middle; - height: 32px; - transform: translateZ(0); /* prevent blurry image on chrome */ -} - -#acore-characters-unstuck input { - display: none; -} - -/* end char unstuck */ +/* end unstuck button */ /* start raf progress view */ @@ -232,7 +260,7 @@ body { gap: 6px; margin-top: 8px; padding: 6px 10px; - background: #f6f7f7; + background: #f0f0f1; border-radius: 4px; } @@ -257,7 +285,7 @@ body { #mail-return-items .mail-items { margin-top: 8px; padding: 6px 10px; - background: #f6f7f7; + background: #2c3338; border-radius: 4px; font-size: 13px; } From c2b0a221306b5c45c7871e92461224249550346d Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 17:14:56 +0100 Subject: [PATCH 08/47] Use the existing selector of characters and make it clickable --- .../web/assets/mail-return/mail-return.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/acore-wp-plugin/web/assets/mail-return/mail-return.js b/src/acore-wp-plugin/web/assets/mail-return/mail-return.js index 85c52d286..fbcc5a80b 100644 --- a/src/acore-wp-plugin/web/assets/mail-return/mail-return.js +++ b/src/acore-wp-plugin/web/assets/mail-return/mail-return.js @@ -1,15 +1,14 @@ jQuery(document).ready(function () { - jQuery("#mail-return-char-select").on("change", function () { - var charGuid = jQuery(this).val(); + jQuery("#acore-characters-mail").on("click", ".acore-char-card", function () { + var card = jQuery(this); + var charGuid = card.data("char-guid"); var mailList = jQuery("#mail-return-list"); var mailItems = jQuery("#mail-return-items"); var emptyMsg = jQuery("#mail-return-empty"); var loading = jQuery("#mail-return-loading"); - if (!charGuid) { - mailList.hide(); - return; - } + jQuery(".acore-char-card").removeClass("active"); + card.addClass("active"); loading.show(); mailList.hide(); From d23d348a427e0e40c4f31212c3d863a9fafab01d Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 17:16:52 +0100 Subject: [PATCH 09/47] Dark mode and dark / white button --- .../src/Hooks/Various/DarkMode.php | 113 ++++++ .../web/assets/css/dark-mode.css | 368 ++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 src/acore-wp-plugin/src/Hooks/Various/DarkMode.php create mode 100644 src/acore-wp-plugin/web/assets/css/dark-mode.css diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php new file mode 100644 index 000000000..a76ca45df --- /dev/null +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -0,0 +1,113 @@ +add_node([ + 'id' => 'acore-dark-mode', + 'parent' => 'top-secondary', + 'title' => '' . ($is_dark ? '☀' : '☾') . '', + 'href' => '#', + 'meta' => ['title' => $is_dark ? 'Switch to light mode' : 'Switch to dark mode'], + ]); +} + +add_filter('admin_body_class', __NAMESPACE__ . '\acore_dark_mode_body_class'); + +function acore_dark_mode_body_class($classes) { + if (get_user_meta(get_current_user_id(), 'acore_dark_mode', true) === '1') { + $classes .= ' acore-dark-mode'; + } + return $classes; +} + +add_action('admin_enqueue_scripts', __NAMESPACE__ . '\acore_dark_mode_enqueue'); + +function acore_dark_mode_enqueue() { + wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', [], '1.2'); + + $nonce = wp_create_nonce('acore_dark_mode'); + wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); +} + +/* + * Late inline + $new === '1']); +} + +function acore_dark_mode_js($nonce) { + return << .ab-item').on('click', function(e) { + e.preventDefault(); + $.post(ajaxurl, { action: 'acore_toggle_dark_mode', nonce: '{$nonce}' }, function(res) { + if (!res.success) return; + var dark = res.data.dark; + $('body').toggleClass('acore-dark-mode', dark); + $('#acore-dm-icon').html(dark ? '☀' : '☾'); + $(this).attr('title', dark ? 'Switch to light mode' : 'Switch to dark mode'); + }); + }); +}); +JS; +} diff --git a/src/acore-wp-plugin/web/assets/css/dark-mode.css b/src/acore-wp-plugin/web/assets/css/dark-mode.css new file mode 100644 index 000000000..29054c857 --- /dev/null +++ b/src/acore-wp-plugin/web/assets/css/dark-mode.css @@ -0,0 +1,368 @@ +/* +Acore dark mode overrides +*/ + +/* --- admin bar toggle icon --- */ +#wp-admin-bar-acore-dark-mode > .ab-item { + padding: 0 8px !important; + cursor: pointer; + display: flex !important; + align-items: center; + height: 32px; +} + +#wp-admin-bar-acore-dark-mode > .ab-item #acore-dm-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + font-size: 18px; + line-height: 1; + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 5px; + padding: 2px; +} + +/* --- Bootstrap 5 CSS variable overrides (must come before class rules) --- */ +body.acore-dark-mode { + --bs-body-bg: #0d1117; + --bs-body-color: #c9d1d9; + --bs-card-bg: #161b22; + --bs-card-border-color: #30363d; + --bs-card-cap-bg: #0d1117; + --bs-border-color: #30363d; + --bs-secondary-color: #8b949e; + --bs-link-color: #58a6ff; + --bs-link-hover-color: #79c0ff; + --bs-list-group-bg: #161b22; + --bs-list-group-border-color: #30363d; + --bs-list-group-color: #c9d1d9; +} + +/* --- base --- */ +body.acore-dark-mode, +body.acore-dark-mode #wpwrap { + background: #0d1117; + color: #c9d1d9; +} + +body.acore-dark-mode #wpcontent, +body.acore-dark-mode #wpfooter { + background: #0d1117; +} + +body.acore-dark-mode #wpbody-content { + background: #0d1117; +} + +/* --- headings & text --- */ +body.acore-dark-mode h1, +body.acore-dark-mode h2, +body.acore-dark-mode h3, +body.acore-dark-mode h4, +body.acore-dark-mode h5, +body.acore-dark-mode p, +body.acore-dark-mode label, +body.acore-dark-mode span, +body.acore-dark-mode li { + color: #c9d1d9; +} + +body.acore-dark-mode .description, +body.acore-dark-mode .text-muted { + color: #8b949e !important; +} + +body.acore-dark-mode hr { + border-color: #30363d; +} + +/* --- postboxes (WP core) --- */ +body.acore-dark-mode .postbox, +body.acore-dark-mode #poststuff .postbox { + background: #161b22; + border-color: #30363d; +} + +body.acore-dark-mode .postbox-header { + background: #161b22; + border-bottom-color: #30363d; +} + +body.acore-dark-mode .postbox-header h2, +body.acore-dark-mode .postbox-header .hndle { + color: #c9d1d9; +} + +body.acore-dark-mode .postbox .inside { + color: #c9d1d9; +} + +/* --- Bootstrap card --- */ +body.acore-dark-mode .card { + background-color: #161b22 !important; + border-color: #30363d !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .card-body { + background-color: #161b22 !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .card-header { + background-color: #0d1117 !important; + border-bottom-color: #30363d !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .card-title { + color: #c9d1d9 !important; +} + +/* --- Character list rows --- */ +body.acore-dark-mode .acore-char-row { + background: #1c2128 !important; + border-color: #30363d !important; +} + +body.acore-dark-mode .acore-char-name { + color: #c9d1d9 !important; +} + +body.acore-dark-mode .acore-char-meta { + color: #8b949e !important; +} + +body.acore-dark-mode .acore-char-row:hover { + background: #262d36 !important; +} + +body.acore-dark-mode #acore-characters-mail .acore-char-row.active { + background: #1f3a5c !important; + border-color: #58a6ff !important; +} + +/* --- Bootstrap form-select --- */ +body.acore-dark-mode .form-select { + background-color: #0d1117 !important; + border-color: #30363d !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .form-select:focus { + border-color: #58a6ff !important; + box-shadow: 0 0 0 0.2rem rgba(88, 166, 255, 0.25) !important; +} + +/* --- form elements --- */ +body.acore-dark-mode input[type="text"], +body.acore-dark-mode input[type="password"], +body.acore-dark-mode input[type="email"], +body.acore-dark-mode input[type="number"], +body.acore-dark-mode input[type="search"], +body.acore-dark-mode input[type="url"], +body.acore-dark-mode select, +body.acore-dark-mode textarea { + background-color: #0d1117; + border-color: #30363d; + color: #c9d1d9; + box-shadow: none; +} + +body.acore-dark-mode input[type="text"]:focus, +body.acore-dark-mode input[type="password"]:focus, +body.acore-dark-mode select:focus, +body.acore-dark-mode textarea:focus { + border-color: #58a6ff; + box-shadow: 0 0 0 1px #58a6ff; + outline: none; +} + +body.acore-dark-mode .form-table th { + color: #8b949e; +} + +body.acore-dark-mode .form-table td { + color: #c9d1d9; +} + +/* --- mail entries --- */ +body.acore-dark-mode .mail-entry { + background: #161b22 !important; + border-color: #30363d !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .mail-recipient, +body.acore-dark-mode .mail-items { + background: #0d1117 !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .mail-header, +body.acore-dark-mode .mail-meta, +body.acore-dark-mode .mail-subject, +body.acore-dark-mode .mail-money, +body.acore-dark-mode .recipient-name { + color: #c9d1d9 !important; +} + +/* --- tables --- */ +body.acore-dark-mode .widefat, +body.acore-dark-mode .wp-list-table { + background: #161b22; + border-color: #30363d; + color: #c9d1d9; +} + +body.acore-dark-mode .widefat thead th, +body.acore-dark-mode .widefat tfoot th, +body.acore-dark-mode .wp-list-table thead th { + background: #0d1117; + color: #8b949e; + border-bottom-color: #30363d; +} + +body.acore-dark-mode .widefat td, +body.acore-dark-mode .widefat th, +body.acore-dark-mode .wp-list-table td { + color: #c9d1d9; + border-bottom-color: #21262d; +} + +body.acore-dark-mode .striped > tbody > :nth-child(odd) > td, +body.acore-dark-mode .striped > tbody > :nth-child(odd) > th, +body.acore-dark-mode .alternate { + background: #1c2128; +} + +/* --- buttons --- */ +body.acore-dark-mode .button, +body.acore-dark-mode .button-secondary { + background: #21262d; + border-color: #30363d; + color: #c9d1d9; + text-shadow: none; + box-shadow: none; +} + +body.acore-dark-mode .button:hover, +body.acore-dark-mode .button-secondary:hover { + background: #30363d; + border-color: #8b949e; + color: #f0f6fc; +} + +body.acore-dark-mode .button-primary { + background: #1f6feb; + border-color: #1f6feb; + color: #fff; + text-shadow: none; + box-shadow: none; +} + +body.acore-dark-mode .button-primary:hover { + background: #388bfd; + border-color: #388bfd; +} + +body.acore-dark-mode .button-primary:disabled, +body.acore-dark-mode .button-primary[disabled] { + background: #1f6feb; + opacity: 0.5; +} + +/* --- Bootstrap buttons --- */ +body.acore-dark-mode .btn-primary { + background-color: #1f6feb !important; + border-color: #1f6feb !important; +} + +body.acore-dark-mode .btn-primary:hover { + background-color: #388bfd !important; + border-color: #388bfd !important; +} + +body.acore-dark-mode .btn-outline-secondary, +body.acore-dark-mode .btn-secondary { + background-color: #21262d !important; + border-color: #30363d !important; + color: #c9d1d9 !important; +} + +/* --- notices --- */ +body.acore-dark-mode .notice, +body.acore-dark-mode div.updated, +body.acore-dark-mode div.error { + background: #161b22; + border-color: #30363d; + color: #c9d1d9; +} + +body.acore-dark-mode .notice-success, +body.acore-dark-mode .updated { + border-left-color: #3fb950; +} + +body.acore-dark-mode .notice-error, +body.acore-dark-mode div.error { + border-left-color: #f85149; +} + +body.acore-dark-mode .notice-warning { + border-left-color: #d29922; +} + +body.acore-dark-mode .notice p, +body.acore-dark-mode div.updated p, +body.acore-dark-mode div.error p { + color: #c9d1d9; +} + +/* --- links --- */ +body.acore-dark-mode a { + color: #58a6ff; +} + +body.acore-dark-mode a:hover { + color: #79c0ff; +} + +/* --- footer --- */ +body.acore-dark-mode #wpfooter { + border-top-color: #30363d; + color: #8b949e; +} + +body.acore-dark-mode #wpfooter a { + color: #58a6ff; +} + +/* --- myCred widgets --- */ +body.acore-dark-mode [id^="mycred"], +body.acore-dark-mode [class^="mycred"], +body.acore-dark-mode .mycred-wrap, +body.acore-dark-mode .mycred-widget { + background: #161b22 !important; + border-color: #30363d !important; + color: #c9d1d9 !important; +} + +body.acore-dark-mode .mycred-history, +body.acore-dark-mode .mycred-history *, +body.acore-dark-mode #mycred-log-wrap, +body.acore-dark-mode #mycred-log-wrap * { + background: #161b22 !important; + color: #c9d1d9 !important; + border-color: #30363d !important; +} + +/* WP profile page balance box */ +body.acore-dark-mode #mycred-user-balance, +body.acore-dark-mode .user-profile-mycred, +body.acore-dark-mode td.mycred-field, +body.acore-dark-mode th.mycred-field { + color: #c9d1d9 !important; +} From e279419535384dc34bfb625cd7e11d813a71b530 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 17:31:22 +0100 Subject: [PATCH 10/47] Add a order default button (when the user gets a charc stuck in limbo) --- .../CharactersMenu/CharactersController.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php index 98dee522d..daf4e1644 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php @@ -14,10 +14,17 @@ public function __construct() { public function loadHome() { if ($_SERVER["REQUEST_METHOD"] == "POST") { - $this->saveCharacterOrder(); - ?> -

Character settings succesfully saved.

- resetCharacterOrder(); + ?> +

Character order reset successfully.

+ saveCharacterOrder(); + ?> +

Character settings succesfully saved.

+ getAcoreAccountId(); @@ -38,6 +45,16 @@ public function getView() { return $this->view; } + private function resetCharacterOrder() { + $accId = ACoreServices::I()->getAcoreAccountId(); + $conn = ACoreServices::I()->getCharacterEm()->getConnection(); + $stmt = $conn->prepare( + "UPDATE `characters` SET `order` = NULL WHERE `account` = ? AND `deleteDate` IS NULL" + ); + $stmt->bindValue(1, $accId); + $stmt->executeQuery(); + } + private function saveCharacterOrder() { if (!isset($_POST) || !isset($_POST["characterorder"])) { return; From 49405ed922d9a7e2496f7df00c3da9c0082aa0c4 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 18:10:32 +0100 Subject: [PATCH 11/47] Class Colours --- .../src/Utils/AcoreCharColors.php | 57 +++++++++++++++++++ src/acore-wp-plugin/src/boot.php | 1 + 2 files changed, 58 insertions(+) create mode 100644 src/acore-wp-plugin/src/Utils/AcoreCharColors.php diff --git a/src/acore-wp-plugin/src/Utils/AcoreCharColors.php b/src/acore-wp-plugin/src/Utils/AcoreCharColors.php new file mode 100644 index 000000000..a132c11d2 --- /dev/null +++ b/src/acore-wp-plugin/src/Utils/AcoreCharColors.php @@ -0,0 +1,57 @@ + 'Warrior', + 2 => 'Paladin', + 3 => 'Hunter', + 4 => 'Rogue', + 5 => 'Priest', + 6 => 'Death Knight', + 7 => 'Shaman', + 8 => 'Mage', + 9 => 'Warlock', + 11 => 'Druid', + ]; + + const CLASS_COLORS = [ + 1 => ['light' => '#C69B6D', 'dark' => '#C69B6D'], // Warrior + 2 => ['light' => '#F48CBA', 'dark' => '#F48CBA'], // Paladin + 3 => ['light' => '#AAD372', 'dark' => '#AAD372'], // Hunter + 4 => ['light' => '#C8A800', 'dark' => '#FFF468'], // Rogue (yellow → darkened for light bg) + 5 => ['light' => '#909090', 'dark' => '#E0E0E0'], // Priest (white → grey for light bg) + 6 => ['light' => '#C41E3A', 'dark' => '#FF3355'], // Death Knight + 7 => ['light' => '#0070DD', 'dark' => '#3399FF'], // Shaman + 8 => ['light' => '#3FC7EB', 'dark' => '#3FC7EB'], // Mage + 9 => ['light' => '#8788EE', 'dark' => '#9A9BFF'], // Warlock + 11 => ['light' => '#FF7C0A', 'dark' => '#FF7C0A'], // Druid + ]; + + const FALLBACK_LIGHT = '#646970'; + const FALLBACK_DARK = '#8b949e'; + + public static function getLight(int $classId): string { + return self::CLASS_COLORS[$classId]['light'] ?? self::FALLBACK_LIGHT; + } + + public static function getDark(int $classId): string { + return self::CLASS_COLORS[$classId]['dark'] ?? self::FALLBACK_DARK; + } + + /** + * Returns the inline style string to set CSS variables on a char row. + */ + public static function rowStyle(int $classId): string { + $light = self::getLight($classId); + $dark = self::getDark($classId); + return "--cls-light:{$light};--cls-dark:{$dark};"; + } +} diff --git a/src/acore-wp-plugin/src/boot.php b/src/acore-wp-plugin/src/boot.php index 6eb5cfc10..a7b832cf3 100644 --- a/src/acore-wp-plugin/src/boot.php +++ b/src/acore-wp-plugin/src/boot.php @@ -3,6 +3,7 @@ define('FS_METHOD', 'direct'); require_once ACORE_PATH_PLG . 'src/Utils/AcoreUtils.php'; +require_once ACORE_PATH_PLG . 'src/Utils/AcoreCharColors.php'; require_once ACORE_PATH_PLG . 'src/Deps/class-tgm-plugin-activation.php'; From fcc5c99c0700c803ae1edac346b8a402dfa7dedf Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 6 Jun 2026 18:10:51 +0100 Subject: [PATCH 12/47] Dark mode additions --- .../src/Hooks/Various/DarkMode.php | 10 +- .../web/assets/css/dark-mode.css | 95 ++++++++++++++++++- src/acore-wp-plugin/web/assets/css/main.css | 12 +++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php index a76ca45df..27b45ad4d 100644 --- a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -32,7 +32,7 @@ function acore_dark_mode_body_class($classes) { add_action('admin_enqueue_scripts', __NAMESPACE__ . '\acore_dark_mode_enqueue'); function acore_dark_mode_enqueue() { - wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', [], '1.2'); + wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', [], '1.7'); $nonce = wp_create_nonce('acore_dark_mode'); wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); @@ -51,11 +51,9 @@ function acore_dark_mode_late_styles() { } ?>
@@ -22,19 +63,17 @@
-
+ + +
-
- Worldserver integration -
+
World Server Integration

- + - + + + + + +
+
+
+ + +
+
+
+
Web Integration
+
+ + + + - + - acore_resurrection_scroll != '1') echo 'style="display:none;"'?>> - - - -
-
-
-
-
-
-
- Name Unlock Settings -
-
+
- Allowed banned names table (characters database): - -
-
+ +

Remove 2FA

+

+ Remove Website or In-game 2FA for any account. A warning is shown to the user until they re-enable it. +

- Inactivity Thresholds per Level: - - - - - - - - - -
Add
-
-
-
-
+ +

Website

+
+ + + +
+

+ + +

In-Game

+
+ + + +
+

+ +
+
+ + + -
- -
+

+ +

diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php index ec5467402..075eaaffe 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php @@ -23,24 +23,24 @@ public function getHomeRender($characters) { wp_enqueue_script('jquery-ui-sortable'); ?>
-

Characters Settings

-

Check some details and configure of your characters.

-
-

Order

-

Change the order in which the characters appear in your in-game character selection screen.

+

Order Character Screen

+

Change the order in which the characters appear in your in-game character selection screen, by dragging them, matches in-game position.


    -
  • + " title="">Level diff --git a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php index c62bf0cf9..dc046ffe7 100644 --- a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php +++ b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php @@ -29,7 +29,7 @@ public function getMailReturnRender($chars)
    - +
    @@ -69,34 +69,22 @@ public function getMailReturnRender($chars)
    -
    +
    - +
    -
    -
    Unread Sent Mails
    -
    +
    Unread Sent Mails
      -
      - -
      -
    - - - - - + +
    +
    + + acore_resurrection_scroll != '1') { + return false; + } + $csv = get_option('acore_modules_csv', ''); + $modules = $csv ? array_map('trim', explode(',', $csv)) : []; + return in_array('mod-resurrection-scroll', $modules); + } + function acore_resurrection_scroll_menu() { - if (Opts::I()->acore_resurrection_scroll == '1') { + if ($this->isAvailable()) { add_submenu_page('profile.php', 'Scroll of Resurrection', 'Scroll of Resurrection', 'read', ACORE_SLUG . '-resurrection-scroll', array($this, 'acore_resurrection_scroll_menu_page')); } } function acore_resurrection_scroll_menu_page() { + if (!$this->isAvailable()) { + wp_die(__('This feature is not available.')); + } $controller = new ResurrectionScrollController(); $controller->render(); } diff --git a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php index b6a325647..e37cef2f9 100644 --- a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php +++ b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php @@ -15,12 +15,237 @@ public static function AccountCount() { } +/** + * Pure-PHP TOTP validator (RFC 6238 / HOTP-SHA1). + * Accepts the Base32-encoded secret stored by WP2FA and a 6-digit token string. + * Validates against the current 30-second window +-1 step. + */ +function acore_totp_validate(string $base32Secret, string $rawToken): bool { + $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $input = strtoupper(rtrim($base32Secret, '=')); + $bits = ''; + foreach (str_split($input) as $char) { + $pos = strpos($alphabet, $char); + if ($pos === false) continue; + $bits .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT); + } + $secret = ''; + foreach (str_split($bits, 8) as $chunk) { + if (strlen($chunk) < 8) break; + $secret .= chr(bindec($chunk)); + } + if ($secret === '') return false; + + $token = (int) $rawToken; + $timestamp = (int) floor(time() / 30); + + for ($step = -1; $step <= 1; $step++) { + $t = $timestamp + $step; + $msg = "\x00\x00\x00\x00" . pack('N', $t); + $hash = hash_hmac('sha1', $msg, $secret, true); + $off = ord($hash[19]) & 0x0f; + $otp = ( + ((ord($hash[$off]) & 0x7f) << 24) | + ((ord($hash[$off+1]) & 0xff) << 16) | + ((ord($hash[$off+2]) & 0xff) << 8) | + (ord($hash[$off+3]) & 0xff) + ); + if (($otp % 1000000) === $token) return true; + } + return false; +} + add_action( 'rest_api_init', function () { register_rest_route( ACORE_SLUG . '/v1', 'server-info', array( 'methods' => 'GET', 'callback' => function( $request ) { - $data = ['message' => ServerInfoApi::serverInfo()]; - return $data; + $result = ServerInfoApi::serverInfo(); + $errorPatterns = [ + 'could not connect', 'connection refused', 'operation timed out', + 'error fetching', 'failed to enable', 'soap fault', 'not configured', + 'unable to connect', 'network unreachable', + ]; + foreach ($errorPatterns as $pattern) { + if (stripos($result, $pattern) !== false) { + return new \WP_Error('soap_error', $result, ['status' => 503]); + } + } + return ['message' => $result]; + } + ) ); + + $defaultRequirements = [['Scroll of Resurrection', 'mod-resurrection-scroll']]; + + register_rest_route( ACORE_SLUG . '/v1', 'server-module-requirements', array( + 'methods' => 'GET', + 'permission_callback' => function() { return current_user_can('manage_options'); }, + 'callback' => function( $request ) use ($defaultRequirements) { + return ['requirements' => get_option('acore_module_requirements', $defaultRequirements)]; + } + )); + + register_rest_route( ACORE_SLUG . '/v1', 'server-module-requirements', array( + 'methods' => 'POST', + 'permission_callback' => function() { return current_user_can('manage_options'); }, + 'callback' => function( $request ) { + $data = $request->get_json_params(); + $reqs = isset($data['requirements']) ? $data['requirements'] : []; + $clean = []; + foreach ($reqs as $row) { + if (is_array($row) && count($row) === 2) { + $clean[] = [sanitize_text_field($row[0]), sanitize_text_field($row[1])]; + } + } + update_option('acore_module_requirements', $clean); + return ['success' => true, 'requirements' => $clean]; + } + )); + + register_rest_route( ACORE_SLUG . '/v1', 'server-modules', array( + 'methods' => 'POST', + 'permission_callback' => function() { return current_user_can('manage_options'); }, + 'callback' => function( $request ) { + $raw = ACoreServices::I()->getServerSoap()->executeCommand('.server debug'); + $modules = []; + $capturing = false; + foreach (explode("\n", $raw) as $line) { + $line = trim($line); + if (!$capturing) { + if (stripos($line, 'List of enabled modules:') !== false) $capturing = true; + continue; + } + if (preg_match('/\b(mod-[a-zA-Z0-9_-]+)/', $line, $m)) $modules[] = $m[1]; + } + $csv = implode(',', $modules); + $timestamp = time(); + update_option('acore_modules_csv', $csv); + update_option('acore_modules_refreshed', $timestamp); + return ['modules' => $modules, 'csv' => $csv, 'refreshed' => $timestamp]; + } + ) ); + + // Admin: check 2FA status for any user + register_rest_route( ACORE_SLUG . '/v1', 'admin/2fa-check', array( + 'methods' => 'POST', + 'permission_callback' => function() { return current_user_can('manage_options'); }, + 'callback' => function( \WP_REST_Request $request ) { + $data = $request->get_json_params(); + $type = isset($data['type']) ? sanitize_text_field($data['type']) : ''; + $username = isset($data['username']) ? sanitize_text_field($data['username']) : ''; + if (!in_array($type, ['website', 'ingame'], true)) + return new \WP_Error('invalid_type', 'Invalid type.', ['status' => 400]); + if ($username === '') + return new \WP_Error('missing_username', 'Username is required.', ['status' => 400]); + $user = get_user_by('login', $username); + if (!$user) + return new \WP_Error('user_not_found', 'No WordPress account found with that username.', ['status' => 404]); + + $log = get_user_meta($user->ID, 'acore_2fa_admin_log', true); + $log = is_array($log) ? $log : []; + $lastRemoval = null; + foreach (array_reverse($log) as $entry) { + if ($entry['type'] === $type) { $lastRemoval = $entry; break; } + } + + if ($type === 'website') { + $totpKey = get_user_meta($user->ID, 'wp_2fa_totp_key', true); + $enabledMethods = get_user_meta($user->ID, 'wp_2fa_enabled_methods', true); + $active = !empty($totpKey) && ( + (is_array($enabledMethods) && in_array('totp', $enabledMethods, true)) || + $enabledMethods === 'totp' + ); + $resp = ['found' => true, 'username' => $user->user_login, 'active' => $active]; + if ($lastRemoval) $resp['last_removal'] = ['date' => wp_date('jS \o\f F, Y \a\t H:i', $lastRemoval['timestamp']), 'staff' => $lastRemoval['staff']]; + return $resp; + } + + try { + $conn = ACoreServices::I()->getAccountEm()->getConnection(); + $result = $conn->executeQuery('SELECT totp_secret FROM account WHERE username = ?', [strtoupper($username)]); + $row = $result->fetchAssociative(); + if (!$row) return new \WP_Error('no_game_account', 'No game account found for this user.', ['status' => 404]); + $resp = ['found' => true, 'username' => $user->user_login, 'active' => $row['totp_secret'] !== null]; + if ($lastRemoval) $resp['last_removal'] = ['date' => wp_date('jS \o\f F, Y \a\t H:i', $lastRemoval['timestamp']), 'staff' => $lastRemoval['staff']]; + return $resp; + } catch (\Exception $e) { + return new \WP_Error('db_error', 'Database error.', ['status' => 500]); + } + } + )); + + // Admin: remove 2FA for any user and log the action + register_rest_route( ACORE_SLUG . '/v1', 'admin/2fa-remove', array( + 'methods' => 'POST', + 'permission_callback' => function() { return current_user_can('manage_options'); }, + 'callback' => function( \WP_REST_Request $request ) { + $data = $request->get_json_params(); + $type = isset($data['type']) ? sanitize_text_field($data['type']) : ''; + $username = isset($data['username']) ? sanitize_text_field($data['username']) : ''; + if (!in_array($type, ['website', 'ingame'], true)) + return new \WP_Error('invalid_type', 'Invalid type.', ['status' => 400]); + if ($username === '') + return new \WP_Error('missing_username', 'Username is required.', ['status' => 400]); + $user = get_user_by('login', $username); + if (!$user) + return new \WP_Error('user_not_found', 'No WordPress account found with that username.', ['status' => 404]); + + if ($type === 'website') { + delete_user_meta($user->ID, 'wp_2fa_totp_key'); + delete_user_meta($user->ID, 'wp_2fa_enabled_methods'); + delete_user_meta($user->ID, 'wp_2fa_backup_methods_enabled'); + delete_user_meta($user->ID, 'wp_2fa_grace_period_expiry'); + delete_user_meta($user->ID, 'wp_2fa_user_setup_started_at'); + delete_user_meta($user->ID, 'wp_2fa_user_authenticated_methods'); + } else { + try { + $conn = ACoreServices::I()->getAccountEm()->getConnection(); + $rows = $conn->executeStatement('UPDATE account SET totp_secret = NULL WHERE username = ?', [strtoupper($username)]); + if ($rows === 0) return new \WP_Error('no_game_account', 'No game account found for this user.', ['status' => 404]); + } catch (\Exception $e) { + return new \WP_Error('db_error', 'Database error.', ['status' => 500]); + } + } + + $staff = wp_get_current_user(); + $now = time(); + $log = get_user_meta($user->ID, 'acore_2fa_admin_log', true); + $log = is_array($log) ? $log : []; + $log[] = ['type' => $type, 'timestamp' => $now, 'staff' => $staff->user_login, 'staff_id' => $staff->ID]; + update_user_meta($user->ID, 'acore_2fa_admin_log', $log); + return ['success' => true, 'date' => wp_date('jS \o\f F, Y \a\t H:i', $now), 'staff' => $staff->user_login]; + } + )); + + register_rest_route( ACORE_SLUG . '/v1', 'remove-ingame-2fa', array( + 'methods' => 'POST', + 'permission_callback' => function() { return is_user_logged_in(); }, + 'callback' => function( \WP_REST_Request $request ) { + $user = wp_get_current_user(); + $totpSecret = get_user_meta($user->ID, 'wp_2fa_totp_key', true); + $enabledMethods = get_user_meta($user->ID, 'wp_2fa_enabled_methods', true); + $totpActive = !empty($totpSecret) && ( + (is_array($enabledMethods) && in_array('totp', $enabledMethods)) || + $enabledMethods === 'totp' + ); + if (!$totpActive) + return new \WP_Error('no_website_2fa', __('You must have website 2FA (TOTP) enabled to remove in-game 2FA from here.'), ['status' => 403]); + + $data = $request->get_json_params(); + $token = isset($data['token']) ? trim((string) $data['token']) : ''; + if (!preg_match('/^\d{6}$/', $token)) + return new \WP_Error('invalid_token', __('Please enter a valid 6-digit code.'), ['status' => 400]); + if (!\ACore\Components\ServerInfo\acore_totp_validate($totpSecret, $token)) + return new \WP_Error('wrong_token', __('Incorrect code. Please try again.'), ['status' => 401]); + + try { + $accId = ACoreServices::I()->getAcoreAccountId(); + if (!$accId) return new \WP_Error('no_account', __('Could not find your game account.'), ['status' => 404]); + $conn = ACoreServices::I()->getAccountEm()->getConnection(); + $conn->executeStatement('UPDATE account SET totp_secret = NULL WHERE id = ?', [$accId]); + return ['success' => true]; + } catch (\Exception $e) { + return new \WP_Error('db_error', __('Database error. Please try again.'), ['status' => 500]); + } } ) ); }); diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php index 0cddc55fd..c7685ef8a 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php @@ -17,7 +17,7 @@
    -

    page_alias); ?>

    +

    page_alias); ?>

    diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php index 88fdc13cd..36da9f935 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php @@ -93,178 +93,272 @@ class="regular-text" autocomplete="new-password" />
    - + ID, 'acore_2fa_admin_log', true); + $adminLog = is_array($adminLog) ? $adminLog : []; + $lastWebRemoval = null; + $lastGameRemoval = null; + foreach ($adminLog as $entry) { + if ($entry['type'] === 'website') $lastWebRemoval = $entry; + if ($entry['type'] === 'ingame') $lastGameRemoval = $entry; + } + // Only warn if 2FA is not currently active (user hasn't re-enabled yet) + $showWebWarning = $lastWebRemoval && !$websiteTotpEnabled; + $showGameWarning = $lastGameRemoval && !$ingame2faActive; + ?> +

    - - - - - - - - - - - -

    - - - + + +

    +

    + + +

    + +

    + - ' . esc_html(wp_date('jS \o\f F, Y \a\t H:i', $lastWebRemoval['timestamp'])) . '', + '' . esc_html($lastWebRemoval['staff']) . '' + ); ?> +

    + + +

    + - ' . esc_html(wp_date('jS \o\f F, Y \a\t H:i', $lastGameRemoval['timestamp'])) . '', + '' . esc_html($lastGameRemoval['staff']) . '' + ); ?> +

    + +
    + + +

    + Time-based (TOTP) key.', 'acore-wp-plugin'); ?> + two separate setups: one exclusively for logging into the website, and another for logging into the game server. Each has its own independent code.', 'acore-wp-plugin'); ?>

    -
    -
    - +
    + + +

    + + + callbacks as $callbacks) { + foreach ($callbacks as $cb) { + $func = $cb['function']; + $id = ''; + if (is_array($func) && is_object($func[0])) $id = get_class($func[0]); + elseif (is_array($func) && is_string($func[0])) $id = $func[0]; + elseif (is_string($func)) $id = $func; + if ($id && ( + stripos($id, 'WP2FA') !== false || + stripos($id, 'wp_2fa') !== false || + stripos($id, 'Two_Factor') !== false + )) { + call_user_func($func, $user); + } + } + } + } + $wp2faHtml = ob_get_clean(); + // Strip the WP2FA section heading + subtitle (redundant with our own "Website" label) + $wp2faHtml = preg_replace( + '/]*>\s*Two-factor authentication settings\s*<\/h[1-4]>\s*(]*>[^<]*<\/p>)?/i', + '', + $wp2faHtml + ); + echo $wp2faHtml; + ?> + +

    + + +
    + + +

    + + + + + + + + + + +

    + + + +
    + + + + +

    + +

    +
      +
    1. + + + .account 2fa setup 1 +
    2. +
    3. + +
    4. +
    5. + + .account 2fa setup <6-digit-code> +
    6. +
    + + +

    + +

    + +
    +
    + + +
    +
    +
    + + + +
    +

    + +

    + + + + + +
    -
    +

    - 50): ?> - -
    -
    +
    + -

    +

    - +
    - - + + - $row): ?> - = 50 ? 'class="acore-conn-hidden" style="display:none;"' : '' ?>> - - - + + + + +
    ip_address) ?>country) ?>login_at)) ?>
    +
    -
    -
    +
    - + + 10): ?> +

    + + + + $new === '1']); } -function acore_dark_mode_js($nonce) { +function acore_dark_mode_js(string $nonce): string { return << .ab-item').on('click', function(e) { +(function($){ + var nonce = '{$nonce}'; + $(document).on('click', '#wp-admin-bar-acore-dark-mode > a', function(e){ e.preventDefault(); - $.post(ajaxurl, { action: 'acore_toggle_dark_mode', nonce: '{$nonce}' }, function(res) { + $.post(ajaxurl, { action: 'acore_toggle_dark_mode', nonce: nonce }, function(res){ if (!res.success) return; var dark = res.data.dark; $('body').toggleClass('acore-dark-mode', dark); $('#acore-dm-icon').html(dark ? '☀' : '☾'); - $(this).attr('title', dark ? 'Switch to light mode' : 'Switch to dark mode'); + $('#wp-admin-bar-acore-dark-mode > a').attr('title', dark ? 'Switch to light mode' : 'Switch to dark mode'); }); }); -}); +})(jQuery); JS; } diff --git a/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php b/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php index 2f8b26822..79b89e0a6 100644 --- a/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php +++ b/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php @@ -18,7 +18,7 @@ public function addVanity($charName, $category, $vanityID) { * other service types will be rejected by the mod's HandleSmartStoneUnlockAccountCommand. * * $accountName is expected to come from a WordPress user_login (sanitize_user - * restricts these to alphanumerics, dot, hyphen, underscore, at — no spaces), + * restricts these to alphanumerics, dot, hyphen, underscore, at - no spaces), * but we still cast the numeric args at the boundary as belt-and-braces. */ public function addAccountVanity($accountName, $category, $vanityID) { diff --git a/src/acore-wp-plugin/web/assets/css/dark-mode.css b/src/acore-wp-plugin/web/assets/css/dark-mode.css index e17ce4d35..0bc89fda1 100644 --- a/src/acore-wp-plugin/web/assets/css/dark-mode.css +++ b/src/acore-wp-plugin/web/assets/css/dark-mode.css @@ -292,6 +292,11 @@ body.acore-dark-mode .acore-char-row { border-left-color: var(--cls-dark, #8b949e) !important; } +body.acore-dark-mode .acore-char-pos { + color: #e6edf3 !important; + background: rgba(255, 255, 255, 0.12) !important; +} + body.acore-dark-mode .acore-char-name { color: #c9d1d9 !important; } @@ -645,43 +650,49 @@ body.acore-dark-mode #mail-return-items .mail-entry { } body.acore-dark-mode #mail-return-items .mail-header { - background: transparent !important; - border-top-color: transparent !important; - border-right-color: transparent !important; - border-bottom-color: transparent !important; - /* border-left-color stays as JS inline class color */ + background: #0d1117; + border-bottom-color: #30363d; } -/* recipient div inside header must also be transparent */ -body.acore-dark-mode #mail-return-items .mail-recipient { - background: transparent !important; +body.acore-dark-mode #mail-return-items .mail-recipient, +body.acore-dark-mode #mail-return-items .mail-subject, +body.acore-dark-mode #mail-return-items .mail-meta { + color: #c9d1d9; } body.acore-dark-mode #mail-return-items .mail-to-label, -body.acore-dark-mode #mail-return-items .recipient-level { - color: #8b949e !important; +body.acore-dark-mode #mail-return-items .mail-subject-label { + color: #8b949e; } -body.acore-dark-mode #mail-return-items .recipient-name { - color: #e6edf3 !important; +body.acore-dark-mode #mail-return-items .mail-items-grid { + background: #0d1117; + border-top-color: #30363d; +} + +body.acore-dark-mode #mail-return-items .mail-item-slot { + background: #161b22; +} + +body.acore-dark-mode #mail-return-items .mail-item-slot a { + color: #c9d1d9; } -body.acore-dark-mode #mail-return-items .mail-subject { +body.acore-dark-mode #mail-return-items .mail-cod-label { color: #8b949e; } -body.acore-dark-mode .mail-item-slot { - border-color: rgba(255, 255, 255, 0.12); - background: #0d1117; +body.acore-dark-mode .mail-return-button { + background: #21262d !important; + border-color: #30363d !important; + color: #c9d1d9 !important; } -/* Dark overlay strip so white quantity text is readable */ -body.acore-dark-mode .mail-item-qty { - background: rgba(0, 0, 0, 0.65); - color: #fff; +body.acore-dark-mode .mail-return-button:hover { + background: #30363d !important; + color: #e6edf3 !important; } -body.acore-dark-mode #mail-return-items .mail-return-button { - background: #1f6feb; - border-color: #1f6feb; +body.acore-dark-mode #mail-return-empty { + color: #8b949e; } diff --git a/src/acore-wp-plugin/web/assets/css/main.css b/src/acore-wp-plugin/web/assets/css/main.css index 6b66c8dde..766de09b8 100644 --- a/src/acore-wp-plugin/web/assets/css/main.css +++ b/src/acore-wp-plugin/web/assets/css/main.css @@ -50,6 +50,21 @@ body { border-radius: 2px; } +.acore-char-pos { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + font-size: 11px; + font-weight: 700; + color: #3c434a; + background: rgba(0, 0, 0, 0.10); + border-radius: 4px; + margin-right: 8px; + flex-shrink: 0; +} + .acore-char-name { flex: 1; font-weight: 500; From 8f3fa2aad8344774f082e6ce939a2dcf874fa73d Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 17:19:44 +0100 Subject: [PATCH 19/47] Fixed --- .../CharactersMenu/CharactersView.php | 18 ++++-- .../MailReturnMenu/MailReturnView.php | 6 ++ .../ResurrectionScrollMenu.php | 4 -- .../Components/UnstuckMenu/UnstuckView.php | 6 -- .../UserPanel/Pages/ItemRestorationPage.php | 33 +++++++---- .../src/Utils/AcoreCharColors.php | 57 +++++++------------ 6 files changed, 61 insertions(+), 63 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php index 075eaaffe..9155f6fd6 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php @@ -55,7 +55,6 @@ public function getHomeRender($characters) { - @@ -64,12 +63,21 @@ public function getHomeRender($characters) { - - rest_url(ACORE_SLUG . '/v1/mail-return/list'), + 'returnUrl' => rest_url(ACORE_SLUG . '/v1/mail-return'), + 'assetsUrl' => ACORE_URL_PLG . 'web/assets/', + 'nonce' => wp_create_nonce('wp_rest'), + ]); ?> diff --git a/src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php b/src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php index 7c62dbc01..45816b1e0 100644 --- a/src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php +++ b/src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php @@ -43,9 +43,6 @@ function acore_resurrection_scroll_menu() function acore_resurrection_scroll_menu_page() { - if (!$this->isAvailable()) { - wp_die(__('This feature is not available.')); - } $controller = new ResurrectionScrollController(); $controller->render(); } @@ -54,6 +51,5 @@ function acore_resurrection_scroll_menu_page() function resurrection_scroll_menu_init() { $menu = ResurrectionScrollMenu::I(); - add_action('admin_menu', array($menu, 'acore_resurrection_scroll_menu')); } diff --git a/src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php b/src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php index 41bced8e9..1bde0929a 100644 --- a/src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php +++ b/src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php @@ -70,12 +70,6 @@ class="unstuck-button" - No items to recover for the selected character.'; - } else { - var existing = document.getElementById('no-recoverable-items'); - if (existing) existing.parentElement.removeChild(existing); + var rows = document.querySelectorAll('#item-restoration-table tbody tr:not(.hidden)'); + if (rows.length === 0) { + var emptyRow = document.getElementById('item-restoration-empty'); + if (emptyRow) emptyRow.classList.remove('hidden'); } } function resetState() { - successBox.innerHTML = ''; errorBox.innerHTML = ''; + successBox.innerHTML = ''; successBox.classList.add('invisible'); noResults.classList.add('hidden'); } -}()); + + // Load items on character select + document.querySelectorAll('.acore-char-card').forEach(function(card) { + card.addEventListener('click', function() { + document.querySelectorAll('.acore-char-card').forEach(function(c) { c.classList.remove('active'); }); + this.classList.add('active'); + selectCharacter(this.dataset.charGuid, this.dataset.charName); + }); + }); +})(); + diff --git a/src/acore-wp-plugin/src/Utils/AcoreCharColors.php b/src/acore-wp-plugin/src/Utils/AcoreCharColors.php index 5d6e24357..13620f841 100644 --- a/src/acore-wp-plugin/src/Utils/AcoreCharColors.php +++ b/src/acore-wp-plugin/src/Utils/AcoreCharColors.php @@ -65,60 +65,45 @@ class AcoreCharColors { 11 => 'alliance', // Draenei ]; - const FACTION_COLORS = [ - 'alliance' => '#3FACF4', - 'horde' => '#FF653D', - 'neutral' => '#8b949e', - ]; - - public static function getLight(int $classId): string { - return self::CLASS_COLORS[$classId]['light'] ?? self::FALLBACK_LIGHT; - } - - public static function getDark(int $classId): string { - return self::CLASS_COLORS[$classId]['dark'] ?? self::FALLBACK_DARK; + public static function getClassName(int $classId): string { + return self::CLASS_NAMES[$classId] ?? 'Unknown'; } public static function getRaceName(int $raceId): string { return self::RACE_NAMES[$raceId] ?? 'Unknown'; } - public static function getClassName(int $classId): string { - return self::CLASS_NAMES[$classId] ?? 'Unknown'; - } - - public static function factionColor(int $raceId): string { - $faction = self::RACE_FACTION[$raceId] ?? 'neutral'; - return self::FACTION_COLORS[$faction]; + public static function getFaction(int $raceId): string { + return self::RACE_FACTION[$raceId] ?? 'unknown'; } /** - * Returns the inline style string to set CSS variables on a char row. - * Includes class color (light+dark) and faction border color. + * Returns inline style string for a character row. + * Uses CSS custom properties so dark-mode.css can override via color-mix. */ - public static function rowStyle(int $classId, int $raceId = 0): string { - $light = self::getLight($classId); - $dark = self::getDark($classId); - $faction = self::factionColor($raceId); - return "--cls-light:{$light};--cls-dark:{$dark};--faction-color:{$faction};"; + public static function rowStyle(int $classId, int $raceId): string { + $cls = self::CLASS_COLORS[$classId] ?? ['light' => self::FALLBACK_LIGHT, 'dark' => self::FALLBACK_DARK]; + $faction = self::RACE_FACTION[$raceId] ?? 'unknown'; + $fLight = $faction === 'alliance' ? '#3FACF4' : '#FF653D'; + $fDark = $faction === 'alliance' ? '#3FACF4' : '#FF653D'; + return sprintf( + '--cls-light:%s; --cls-dark:%s; --faction-color:%s; border-top:2px solid %s; border-right:2px solid %s; border-bottom:2px solid %s; border-left:4px solid %s;', + $cls['light'], $cls['dark'], $fLight, + $fLight, $fLight, $fLight, $cls['light'] + ); } - /** - * Returns the expansion slug for a given level. - * Used to color level badges: Vanilla (1-60), TBC (61-70), Wrath (71-80). - */ + /** Returns the expansion slug for a level, used as data-exp attribute. */ public static function expansionSlug(int $level): string { if ($level <= 60) return 'vanilla'; if ($level <= 70) return 'tbc'; return 'wrath'; } - /** - * Returns a human-readable expansion bracket label for use as a tooltip. - */ + /** Returns the expansion label for a level, used as title attribute. */ public static function expansionLabel(int $level): string { - if ($level <= 60) return 'Vanilla Bracket (1–60)'; - if ($level <= 70) return 'The Burning Crusade Bracket (61–70)'; - return 'Wrath of the Lich King Bracket (71–80)'; + if ($level <= 60) return 'Classic'; + if ($level <= 70) return 'The Burning Crusade'; + return 'Wrath of the Lich King'; } } From 7c0b9ac5eec7852be1d20df3c2a9664abaf40385 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 17:21:33 +0100 Subject: [PATCH 20/47] Mail fixed --- .../MailReturnMenu/MailReturnView.php | 2 ++ .../web/assets/mail-return/mail-return.js | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php index 9339e0e51..cd92267b8 100644 --- a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php +++ b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php @@ -22,6 +22,7 @@ public function getMailReturnRender($chars) wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.5'); wp_enqueue_script('bootstrap-js', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array(), '5.1.3'); wp_enqueue_script('jquery'); + wp_enqueue_script('power-js', 'https://wow.zamimg.com/widgets/power.js', array(), null, false); wp_enqueue_script('acore-mail-return-js', ACORE_URL_PLG . 'web/assets/mail-return/mail-return.js', array('jquery'), '2.3', true); wp_localize_script('acore-mail-return-js', 'mailReturnData', [ 'mailsUrl' => rest_url(ACORE_SLUG . '/v1/mail-return/list'), @@ -32,6 +33,7 @@ public function getMailReturnRender($chars) ?> +
    diff --git a/src/acore-wp-plugin/web/assets/mail-return/mail-return.js b/src/acore-wp-plugin/web/assets/mail-return/mail-return.js index bcb4d08b2..bad756e5a 100644 --- a/src/acore-wp-plugin/web/assets/mail-return/mail-return.js +++ b/src/acore-wp-plugin/web/assets/mail-return/mail-return.js @@ -146,13 +146,8 @@ jQuery(document).ready(function () { mailItems.append(entry); }); - // Refresh Wowhead tooltips, then move icons to slot background for proper fill - if (typeof $WowheadPower !== "undefined" && $WowheadPower.refreshLinks) { - $WowheadPower.refreshLinks(); - } - // WowHead sets background-image on the itself (class "icontinyl") - // Swap tiny GIF → large JPG for sharper icons - setTimeout(function () { + // Refresh Wowhead tooltips + upgrade icons to large JPG + function upgradeWowheadIcons() { document.querySelectorAll('.mail-item-slot > a').forEach(function (a) { var bg = a.style.backgroundImage; if (bg) { @@ -161,7 +156,24 @@ jQuery(document).ready(function () { a.style.backgroundImage = bg; } }); - }, 500); + } + if (typeof $WowheadPower !== "undefined" && $WowheadPower.refreshLinks) { + $WowheadPower.refreshLinks(); + setTimeout(upgradeWowheadIcons, 1500); + } else { + // power.js loads async — wait for it + var attempts = 0; + var wait = setInterval(function () { + attempts++; + if (typeof $WowheadPower !== "undefined" && $WowheadPower.refreshLinks) { + $WowheadPower.refreshLinks(); + clearInterval(wait); + setTimeout(upgradeWowheadIcons, 1500); + } else if (attempts > 20) { + clearInterval(wait); + } + }, 250); + } }, error: function (xhr, status, error) { loading.hide(); From 325b367c4bdc47ba6356dafba7ac3e487f8d89d8 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 17:22:33 +0100 Subject: [PATCH 21/47] Update ItemRestorationPage.php --- .../src/Components/UserPanel/Pages/ItemRestorationPage.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php index 229b1fcf7..cd4b83d37 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php @@ -202,4 +202,3 @@ function resetState() { }); })(); - From fc51b06196e6f52ea3fb0b028d98ace8850e8cb7 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 17:48:14 +0100 Subject: [PATCH 22/47] Base --- .../src/Components/UserPanel/UserView.php | 2 +- src/acore-wp-plugin/src/Manager/ACoreServices.php | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserView.php b/src/acore-wp-plugin/src/Components/UserPanel/UserView.php index 31c58b666..8fb260b8b 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserView.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserView.php @@ -29,7 +29,7 @@ function getItemRestorationRender($characters) { wp_enqueue_style('bootstrap-css', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', array(), '5.1.3'); wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.5'); wp_enqueue_script('bootstrap-js', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array(), '5.1.3'); - wp_enqueue_script('power-js', 'https://wow.zamimg.com/widgets/power.js', array()); + wp_enqueue_script('power-js', 'https://wow.zamimg.com/widgets/power.js', array(), null, false); include(__DIR__ . '/Pages/ItemRestorationPage.php'); return ob_get_clean(); } diff --git a/src/acore-wp-plugin/src/Manager/ACoreServices.php b/src/acore-wp-plugin/src/Manager/ACoreServices.php index fbdd2ebcd..2c6501af7 100644 --- a/src/acore-wp-plugin/src/Manager/ACoreServices.php +++ b/src/acore-wp-plugin/src/Manager/ACoreServices.php @@ -402,16 +402,15 @@ public function getUserNameByUserId($usedId) { } public function getRestorableItemsByCharacter($character) { - $query = "SELECT `Id`, `ItemEntry` - FROM `recovery_item` - WHERE `Guid` = ? - "; $conn = $this->getCharacterEm()->getConnection(); - $stmt = $conn->prepare($query); + $stmt = $conn->prepare( + "SELECT `Id`, `ItemEntry`, `Count`, `DeleteDate` + FROM `recovery_item` + WHERE `Guid` = ? + ORDER BY `DeleteDate` DESC" + ); $stmt->bindValue(1, $character); - $stmt->executeQuery(); - $res = $stmt->executeQuery(); - return $res->fetchAllAssociative(); + return $stmt->executeQuery()->fetchAllAssociative(); } /** From 3d0d4f0f58e7bb2425940346d78238aa0168f1b9 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 21:58:08 +0100 Subject: [PATCH 23/47] Base --- .../UserPanel/Pages/ItemRestorationPage.php | 272 +++++++++++------- src/acore-wp-plugin/web/assets/css/main.css | 122 +++++++- 2 files changed, 286 insertions(+), 108 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php index cd4b83d37..4326c4b6f 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php @@ -6,17 +6,19 @@

    page_alias); ?>

    -
    -
    +
    + + +
    -

    Item Restoration Service

    -

    Restored items will be sent to the selected character's mailbox.

    +

    page_alias); ?>

    +

    page_alias); ?>


    page_alias); ?> -
      +
        @@ -40,50 +42,55 @@

        page_alias); ?>

        -
        - - - -
    -
    +
    + + + + +
    diff --git a/src/acore-wp-plugin/web/assets/css/main.css b/src/acore-wp-plugin/web/assets/css/main.css index 766de09b8..c970b7ef7 100644 --- a/src/acore-wp-plugin/web/assets/css/main.css +++ b/src/acore-wp-plugin/web/assets/css/main.css @@ -606,23 +606,36 @@ body.profile-php .form-table td { /* --- Item Restoration sub-page --- */ #acore-item-restoration-page { - max-width: 860px; + max-width: 100%; } #acore-item-restoration-page > h1 { margin-bottom: 16px; } -.acore-item-restoration-row { - margin-top: 0; +/* Layout: sidebar + content (mirrors mail-return) */ +#item-restore-layout { + display: grid; + grid-template-columns: 460px 1fr; + gap: 16px; + align-items: start; +} + +#item-restore-sidebar { + grid-column: 1; +} + +#item-restore-content { + grid-column: 2; } -/* Table always dark so WowHead white-quality item links (#ffffff) are readable */ -#acore-item-restoration-page .table tbody td, -#acore-item-restoration-page .table tbody th { - background: #1d2327 !important; - color: #c9d1d9; - border-color: #30363d; +@media (max-width: 800px) { + #item-restore-layout { + grid-template-columns: 1fr; + } + #item-restore-content { + grid-column: 1; + } } /* click cursor + active state (mirrors mail return) */ @@ -636,3 +649,94 @@ body.profile-php .form-table td { border-right-width: 3px; border-bottom-width: 3px; } + +/* Item restore: 4-column card grid */ +.item-restore-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +/* Card: white bg, quality-coloured border (set by JS after wowhead loads) */ +.item-restore-card { + position: relative; + background: #fff; + border: 3px solid #9d9d9d; + border-radius: 4px; + padding: 20px 12px 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +/* Number badge — top-left corner */ +.item-restore-num { + position: absolute; + top: 6px; + left: 8px; + font-size: 11px; + font-weight: 600; + color: #646970; + line-height: 1; +} + +/* Icon container */ +.item-restore-icon { + position: relative; + width: 80px; + height: 80px; + background: #1c1c1c; + border-radius: 3px; + overflow: hidden; + flex-shrink: 0; +} + +/* WowHead puts icon as background-image on the
    . + font-size/color hide the item-name text wowhead injects. */ +.item-restore-icon > a { + position: absolute !important; + inset: 0 !important; + display: block !important; + width: 100% !important; + height: 100% !important; + font-size: 0 !important; + line-height: 0 !important; + color: transparent !important; + text-indent: -9999px !important; + overflow: hidden !important; + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + padding: 0 !important; +} + +/* Deleted date */ +.item-restore-date { + font-size: 11px; + color: #646970; + text-align: center; + line-height: 1.3; +} + +.item-restore-btn { + background: #2271b1; + color: #fff; + border: 1px solid #2271b1; + border-radius: 3px; + padding: 5px 0; + cursor: pointer; + font-size: 13px; + width: 100%; + text-align: center; +} + +.item-restore-btn:hover { + background: #135e96; + border-color: #135e96; +} + +.item-restore-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} From 95fd5aef80132ac7328ad8f1b8d0cee364e21a73 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 22:11:03 +0100 Subject: [PATCH 24/47] Update User.php --- src/acore-wp-plugin/src/Hooks/User/User.php | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/acore-wp-plugin/src/Hooks/User/User.php b/src/acore-wp-plugin/src/Hooks/User/User.php index 679eefd04..e3c6ad2ff 100644 --- a/src/acore-wp-plugin/src/Hooks/User/User.php +++ b/src/acore-wp-plugin/src/Hooks/User/User.php @@ -680,6 +680,34 @@ function acore_profile_2fa_removal_warning($user) { callbacks as $priority => $callbacks) { + foreach ($callbacks as $key => $cb) { + $func = $cb['function']; + $id = ''; + if (is_array($func) && is_object($func[0])) $id = get_class($func[0]); + elseif (is_array($func) && is_string($func[0])) $id = $func[0]; + elseif (is_string($func)) $id = $func; + if ($id && ( + stripos($id, 'WP2FA') !== false || + stripos($id, 'wp_2fa') !== false || + stripos($id, 'Two_Factor') !== false + )) { + unset($wp_filter[$hook]->callbacks[$priority][$key]); + } + } + } + } +}, 99); + add_action('show_user_profile', __NAMESPACE__ . '\acore_profile_recent_connections'); function acore_profile_recent_connections($user) { From b9d3fd4e00d5d67608cdecb3b8c325d83ec4cc11 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 22:27:29 +0100 Subject: [PATCH 25/47] Update Tools.php --- .../src/Components/AdminPanel/Pages/Tools.php | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index 3ef79b94d..d10b58d9b 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -160,7 +160,7 @@ class=""
    - +

    Remove 2FA

    Remove Website or In-game 2FA for any account. A warning is shown to the user until they re-enable it. @@ -188,6 +188,40 @@ class=""

    + +
    +
    +
    +
    Name Unlock Settings
    +
    + + Allowed banned names table (characters database): + +

    + + Inactivity Thresholds per Level: + + + + + + + + + +
    +
    + Add +
    +
    + Reset +
    +
    +
    +
    +
    +

    @@ -279,5 +313,59 @@ function wire2fa(type, $userInput, $check, $remove, $msg) { wire2fa('website', $('#acore-2fa-web-user'), $('#acore-2fa-web-check'), $('#acore-2fa-web-remove'), $('#acore-2fa-web-msg')); wire2fa('ingame', $('#acore-2fa-game-user'), $('#acore-2fa-game-check'), $('#acore-2fa-game-remove'), $('#acore-2fa-game-msg')); + /* ── Name Unlock Thresholds ───────────────────────────────────────── */ + const deleteThreshold = (ev) => { + const $btn = $(ev.target).closest('.acore-btn-danger'); + const $tr = $btn.closest('tr'); + $tr.remove(); + let i = 0; + $('#acore-name-unlock-thresholds tbody tr').each(function () { + const previ = $(this).data('i'); + $(this).data('i', i); + $(this).find(`input[name="acore_name_unlock_thresholds[${previ}][0]"]`).attr('name', `acore_name_unlock_thresholds[${i}][0]`); + $(this).find(`input[name="acore_name_unlock_thresholds[${previ}][1]"]`).attr('name', `acore_name_unlock_thresholds[${i}][1]`); + i++; + }); + }; + + const addThreshold = (i = undefined, level = '', days = '') => { + if (i === undefined) { + const $trs = $('#acore-name-unlock-thresholds tbody tr'); + i = $trs.length ? $($trs[$trs.length - 1]).data('i') + 1 : 0; + } + const $tr = $('').appendTo('#acore-name-unlock-thresholds tbody'); + $tr.data('i', i); + let $td = $('').appendTo($tr); + $(``).appendTo($td); + $td = $('').appendTo($tr); + $(``).appendTo($td); + $td = $('').appendTo($tr); + const $btnDel = $(`

    `).appendTo($td); + $btnDel.append(``); + $btnDel.on('click', deleteThreshold); + }; + + $('#acore-name-unlock-thresholds-add').on('click', () => addThreshold()); + + /* ── Reset Name Unlock to Defaults ──────────────────────────────── */ + $('#acore-name-unlock-reset').on('click', function () { + if (!confirm( + 'Reset Name Unlock settings to defaults?\n\n' + + 'This will clear the banned names table and delete all inactivity thresholds.\n\n' + + 'This cannot be undone. Continue?' + )) return; + + $('input[name="acore_name_unlock_allowed_banned_names_table"]').val(''); + $('#acore-name-unlock-thresholds tbody tr').remove(); + + $('input[name="Submit"]').closest('form').submit(); + }); + + acore_name_unlock_thresholds as $i => $threshold) { + if ($threshold[0] != '' && $threshold[1] != '') { + echo "addThreshold($i, $threshold[0], $threshold[1]);"; + } + } ?> + })(jQuery); From 16b6231edf4b52b00ff98ef3c66ea5892238c979 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sun, 7 Jun 2026 22:30:13 +0100 Subject: [PATCH 26/47] Update CharactersView.php --- .../Components/CharactersMenu/CharactersView.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php index 9155f6fd6..9fb8e341d 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php @@ -54,7 +54,10 @@ public function getHomeRender($characters) { - +
    + + +
    @@ -63,6 +66,15 @@ public function getHomeRender($characters) {
    + + diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php index 3f5d6cdb9..7cc3fd577 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php @@ -154,6 +154,11 @@ public function showItemRestorationPage() { public function showSecurityPage() { $user = wp_get_current_user(); + // Detect & log a user-initiated Website 2FA removal (records the user's IP). + if (function_exists('ACore\\Components\\ServerInfo\\acore_2fa_sync_self_removals')) { + \ACore\Components\ServerInfo\acore_2fa_sync_self_removals($user->ID); + } + $passwordMessage = \ACore\Hooks\User\acore_pw_get_message($user->ID); $passwordChangedAt = get_user_meta($user->ID, 'acore_password_changed_at', true); $twoFaData = $this->getTwoFaData($user); From 5daf2880f1f12c4218793932656b1a0e4f7b9035 Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:16:01 +0100 Subject: [PATCH 30/47] Dark theme --- .../src/Hooks/Various/DarkMode.php | 2 + src/acore-wp-plugin/web/assets/css/theme.css | 159 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/acore-wp-plugin/web/assets/css/theme.css diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php index 2de9657a5..673e2c163 100644 --- a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -34,6 +34,8 @@ function acore_dark_mode_body_class($classes) { function acore_dark_mode_enqueue() { wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', [], '3.0'); wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', ['acore-css'], '3.7'); + // Central light/dark theme layer (edit colours here). Loaded last so it wins. + wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.0'); $nonce = wp_create_nonce('acore_dark_mode'); wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); diff --git a/src/acore-wp-plugin/web/assets/css/theme.css b/src/acore-wp-plugin/web/assets/css/theme.css new file mode 100644 index 000000000..28f6bd468 --- /dev/null +++ b/src/acore-wp-plugin/web/assets/css/theme.css @@ -0,0 +1,159 @@ +/* ===================================================================== + * acore-theme.css + * Single place to edit light/dark theming for the AzerothCore plugin. + * + * Colours are driven by CSS custom properties. Edit the values in the + * two blocks below (":root" = light, "body.acore-dark-mode" = dark) and + * every rule further down updates automatically. + * + * Loaded after main.css and dark-mode.css, so rules here win. + * ===================================================================== */ + +:root { + --acore-bg: #ffffff; + --acore-surface: #ffffff; + --acore-surface-2: #f3f4f5; + --acore-text: #1d2327; + --acore-muted: #646970; + --acore-border: #c3c4c7; + --acore-border-strong: #8c8f94; + --acore-accent: #2271b1; + --acore-danger: #d63638; + --acore-danger-bg: #fbeaea; + --acore-danger-text: #b32d2e; + + /* WoW item-quality colours (shared by both themes) */ + --q0: #9d9d9d; --q1: #ffffff; --q2: #1eff00; --q3: #0070dd; + --q4: #a335ee; --q5: #ff8000; --q6: #e6cc80; --q7: #e6cc80; +} + +body.acore-dark-mode { + --acore-bg: #0d1117; + --acore-surface: #161b22; + --acore-surface-2: #21262d; + --acore-text: #c9d1d9; + --acore-muted: #9aa4af; + --acore-border: #30363d; + --acore-border-strong: #484f58; + --acore-accent: #58a6ff; + --acore-danger: #f85149; + --acore-danger-bg: #2a1416; + --acore-danger-text: #ff7b72; + + --q1: #c0c0c0; /* pure white reads poorly on dark; soften common quality */ +} + +/* ── Danger buttons (Reset Order, Remove, Reset, …) ─────────────────── */ +.acore-btn-danger { + border: 1px solid var(--acore-danger) !important; + color: var(--acore-danger-text) !important; + background: var(--acore-danger-bg) !important; + transition: background .12s ease, color .12s ease; +} +.acore-btn-danger:hover, +.acore-btn-danger:focus { + background: var(--acore-danger) !important; + color: #ffffff !important; +} +.acore-btn-danger .dashicons { margin-top: 3px; } + +/* ── Item Restoration: cards ────────────────────────────────────────── */ +/* Outer card: neutral border + theme surface (works light & dark). */ +.item-restore-card { + background: var(--acore-surface) !important; + border: 1px solid var(--acore-border) !important; + color: var(--acore-text) !important; + border-radius: 6px; +} +.item-restore-card .item-restore-date { color: var(--acore-muted) !important; } +.item-restore-card .item-restore-num { color: var(--acore-muted) !important; } +/* Inner icon shows the item quality as its border. wowhead adds q0..q7 */ +/* to the icon link; we colour the icon frame from that. */ +.item-restore-icon a { + display: block; + border: 2px solid var(--acore-border-strong); + border-radius: 4px; + box-sizing: border-box; +} +.item-restore-icon a.q0 { border-color: var(--q0) !important; } +.item-restore-icon a.q1 { border-color: var(--q1) !important; } +.item-restore-icon a.q2 { border-color: var(--q2) !important; } +.item-restore-icon a.q3 { border-color: var(--q3) !important; } +.item-restore-icon a.q4 { border-color: var(--q4) !important; } +.item-restore-icon a.q5 { border-color: var(--q5) !important; } +.item-restore-icon a.q6 { border-color: var(--q6) !important; } +.item-restore-icon a.q7 { border-color: var(--q7) !important; } + +/* ── Security page: readability in dark mode ────────────────────────── */ +/* Inline muted greys (#646970) are too dark on a dark surface. */ +body.acore-dark-mode #acore-security-page [style*="646970"], +body.acore-dark-mode #acore-security-page [style*="#646970"], +body.acore-dark-mode #acore-security-page .description, +body.acore-dark-mode #acore-security-page .text-muted { + color: var(--acore-muted) !important; +} +body.acore-dark-mode #acore-security-page, +body.acore-dark-mode #acore-security-page p, +body.acore-dark-mode #acore-security-page li, +body.acore-dark-mode #acore-security-page h3 { color: var(--acore-text); } +body.acore-dark-mode #acore-security-page code { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border); + border-radius: 3px; + padding: 1px 5px; +} +/* Recent Connections table */ +body.acore-dark-mode #acore-security-page .wp-list-table { + background: var(--acore-surface) !important; + border-color: var(--acore-border) !important; +} +body.acore-dark-mode #acore-security-page .wp-list-table th, +body.acore-dark-mode #acore-security-page .wp-list-table td { + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; + background: transparent !important; +} +body.acore-dark-mode #acore-security-page .wp-list-table tr:nth-child(even) td { + background: var(--acore-surface-2) !important; +} + +/* ── Mail Return surfaces ───────────────────────────────────────────── */ +body.acore-dark-mode #acore-mail-return-page .mail-entry { background: var(--acore-surface) !important; } +body.acore-dark-mode #acore-mail-return-page .mail-recipient, +body.acore-dark-mode #acore-mail-return-page .mail-items { background: var(--acore-bg) !important; } +body.acore-dark-mode #acore-mail-return-page .mail-entry *, +body.acore-dark-mode #acore-mail-return-page .mail-meta { color: var(--acore-text) !important; } +body.acore-dark-mode #acore-mail-return-page .text-muted { color: var(--acore-muted) !important; } + +/* ── WP 2FA plugin UI (embedded on the Security page) ───────────────── */ +body.acore-dark-mode #acore-security-page [class*="wp2fa"], +body.acore-dark-mode #acore-security-page [class*="wp-2fa"] { color: var(--acore-text) !important; } +body.acore-dark-mode #acore-security-page [class*="wp2fa"] input, +body.acore-dark-mode #acore-security-page [class*="wp2fa"] select, +body.acore-dark-mode #acore-security-page [class*="wp2fa"] textarea { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border) !important; +} +body.acore-dark-mode #acore-security-page [class*="wp2fa"] code, +body.acore-dark-mode #acore-security-page [class*="wp2fa"] pre { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; +} + +/* ── myCRED points / history page ───────────────────────────────────── */ +body.acore-dark-mode .mycred-history, +body.acore-dark-mode table.mycred-history, +body.acore-dark-mode [id*="mycred"], +body.acore-dark-mode [class*="mycred"] { + background: var(--acore-surface) !important; + border-color: var(--acore-border) !important; + color: var(--acore-text) !important; +} +body.acore-dark-mode [class*="mycred"] th, +body.acore-dark-mode [class*="mycred"] td { + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; +} +body.acore-dark-mode [class*="mycred"] a { color: var(--acore-accent) !important; } From 6b645389b1598871999039cfe00d6194f323c2ed Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:43:30 +0100 Subject: [PATCH 31/47] Dark theme MyCred "Points History" --- .../src/Hooks/Various/DarkMode.php | 2 +- src/acore-wp-plugin/web/assets/css/theme.css | 69 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php index 673e2c163..933329c3c 100644 --- a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -35,7 +35,7 @@ function acore_dark_mode_enqueue() { wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', [], '3.0'); wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', ['acore-css'], '3.7'); // Central light/dark theme layer (edit colours here). Loaded last so it wins. - wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.0'); + wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.2'); $nonce = wp_create_nonce('acore_dark_mode'); wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); diff --git a/src/acore-wp-plugin/web/assets/css/theme.css b/src/acore-wp-plugin/web/assets/css/theme.css index 28f6bd468..d8760532f 100644 --- a/src/acore-wp-plugin/web/assets/css/theme.css +++ b/src/acore-wp-plugin/web/assets/css/theme.css @@ -142,18 +142,73 @@ body.acore-dark-mode #acore-security-page [class*="wp2fa"] pre { color: var(--acore-text) !important; } -/* ── myCRED points / history page ───────────────────────────────────── */ -body.acore-dark-mode .mycred-history, -body.acore-dark-mode table.mycred-history, -body.acore-dark-mode [id*="mycred"], -body.acore-dark-mode [class*="mycred"] { +/* ── myCRED points / history page (profile.php?page=mycred_default-history) ── */ +body.acore-dark-mode.mycred-log-page #wpbody-content { color: var(--acore-text); } + +/* Filter tabs (All / Today / This Week …) */ +body.acore-dark-mode.mycred-log-page .subsubsub, +body.acore-dark-mode.mycred-log-page .subsubsub a { color: var(--acore-muted) !important; } +body.acore-dark-mode.mycred-log-page .subsubsub a.current, +body.acore-dark-mode.mycred-log-page .subsubsub a:hover { color: var(--acore-accent) !important; } + +/* Search box + pagination chrome */ +body.acore-dark-mode.mycred-log-page .search-box input, +body.acore-dark-mode.mycred-log-page .tablenav input, +body.acore-dark-mode.mycred-log-page .tablenav select { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border) !important; +} +body.acore-dark-mode.mycred-log-page .tablenav, +body.acore-dark-mode.mycred-log-page .tablenav * { color: var(--acore-text) !important; } +body.acore-dark-mode.mycred-log-page .tablenav .tablenav-pages a, +body.acore-dark-mode.mycred-log-page .tablenav .tablenav-pages span.tablenav-pages-navspan { + background: var(--acore-surface-2) !important; + border-color: var(--acore-border) !important; + color: var(--acore-text) !important; +} + +/* The points-history list table */ +body.acore-dark-mode.mycred-log-page .wp-list-table { background: var(--acore-surface) !important; border-color: var(--acore-border) !important; color: var(--acore-text) !important; } -body.acore-dark-mode [class*="mycred"] th, -body.acore-dark-mode [class*="mycred"] td { +body.acore-dark-mode.mycred-log-page .wp-list-table thead th, +body.acore-dark-mode.mycred-log-page .wp-list-table tfoot th, +body.acore-dark-mode.mycred-log-page .wp-list-table thead th a, +body.acore-dark-mode.mycred-log-page .wp-list-table thead th span { + background: var(--acore-surface-2) !important; color: var(--acore-text) !important; border-color: var(--acore-border) !important; } +body.acore-dark-mode.mycred-log-page .wp-list-table td, +body.acore-dark-mode.mycred-log-page .wp-list-table th { + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; + background: transparent !important; +} +body.acore-dark-mode.mycred-log-page .wp-list-table > tbody > tr:nth-child(odd) { + background: var(--acore-surface-2) !important; +} +body.acore-dark-mode.mycred-log-page .wp-list-table a { color: var(--acore-accent) !important; } + +/* Export button row */ +body.acore-dark-mode.mycred-log-page #export-log-history, +body.acore-dark-mode.mycred-log-page #export-log-history * { color: var(--acore-text) !important; } + +/* Generic myCRED widgets elsewhere (points balance boxes, etc.) */ +body.acore-dark-mode [class*="mycred"] th, +body.acore-dark-mode [class*="mycred"] td { color: var(--acore-text) !important; border-color: var(--acore-border) !important; } body.acore-dark-mode [class*="mycred"] a { color: var(--acore-accent) !important; } + +/* myCRED page outer wrapper (#myCRED-wrap is white) -> blend into dark admin bg */ +body.acore-dark-mode.mycred-log-page #myCRED-wrap { + background: transparent !important; +} +body.acore-dark-mode.mycred-log-page #myCRED-wrap, +body.acore-dark-mode.mycred-log-page #myCRED-wrap h1, +body.acore-dark-mode.mycred-log-page #myCRED-wrap h2, +body.acore-dark-mode.mycred-log-page #myCRED-wrap p { + color: var(--acore-text) !important; +} From 3fc511783684d8e8f5ca323ee71e0ccd60c980df Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:54:43 +0100 Subject: [PATCH 32/47] Make security widget dark theme --- .../src/Hooks/Various/DarkMode.php | 18 ++- src/acore-wp-plugin/web/assets/css/theme.css | 116 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php index 933329c3c..afa9416ed 100644 --- a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -35,7 +35,7 @@ function acore_dark_mode_enqueue() { wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', [], '3.0'); wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', ['acore-css'], '3.7'); // Central light/dark theme layer (edit colours here). Loaded last so it wins. - wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.2'); + wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.5'); $nonce = wp_create_nonce('acore_dark_mode'); wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); @@ -131,3 +131,19 @@ function acore_dark_mode_js(string $nonce): string { })(jQuery); JS; } + +/* + * The WP 2FA setup wizard (profile.php?page=wp-2fa-setup) renders a sealed + * full-page template that only prints its own stylesheets. It exposes + * `wp_2fa_setup_page_scripts` inside its ; we use it to inject theme.css + * (whose body.wp2fa-setup rules are dark) only when the user has dark mode on. + */ +add_action('wp_2fa_setup_page_scripts', __NAMESPACE__ . '\acore_dark_mode_wizard_css'); + +function acore_dark_mode_wizard_css() { + if (get_user_meta(get_current_user_id(), 'acore_dark_mode', true) !== '1') { + return; + } + echo '' . "\n"; +} diff --git a/src/acore-wp-plugin/web/assets/css/theme.css b/src/acore-wp-plugin/web/assets/css/theme.css index d8760532f..78848de5d 100644 --- a/src/acore-wp-plugin/web/assets/css/theme.css +++ b/src/acore-wp-plugin/web/assets/css/theme.css @@ -212,3 +212,119 @@ body.acore-dark-mode.mycred-log-page #myCRED-wrap h2, body.acore-dark-mode.mycred-log-page #myCRED-wrap p { color: var(--acore-text) !important; } + +/* ── WP 2FA standalone setup wizard (profile.php?page=wp-2fa-setup) ────── + * The wizard renders its own with no admin/dark + * class, and only prints its own stylesheets. We inject theme.css into its + * via the `wp_2fa_setup_page_scripts` hook ONLY when dark mode is on + * (see DarkMode.php), so these body.wp2fa-setup rules are dark-only by load. + * The dark palette is redeclared here because .acore-dark-mode isn't present. */ +body.wp2fa-setup { + --acore-bg: #0d1117; + --acore-surface: #161b22; + --acore-surface-2: #21262d; + --acore-text: #c9d1d9; + --acore-muted: #9aa4af; + --acore-border: #30363d; + --acore-accent: #58a6ff; + background: var(--acore-bg) !important; + color: var(--acore-text) !important; +} +body.wp2fa-setup .setup-wizard-wrapper, +body.wp2fa-setup .wp2fa-setup-content, +body.wp2fa-setup .modal__container { + background: var(--acore-surface) !important; + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; +} +body.wp2fa-setup h1, body.wp2fa-setup h2, body.wp2fa-setup h3, +body.wp2fa-setup h4, body.wp2fa-setup p, body.wp2fa-setup label, +body.wp2fa-setup li, body.wp2fa-setup span, body.wp2fa-setup strong { + color: var(--acore-text) !important; +} +body.wp2fa-setup .description, +body.wp2fa-setup .wp2fa-setup-footer a { color: var(--acore-muted) !important; } +body.wp2fa-setup input[type="text"], +body.wp2fa-setup input[type="email"], +body.wp2fa-setup input[type="number"], +body.wp2fa-setup input[type="password"], +body.wp2fa-setup select, +body.wp2fa-setup textarea, +body.wp2fa-setup .select2-selection { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border) !important; +} +body.wp2fa-setup .steps li { color: var(--acore-muted) !important; } +body.wp2fa-setup .steps li.is-active { color: var(--acore-accent) !important; font-weight: 600; } +body.wp2fa-setup .button-secondary, +body.wp2fa-setup .wp-2fa-button-secondary { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; +} +body.wp2fa-setup code, +body.wp2fa-setup pre { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border); +} +body.wp2fa-setup .modal__overlay { background: rgba(0,0,0,0.6) !important; } + +/* ── WP 2FA micromodal on normal admin pages (Security page "Configure 2FA") ── + * The modal is appended at level (outside #acore-security-page), so it is + * scoped by the body dark class instead. */ +body.acore-dark-mode .wp2fa-modal .modal__container { + background: var(--acore-surface) !important; + color: var(--acore-text) !important; +} +body.acore-dark-mode .wp2fa-modal .modal__content, +body.acore-dark-mode .wp2fa-modal .modal__content h1, +body.acore-dark-mode .wp2fa-modal .modal__content h2, +body.acore-dark-mode .wp2fa-modal .modal__content h3, +body.acore-dark-mode .wp2fa-modal .modal__content h4, +body.acore-dark-mode .wp2fa-modal .modal__content p, +body.acore-dark-mode .wp2fa-modal .modal__content label, +body.acore-dark-mode .wp2fa-modal .modal__content span, +body.acore-dark-mode .wp2fa-modal .modal__content li, +body.acore-dark-mode .wp2fa-modal .modal__content strong { + color: var(--acore-text) !important; +} +body.acore-dark-mode .wp2fa-modal .description { color: var(--acore-muted) !important; } +body.acore-dark-mode .wp2fa-modal .option-pill, +body.acore-dark-mode .wp2fa-modal .radio-cells .option-pill { + background: var(--acore-surface-2) !important; + border-color: var(--acore-border) !important; + color: var(--acore-text) !important; +} +body.acore-dark-mode .wp2fa-modal .option-pill.selected, +body.acore-dark-mode .wp2fa-modal .option-pill:hover { + border-color: var(--acore-accent) !important; +} +body.acore-dark-mode .wp2fa-modal input, +body.acore-dark-mode .wp2fa-modal select, +body.acore-dark-mode .wp2fa-modal textarea { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border) !important; +} +body.acore-dark-mode .wp2fa-modal .modal__close { color: var(--acore-text) !important; } +body.acore-dark-mode .wp2fa-modal code, +body.acore-dark-mode .wp2fa-modal pre { + background: var(--acore-surface-2) !important; + color: var(--acore-text) !important; + border: 1px solid var(--acore-border); +} + +/* WP 2FA TOTP secret-key box (shown in both the modal and the wizard) */ +body.acore-dark-mode .wp2fa-modal .app-key-wrapper, +body.wp2fa-setup .app-key-wrapper { + background: var(--acore-surface-2) !important; + border-color: var(--acore-border) !important; +} +body.acore-dark-mode .wp2fa-modal .app-key-wrapper .app-key, +body.acore-dark-mode .wp2fa-modal .app-key-wrapper input, +body.wp2fa-setup .app-key-wrapper .app-key, +body.wp2fa-setup .app-key-wrapper input { + color: var(--acore-text) !important; +} From aa3b04ffa517eea0a7106e3aa06c473bc3bfb273 Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:31:00 +0100 Subject: [PATCH 33/47] Cleanup Secuirty 2FA intsustcotrs --- .../UserPanel/Pages/SecurityPage.php | 29 ++++++++--- .../src/Hooks/Various/DarkMode.php | 4 +- src/acore-wp-plugin/web/assets/css/theme.css | 48 +++++++++++++++++++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php index 05973ad78..c872a5f8e 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php @@ -264,7 +264,7 @@ class="regular-text" autocomplete="new-password" /> -
    +
    @@ -274,16 +274,30 @@ class="regular-text" autocomplete="new-password" />

    1. - - + .account 2fa setup 1
    2. - + 2FA Key, for example:', 'acore-wp-plugin'); ?> + K6NXC763GDQTZJG3CTH4WIOGAW6MZYOO
    3. - + +
      +
    4. +
    5. + Time based (TOTP).', 'acore-wp-plugin'); ?> +
    6. +
    7. + +
    8. +
    9. + .account 2fa setup <6-digit-code> + without the < > brackets.', 'acore-wp-plugin'); ?> +
    10. +
    11. +
    @@ -307,8 +321,9 @@ class="regular-text" autocomplete="new-password" />
    -

    - +

    + +

    diff --git a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php index afa9416ed..f5b860119 100644 --- a/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php +++ b/src/acore-wp-plugin/src/Hooks/Various/DarkMode.php @@ -35,7 +35,7 @@ function acore_dark_mode_enqueue() { wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', [], '3.0'); wp_enqueue_style('acore-dark-mode', ACORE_URL_PLG . 'web/assets/css/dark-mode.css', ['acore-css'], '3.7'); // Central light/dark theme layer (edit colours here). Loaded last so it wins. - wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.5'); + wp_enqueue_style('acore-theme', ACORE_URL_PLG . 'web/assets/css/theme.css', ['acore-dark-mode'], '1.9'); $nonce = wp_create_nonce('acore_dark_mode'); wp_add_inline_script('jquery-core', acore_dark_mode_js($nonce)); @@ -145,5 +145,5 @@ function acore_dark_mode_wizard_css() { return; } echo '' . "\n"; + . esc_url(ACORE_URL_PLG . 'web/assets/css/theme.css') . '?ver=1.9" />' . "\n"; } diff --git a/src/acore-wp-plugin/web/assets/css/theme.css b/src/acore-wp-plugin/web/assets/css/theme.css index 78848de5d..68db8bdce 100644 --- a/src/acore-wp-plugin/web/assets/css/theme.css +++ b/src/acore-wp-plugin/web/assets/css/theme.css @@ -328,3 +328,51 @@ body.wp2fa-setup .app-key-wrapper .app-key, body.wp2fa-setup .app-key-wrapper input { color: var(--acore-text) !important; } + + +/* ── "Disabled" sections (e.g. In-game 2FA before Website 2FA is set up) ── + * A greyed background box signals "disabled" while text stays fully legible + * (no opacity wash-out). Works in both light and dark themes. */ +.acore-2fa-disabled { + position: relative; + pointer-events: none; + user-select: none; + background: var(--acore-surface-2); + border: 1px solid var(--acore-border); + border-radius: 6px; + padding: 10px 16px 12px; + margin-top: 8px; +} +/* Light theme: keep the intro + steps at readable contrast on the grey box */ +.acore-2fa-disabled p, +.acore-2fa-disabled li { color: #3c434a !important; } +.acore-2fa-disabled p:first-child { color: #50575e !important; } +/* Dark theme */ +body.acore-dark-mode .acore-2fa-disabled, +body.acore-dark-mode .acore-2fa-disabled p, +body.acore-dark-mode .acore-2fa-disabled li, +body.acore-dark-mode .acore-2fa-disabled span { color: var(--acore-text) !important; } +body.acore-dark-mode .acore-2fa-disabled p:first-child { color: var(--acore-muted) !important; } + +/* ── "Website 2FA required" callout (bright & visible in both themes) ── */ +.acore-2fa-required-note { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0 0; + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + border-radius: 6px; + color: #9a5b00; + background: #fff4e0; + border: 1px solid #f0b429; +} +.acore-2fa-required-note .dashicons { color: #d98300; flex: 0 0 auto; } +body.acore-dark-mode .acore-2fa-required-note { + color: #f5c451 !important; + background: rgba(240, 180, 41, 0.12) !important; + border-color: #8a6d1f !important; +} +body.acore-dark-mode .acore-2fa-required-note .dashicons { color: #f5c451 !important; } From d1aa9fe4295f66d715e9d7c4690e514802583202 Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sun, 21 Jun 2026 06:02:05 +0100 Subject: [PATCH 34/47] Improvements to IP Logging --- .../Components/ServerInfo/ServerInfoApi.php | 43 ++++++++ .../UserPanel/Pages/SecurityPage.php | 92 ++++++++++++++-- .../Components/UserPanel/UserController.php | 6 ++ .../src/Hooks/User/ConnectionLogger.php | 102 ++++++++++++++++-- src/acore-wp-plugin/src/Hooks/User/User.php | 47 +++++--- .../src/Hooks/Various/DarkMode.php | 4 +- src/acore-wp-plugin/web/assets/css/theme.css | 85 ++++++++++++++- 7 files changed, 342 insertions(+), 37 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php index f48a6f868..adc92bfa7 100644 --- a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php +++ b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php @@ -470,4 +470,47 @@ function acore_2fa_sync_self_removals(int $userId): void { ]; } ) ); + + // User: paginated connection history (for "see more" without a page reload) + register_rest_route( ACORE_SLUG . '/v1', 'connections', array( + 'methods' => 'GET', + 'permission_callback' => function() { return is_user_logged_in(); }, + 'callback' => function( \WP_REST_Request $request ) { + $user = wp_get_current_user(); + $perPage = 50; + $page = max(1, (int) $request->get_param('page')); + $mock = $request->get_param('mock'); + + if ($mock !== null && $mock !== '') { + $all = \ACore\Hooks\User\acore_mock_login_history((int) $mock); + } else { + $all = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); + } + $all = is_array($all) ? $all : []; + $total = count($all); + $offset = ($page - 1) * $perPage; + $slice = array_slice($all, $offset, $perPage); + $myIp = \ACore\Hooks\User\acore_resolve_client_ip(); + + $rows = array_map(function ($r) use ($myIp) { + $ip = $r['ip_address'] ?? ($r['ip'] ?? ''); + return [ + 'ip' => $ip, + 'country' => ($r['country'] ?? '') !== '' ? $r['country'] : 'Unknown', + 'date' => ($r['login_at'] ?? '') !== '' ? \ACore\Hooks\User\acore_format_connection_date($r['login_at']) : '', + 'where' => (($r['source'] ?? 'website') === 'ingame') ? 'In-game' : 'Website', + 'current' => ($ip !== '' && $ip === $myIp), + ]; + }, $slice); + + return [ + 'rows' => array_values($rows), + 'page' => $page, + 'total' => $total, + 'from' => $total ? $offset + 1 : 0, + 'to' => $offset + count($slice), + 'has_more' => ($offset + count($slice)) < $total, + ]; + } + ) ); }); diff --git a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php index c872a5f8e..9eaa5c209 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php @@ -333,31 +333,65 @@ class="regular-text" autocomplete="new-password" />
    -

    +

    + + -

    +

    - +

    + - + $perPage): ?> + + +

    +
    - + + - + - - - - - - + + + + > + + + +
    + +

    + +

    +
    @@ -509,4 +543,40 @@ function check(){ } setInterval(check, 20000); })(); + +/* Recent Connections: load the next 50 in place (no page reload) */ +(function(){ + var btn = document.getElementById('acore-conn-more'); + if (!btn) return; + var tbody = document.getElementById('acore-conn-tbody'); + var toEl = document.getElementById('acore-conn-to'); + var base = ''; + var nonce = ''; + btn.addEventListener('click', function(){ + var next = parseInt(btn.getAttribute('data-page'), 10) + 1; + var mock = btn.getAttribute('data-mock') || ''; + var url = base + '?page=' + next + (mock ? '&mock=' + encodeURIComponent(mock) : ''); + var label = btn.textContent; + btn.disabled = true; btn.textContent = 'Loading...'; + fetch(url, { headers: { 'X-WP-Nonce': nonce } }) + .then(function(r){ return r.json(); }) + .then(function(d){ + (d.rows || []).forEach(function(row){ + var tr = document.createElement('tr'); + if (row.current) { tr.className = 'acore-conn-current'; tr.title = 'This matches your current IP'; } + ['ip','country','date','where'].forEach(function(k){ + var td = document.createElement('td'); + td.textContent = row[k] || ''; + tr.appendChild(td); + }); + tbody.appendChild(tr); + }); + if (toEl && typeof d.to !== 'undefined') toEl.textContent = d.to; + btn.setAttribute('data-page', d.page); + if (d.has_more) { btn.disabled = false; btn.textContent = label; } + else if (btn.parentNode) { btn.parentNode.removeChild(btn); } + }) + .catch(function(){ btn.disabled = false; btn.textContent = label; }); + }); +})(); diff --git a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php index 7cc3fd577..7c138c682 100644 --- a/src/acore-wp-plugin/src/Components/UserPanel/UserController.php +++ b/src/acore-wp-plugin/src/Components/UserPanel/UserController.php @@ -165,6 +165,12 @@ public function showSecurityPage() { $ingame2faActive = $this->getIngame2faStatus(); $connections = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); + // TEMP preview: ?mock_connections=50 fills the Recent Connections table + // with fake rows so the styling can be reviewed. Remove when no longer needed. + if (isset($_GET['mock_connections'])) { + $connections = \ACore\Hooks\User\acore_mock_login_history($_GET['mock_connections']); + } + echo $this->getView()->getSecurityRender($connections, $passwordChangedAt, $twoFaData, $ingame2faActive, $passwordMessage); } diff --git a/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php b/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php index c05af8d42..3af7f0c03 100644 --- a/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php +++ b/src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php @@ -5,19 +5,19 @@ add_action('init', __NAMESPACE__ . '\\acore_create_login_history_table'); function acore_create_login_history_table() { - global $wpdb; - $table = $wpdb->prefix . 'acore_login_history'; - - if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) { + if (get_option('acore_login_history_db_version') === '2') { return; } + global $wpdb; + $table = $wpdb->prefix . 'acore_login_history'; $collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id bigint(20) UNSIGNED NOT NULL, ip_address varchar(45) NOT NULL, country varchar(10) NOT NULL DEFAULT 'Unknown', + source varchar(20) NOT NULL DEFAULT 'website', login_at datetime NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), @@ -26,6 +26,7 @@ function acore_create_login_history_table() { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); + update_option('acore_login_history_db_version', '2'); } add_action('wp_login', __NAMESPACE__ . '\\acore_log_login', 10, 2); @@ -46,9 +47,60 @@ function acore_log_login($user_login, $user) { 'user_id' => $user->ID, 'ip_address' => $ip, 'country' => $country, + 'source' => 'website', 'login_at' => current_time('mysql'), ], - ['%d', '%s', '%s', '%s'] + ['%d', '%s', '%s', '%s', '%s'] + ); + + // Also capture the account's most recent in-game login IP. + acore_log_ingame_last_ip($user); +} + +/** + * Record the player's last in-game login IP (from the game account table) as an + * "ingame" connection entry. Skipped if it already matches the latest one. + */ +function acore_log_ingame_last_ip($user) { + try { + $conn = \ACore\Manager\ACoreServices::I()->getAccountEm()->getConnection(); + $row = $conn->executeQuery( + 'SELECT last_ip, last_login FROM account WHERE username = ?', + [strtoupper($user->user_login)] + )->fetchAssociative(); + } catch (\Throwable $e) { + return; + } + + if (!$row || empty($row['last_ip']) || $row['last_ip'] === '0.0.0.0') { + return; + } + + $ip = sanitize_text_field($row['last_ip']); + $loginAt = !empty($row['last_login']) ? $row['last_login'] : current_time('mysql'); + + global $wpdb; + $table = $wpdb->prefix . 'acore_login_history'; + + // De-dup: skip if the latest ingame row already matches this IP + time. + $last = $wpdb->get_row($wpdb->prepare( + "SELECT ip_address, login_at FROM {$table} WHERE user_id = %d AND source = 'ingame' ORDER BY login_at DESC, id DESC LIMIT 1", + $user->ID + ), ARRAY_A); + if ($last && $last['ip_address'] === $ip && (string) $last['login_at'] === (string) $loginAt) { + return; + } + + $wpdb->insert( + $table, + [ + 'user_id' => $user->ID, + 'ip_address' => $ip, + 'country' => acore_lookup_country($ip), + 'source' => 'ingame', + 'login_at' => $loginAt, + ], + ['%d', '%s', '%s', '%s', '%s'] ); } @@ -98,10 +150,46 @@ function acore_get_login_history($user_id, $limit = 50) { global $wpdb; $table = $wpdb->prefix . 'acore_login_history'; return $wpdb->get_results($wpdb->prepare( - "SELECT ip_address, country, login_at FROM $table WHERE user_id = %d ORDER BY login_at DESC LIMIT %d", + "SELECT ip_address, country, login_at, source FROM $table WHERE user_id = %d ORDER BY login_at DESC LIMIT %d", $user_id, $limit - )); + ), ARRAY_A); +} + +/** + * Build realistic mock connection rows for UI preview (?mock_connections=N). + * IPs repeat across website/in-game like real usage, and the viewer's own IP + * appears in both so the "current IP" highlight is visible. + */ +function acore_mock_login_history($count) { + // Default size when ?mock_connections is present without a number, and a + // hard cap so it can never render more than this many rows. + $count = (int) $count; + if ($count <= 0) { + $count = 120; + } + $count = max(1, min(200, $count)); // 200 = hard cap + $myIp = acore_resolve_client_ip(); + $pool = [ + [$myIp, 'Local'], + ['203.0.113.45', 'US'], + ['198.51.100.22', 'GB'], + ['192.0.2.181', 'DE'], + ['203.0.113.45', 'US'], + [$myIp, 'Local'], + ['198.51.100.22', 'GB'], + ]; + $out = []; + for ($i = 0; $i < $count; $i++) { + $entry = $pool[$i % count($pool)]; + $out[] = [ + 'ip_address' => $entry[0], + 'country' => $entry[1], + 'login_at' => date('Y-m-d H:i:s', time() - $i * 3600), + 'source' => ($i % 2 === 0) ? 'website' : 'ingame', + ]; + } + return $out; } function acore_format_connection_date($datetime_str) { diff --git a/src/acore-wp-plugin/src/Hooks/User/User.php b/src/acore-wp-plugin/src/Hooks/User/User.php index 34594dfd2..24bd449d2 100644 --- a/src/acore-wp-plugin/src/Hooks/User/User.php +++ b/src/acore-wp-plugin/src/Hooks/User/User.php @@ -780,33 +780,54 @@ function acore_profile_2fa_removal_warning($user) { function acore_profile_recent_connections($user) { $security_url = admin_url('profile.php?page=' . ACORE_SLUG . '-security'); - $rows = \ACore\Hooks\User\acore_get_login_history($user->ID, 20); + $myIp = acore_resolve_client_ip(); + + if (isset($_GET['mock_connections'])) { + $rows = acore_mock_login_history($_GET['mock_connections']); + } else { + $rows = acore_get_login_history($user->ID, 10); + } + $rows = array_slice($rows, 0, 10); ?> -

    +

    - +

    + + +

    +
    - + + - + - - - - - + + > + + + +
    - 10): ?> -

    - +

    + +

    ' . "\n"; + . esc_url(ACORE_URL_PLG . 'web/assets/css/theme.css') . '?ver=2.5" />' . "\n"; } diff --git a/src/acore-wp-plugin/web/assets/css/theme.css b/src/acore-wp-plugin/web/assets/css/theme.css index 68db8bdce..0ea0e1efe 100644 --- a/src/acore-wp-plugin/web/assets/css/theme.css +++ b/src/acore-wp-plugin/web/assets/css/theme.css @@ -103,20 +103,25 @@ body.acore-dark-mode #acore-security-page code { border-radius: 3px; padding: 1px 5px; } -/* Recent Connections table */ +/* Recent Connections table - stripe rows at the level so WP's light + .striped odd-row background doesn't show through transparent cells. */ body.acore-dark-mode #acore-security-page .wp-list-table { background: var(--acore-surface) !important; border-color: var(--acore-border) !important; } +body.acore-dark-mode #acore-security-page .wp-list-table > thead > tr, +body.acore-dark-mode #acore-security-page .wp-list-table > tbody > tr:nth-child(odd) { + background: var(--acore-surface-2) !important; +} +body.acore-dark-mode #acore-security-page .wp-list-table > tbody > tr:nth-child(even) { + background: var(--acore-surface) !important; +} body.acore-dark-mode #acore-security-page .wp-list-table th, body.acore-dark-mode #acore-security-page .wp-list-table td { color: var(--acore-text) !important; border-color: var(--acore-border) !important; background: transparent !important; } -body.acore-dark-mode #acore-security-page .wp-list-table tr:nth-child(even) td { - background: var(--acore-surface-2) !important; -} /* ── Mail Return surfaces ───────────────────────────────────────────── */ body.acore-dark-mode #acore-mail-return-page .mail-entry { background: var(--acore-surface) !important; } @@ -376,3 +381,75 @@ body.acore-dark-mode .acore-2fa-required-note { border-color: #8a6d1f !important; } body.acore-dark-mode .acore-2fa-required-note .dashicons { color: #f5c451 !important; } + +/* ── Highlight the connection row matching the viewer's current IP ── */ +#acore-security-page .wp-list-table > tbody > tr.acore-conn-current > td { + background: #fff3cd !important; + border-color: #f0d488 !important; +} +#acore-security-page .wp-list-table > tbody > tr.acore-conn-current > td:first-child { + box-shadow: inset 3px 0 0 0 #e0a800; + font-weight: 600; +} +body.acore-dark-mode #acore-security-page .wp-list-table > tbody > tr.acore-conn-current > td { + background: rgba(240, 180, 41, 0.16) !important; + color: #f5d98a !important; + border-color: #6e5a1e !important; +} +body.acore-dark-mode #acore-security-page .wp-list-table > tbody > tr.acore-conn-current > td:first-child { + box-shadow: inset 3px 0 0 0 #f0b429; +} + +/* ── Shared connection tables (Security page + Profile page) ── */ +body.acore-dark-mode .acore-conn-table { + background: var(--acore-surface) !important; + border-color: var(--acore-border) !important; +} +body.acore-dark-mode .acore-conn-table > thead > tr, +body.acore-dark-mode .acore-conn-table > tbody > tr:nth-child(odd) { + background: var(--acore-surface-2) !important; +} +body.acore-dark-mode .acore-conn-table > tbody > tr:nth-child(even) { + background: var(--acore-surface) !important; +} +body.acore-dark-mode .acore-conn-table th, +body.acore-dark-mode .acore-conn-table td { + color: var(--acore-text) !important; + border-color: var(--acore-border) !important; + background: transparent !important; +} +.acore-conn-table > tbody > tr.acore-conn-current > td { + background: #fff3cd !important; + border-color: #f0d488 !important; +} +.acore-conn-table > tbody > tr.acore-conn-current > td:first-child { + box-shadow: inset 3px 0 0 0 #e0a800; + font-weight: 600; +} +body.acore-dark-mode .acore-conn-table > tbody > tr.acore-conn-current > td { + background: rgba(240, 180, 41, 0.16) !important; + color: #f5d98a !important; + border-color: #6e5a1e !important; +} +body.acore-dark-mode .acore-conn-table > tbody > tr.acore-conn-current > td:first-child { + box-shadow: inset 3px 0 0 0 #f0b429; +} + +/* "Your IPv4:" label (right-aligned) + connection notes - readable both themes */ +.acore-conn-heading { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; + max-width: 860px; + flex-wrap: wrap; +} +.acore-conn-myip { + font-size: 13px; + font-weight: 400; + white-space: nowrap; + color: #50575e; +} +body.acore-dark-mode .acore-conn-myip { color: #c9d1d9; } +.acore-conn-note { color: #50575e; font-size: 13px; } +body.acore-dark-mode .acore-conn-note { color: #9aa4af !important; } From 3204234075ed3cf1a3e1dfecbe7d7a25440f982d Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sun, 21 Jun 2026 06:14:45 +0100 Subject: [PATCH 35/47] Expand the logging login and infinite scrolling to the Tools tab admins --- .../src/Components/AdminPanel/Pages/Tools.php | 74 ++++++++++++------- .../Components/ServerInfo/ServerInfoApi.php | 42 ++++++++--- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index db6dafb56..cc2737a0f 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -281,10 +281,13 @@ class=""

    - + +

    + +

    @@ -421,38 +424,59 @@ function wire2fa(type, $userInput, $check, $remove, $msg, onCheck) { }); /* User Login History lookup */ - $('#acore-history-lookup').on('click', function(){ - var username = $('#acore-history-user').val().trim(); - var $msg = $('#acore-history-msg'); - var $tbl = $('#acore-history-table'); - if (!username) { $msg.css('color','#d63638').text('Enter an account name first.'); return; } - var $btn = $(this).prop('disabled', true).text('Looking up…'); - $msg.css('color','#646970').text(''); - ajaxPost('admin/login-history', { username: username }) + var acoreHistory = { username: '', mock: null, page: 0, total: 0, shown: 0 }; + + function acoreHistoryFetch(page) { + var $tbl = $('#acore-history-table'), $tb = $tbl.find('tbody'), + $msg = $('#acore-history-msg'), $more = $('#acore-history-more'); + return ajaxPost('admin/login-history', { + username: acoreHistory.username, + mock: acoreHistory.mock, + page: page + }) .done(function(data){ var rows = data.history || []; - var $tb = $tbl.find('tbody').empty(); - if (!rows.length) { - $tbl.hide(); + if (page === 1) { $tb.empty(); acoreHistory.shown = 0; acoreHistory.total = data.total || 0; } + if (page === 1 && !rows.length) { + $tbl.hide(); $more.hide(); $msg.css('color','#646970').text('No login history recorded for ' + data.username + '.'); - } else { - rows.forEach(function(r){ - $('').append( - $('').text(r.ip), - $('').text(r.country), - $('').text(r.date) - ).appendTo($tb); - }); - $tbl.show(); - $msg.css('color','#646970').text(rows.length + ' record(s) for ' + data.username + '.'); + return; } + rows.forEach(function(r){ + $('').append( + $('').text(r.ip), + $('').text(r.country), + $('').text(r.date), + $('').text(r.where) + ).appendTo($tb); + }); + acoreHistory.shown += rows.length; + acoreHistory.page = data.page || page; + acoreHistory.total = data.total || acoreHistory.total; + $tbl.show(); + $msg.css('color','#646970').text('Showing ' + acoreHistory.shown + ' of ' + acoreHistory.total + ' for ' + data.username + '.'); + $more.toggle(!!data.has_more); }) .fail(function(xhr){ - $tbl.hide(); + if (page === 1) { $tbl.hide(); $more.hide(); } var err = xhr.responseJSON ? (xhr.responseJSON.message || 'Error.') : 'Error.'; $msg.css('color','#d63638').text(err); - }) - .always(function(){ $btn.prop('disabled', false).text('Look up'); }); + }); + } + + $('#acore-history-lookup').on('click', function(){ + var username = $('#acore-history-user').val().trim(); + if (!username) { $('#acore-history-msg').css('color','#d63638').text('Enter an account name first.'); return; } + acoreHistory.username = username; + acoreHistory.mock = new URLSearchParams(location.search).get('mock_connections'); + $('#acore-history-msg').css('color','#646970').text(''); + var $btn = $(this).prop('disabled', true).text('Looking up…'); + acoreHistoryFetch(1).always(function(){ $btn.prop('disabled', false).text('Look up'); }); + }); + + $('#acore-history-more').on('click', function(){ + var $b = $(this).prop('disabled', true).text('Loading…'); + acoreHistoryFetch(acoreHistory.page + 1).always(function(){ $b.prop('disabled', false).text('See more'); }); }); /* ── Name Unlock Thresholds ───────────────────────────────────────── */ diff --git a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php index adc92bfa7..870711887 100644 --- a/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php +++ b/src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php @@ -379,22 +379,46 @@ function acore_2fa_sync_self_removals(int $userId): void { 'callback' => function( \WP_REST_Request $request ) { $data = $request->get_json_params(); $username = isset($data['username']) ? sanitize_text_field($data['username']) : ''; + $mock = isset($data['mock']) ? $data['mock'] : null; + $page = max(1, (int) ($data['page'] ?? 1)); + $perPage = 50; if ($username === '') return new \WP_Error('missing_username', 'Username is required.', ['status' => 400]); - $user = get_user_by('login', $username); - if (!$user) - return new \WP_Error('user_not_found', 'No WordPress account found with that username.', ['status' => 404]); - $rows = \ACore\Hooks\User\acore_get_login_history($user->ID, 200); + if ($mock !== null && $mock !== '') { + // Preview mode: return mock connections for the searched account. + $all = \ACore\Hooks\User\acore_mock_login_history((int) $mock); + } else { + $user = get_user_by('login', $username); + if (!$user) + return new \WP_Error('user_not_found', 'No WordPress account found with that username.', ['status' => 404]); + $all = \ACore\Hooks\User\acore_get_login_history($user->ID, 500); + } + + $all = is_array($all) ? $all : []; + $total = count($all); + $offset = ($page - 1) * $perPage; + $slice = array_slice($all, $offset, $perPage); + $history = []; - foreach ((array) $rows as $r) { + foreach ($slice as $r) { $history[] = [ - 'ip' => $r->ip_address, - 'country' => $r->country, - 'date' => \ACore\Hooks\User\acore_format_connection_date($r->login_at), + 'ip' => $r['ip_address'] ?? '', + 'country' => $r['country'] ?? 'Unknown', + 'date' => isset($r['login_at']) ? \ACore\Hooks\User\acore_format_connection_date($r['login_at']) : '', + 'where' => (($r['source'] ?? 'website') === 'ingame') ? 'In-game' : 'Website', ]; } - return ['found' => true, 'username' => $user->user_login, 'history' => $history]; + return [ + 'found' => true, + 'username' => $username, + 'history' => $history, + 'total' => $total, + 'from' => $total ? $offset + 1 : 0, + 'to' => $offset + count($slice), + 'page' => $page, + 'has_more' => ($offset + count($slice)) < $total, + ]; } )); From aca7a16df94b73705358123dce072090420cc7f1 Mon Sep 17 00:00:00 2001 From: FlyingArowana <16946913+TheSCREWEDSoftware@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:24:13 +0100 Subject: [PATCH 36/47] Dark theme tweaks --- .../src/Components/AdminPanel/Pages/Tools.php | 18 +- .../CharactersMenu/CharactersView.php | 9 +- .../Components/ServerInfo/ServerInfoApi.php | 4 +- .../src/Hooks/Various/DarkMode.php | 4 +- src/acore-wp-plugin/web/assets/css/theme.css | 166 +++++++++++++++++- 5 files changed, 174 insertions(+), 27 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index cc2737a0f..6f26cc706 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -9,17 +9,7 @@ ?> +