diff --git a/assets/admin/src/api/modules/users.js b/assets/admin/src/api/modules/users.js index 897e9b96..b9aed5e6 100644 --- a/assets/admin/src/api/modules/users.js +++ b/assets/admin/src/api/modules/users.js @@ -20,6 +20,14 @@ export const usersApiMethods = { deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) }, runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) }, + getIdentitySettings() { return this.request('/identity/settings') }, + saveIdentitySettings(data) { + return this.request('/identity/settings', { method: 'POST', body: JSON.stringify(data) }) + }, + testIdentitySettings(data = {}) { + return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) }) + }, + getCreditSettings() { return this.request('/credits/settings') }, saveCreditSettings(data) { return this.request('/credits/settings', { method: 'POST', body: JSON.stringify(data) }) }, listCreditUsers(params = {}) { diff --git a/assets/admin/src/constants/sections/users.js b/assets/admin/src/constants/sections/users.js index 5496a38e..638d4ee8 100644 --- a/assets/admin/src/constants/sections/users.js +++ b/assets/admin/src/constants/sections/users.js @@ -63,11 +63,12 @@ export const usersSection = { canonicalDomain: 'users', section: 'credits', }), - schemaPage({ + customPage({ key: 'users-verify', name: 'UsersVerify', label: '身份认证', route: '/users/verify', + component: 'UsersVerify', title: '身份认证', desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。', canonicalDomain: 'users', diff --git a/assets/admin/src/pages/users/UsersVerify.vue b/assets/admin/src/pages/users/UsersVerify.vue new file mode 100644 index 00000000..39790ea6 --- /dev/null +++ b/assets/admin/src/pages/users/UsersVerify.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/assets/admin/src/router/component-loaders/users.js b/assets/admin/src/router/component-loaders/users.js index 9f850d99..f5e91037 100644 --- a/assets/admin/src/router/component-loaders/users.js +++ b/assets/admin/src/router/component-loaders/users.js @@ -1,6 +1,7 @@ export const usersPageLoaders = { UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'), UsersSignin: () => import('@/pages/users/UsersSignin.vue'), + UsersVerify: () => import('@/pages/users/UsersVerify.vue'), UsersLevel: () => import('@/pages/users/UsersLevel.vue'), UsersBadge: () => import('@/pages/users/UsersBadge.vue'), UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'), diff --git a/src/AKSRT/Admin/UserController.php b/src/AKSRT/Admin/UserController.php index 06201886..07e02238 100644 --- a/src/AKSRT/Admin/UserController.php +++ b/src/AKSRT/Admin/UserController.php @@ -11,6 +11,7 @@ class UserController extends ControllerBase { use UserControllerReportOperations; use UserControllerInviteOperations; use UserControllerVerifyIntegrations; + use UserControllerIdentitySettings; use UserControllerAuthPhoneOperations; use UserControllerRestrictionOperations; use UserControllerFollowOperations; @@ -260,6 +261,25 @@ class UserController extends ControllerBase { 'permission_callback' => function () { return is_user_logged_in(); }, ]); + // Identity verification provider settings (admin only) + register_rest_route(self::API_NAMESPACE, '/identity/settings', [ + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_identity_settings'], + 'permission_callback' => [$this, 'check_permission'], + ], + [ + 'methods' => 'POST', + 'callback' => [$this, 'update_identity_settings'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + register_rest_route(self::API_NAMESPACE, '/identity/settings/test', [ + 'methods' => 'POST', + 'callback' => [$this, 'test_identity_settings'], + 'permission_callback' => [$this, 'check_permission'], + ]); + // SMS provider status register_rest_route(self::API_NAMESPACE, '/sms/status', [ 'methods' => 'GET', diff --git a/src/AKSRT/Admin/UserControllerIdentitySettings.php b/src/AKSRT/Admin/UserControllerIdentitySettings.php new file mode 100644 index 00000000..540f76fa --- /dev/null +++ b/src/AKSRT/Admin/UserControllerIdentitySettings.php @@ -0,0 +1,49 @@ +success([ + 'data' => $this->identity_settings_response(IdentityAdmin::read()), + ]); + } + + public function update_identity_settings(\WP_REST_Request $request): \WP_REST_Response { + $settings = IdentityAdmin::update($this->get_json_params($request)); + + return $this->success([ + 'message' => __('身份认证服务商配置已保存', 'aksrt'), + 'data' => $this->identity_settings_response($settings), + ]); + } + + public function test_identity_settings(\WP_REST_Request $request): \WP_REST_Response { + $params = $this->get_json_params($request); + $provider = sanitize_key((string) ($params['provider'] ?? '')); + $result = IdentityAdmin::test($provider); + + if (empty($result['success'])) { + return $this->error((string) ($result['message'] ?? __('配置检查失败', 'aksrt')), 422); + } + + return $this->success([ + 'message' => (string) $result['message'], + 'data' => ['provider' => $provider, 'configured' => true], + ]); + } + + private function identity_settings_response(array $settings): array { + $settings['callback_url'] = rest_url(self::API_NAMESPACE . '/identity/callback'); + return $settings; + } +} diff --git a/src/AKSRT/Settings/IdentityAdmin.php b/src/AKSRT/Settings/IdentityAdmin.php new file mode 100644 index 00000000..134a86f6 --- /dev/null +++ b/src/AKSRT/Settings/IdentityAdmin.php @@ -0,0 +1,158 @@ + ['access_key_id', 'access_key_secret'], + 'tencent' => ['secret_id', 'secret_key'], + ]; + + public static function read(): array { + $stored = get_option(self::OPTION_KEY, []); + $stored = is_array($stored) ? $stored : []; + $provider = sanitize_key((string) ($stored['provider'] ?? 'aliyun')); + + if (!isset(self::PROVIDER_FIELDS[$provider])) { + $provider = 'aliyun'; + } + + return [ + 'provider' => $provider, + 'providers' => [ + 'aliyun' => self::provider_state('aliyun', $stored), + 'tencent' => self::provider_state('tencent', $stored), + ], + ]; + } + + public static function update(array $input): array { + $stored = get_option(self::OPTION_KEY, []); + $stored = is_array($stored) ? $stored : []; + + if (array_key_exists('provider', $input)) { + $provider = sanitize_key((string) $input['provider']); + if (isset(self::PROVIDER_FIELDS[$provider])) { + $stored['provider'] = $provider; + } + } + + $provider_input = isset($input['providers']) && is_array($input['providers']) + ? $input['providers'] + : []; + + foreach (self::PROVIDER_FIELDS as $provider => $fields) { + $changes = isset($provider_input[$provider]) && is_array($provider_input[$provider]) + ? $provider_input[$provider] + : []; + + if (!empty($changes['clear_credentials'])) { + unset($stored['providers'][$provider]); + continue; + } + + $credentials = isset($changes['credentials']) && is_array($changes['credentials']) + ? $changes['credentials'] + : []; + + foreach ($fields as $field) { + if (!isset($credentials[$field]) || trim((string) $credentials[$field]) === '') { + continue; + } + $stored['providers'][$provider][$field] = self::encrypt( + sanitize_text_field((string) $credentials[$field]) + ); + } + } + + update_option(self::OPTION_KEY, $stored, false); + return self::read(); + } + + public static function test(string $provider): array { + $settings = self::read(); + if (!isset(self::PROVIDER_FIELDS[$provider])) { + return ['success' => false, 'message' => '不支持的身份认证服务商']; + } + + $state = $settings['providers'][$provider]; + if (!$state['configured']) { + return [ + 'success' => false, + 'message' => sprintf('%s凭证不完整,请补充后保存', $state['label']), + ]; + } + + return [ + 'success' => true, + 'message' => sprintf('%s配置检查通过,凭证字段完整', $state['label']), + ]; + } + + private static function encrypt(string $value): string { + if ($value === '' || !function_exists('openssl_encrypt')) { + return ''; + } + + $key = hash('sha256', wp_salt('auth'), true); + $iv = random_bytes(12); + $tag = ''; + $ciphertext = openssl_encrypt($value, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); + if ($ciphertext === false) { + return ''; + } + + return base64_encode($iv . $tag . $ciphertext); + } + + private static function decrypt(string $encoded): string { + if ($encoded === '' || !function_exists('openssl_decrypt')) { + return ''; + } + + $payload = base64_decode($encoded, true); + if ($payload === false || strlen($payload) <= 28) { + return ''; + } + + $iv = substr($payload, 0, 12); + $tag = substr($payload, 12, 16); + $ciphertext = substr($payload, 28); + $key = hash('sha256', wp_salt('auth'), true); + $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); + + return is_string($plaintext) ? $plaintext : ''; + } + + private static function provider_state(string $provider, array $stored): array { + $credentials = isset($stored['providers'][$provider]) && is_array($stored['providers'][$provider]) + ? $stored['providers'][$provider] + : []; + $missing = []; + $configured_fields = []; + + foreach (self::PROVIDER_FIELDS[$provider] as $field) { + $has_value = self::decrypt((string) ($credentials[$field] ?? '')) !== ''; + $configured_fields[$field] = $has_value; + if (!$has_value) { + $missing[] = $field; + } + } + + return [ + 'label' => $provider === 'aliyun' ? '阿里云' : '腾讯云', + 'configured' => $missing === [], + 'configured_fields' => $configured_fields, + 'missing_fields' => $missing, + ]; + } +}