diff --git a/assets/admin/src/pages/extensions/ExtensionSecurity.vue b/assets/admin/src/pages/extensions/ExtensionSecurity.vue index 2b148ab5..8885a190 100644 --- a/assets/admin/src/pages/extensions/ExtensionSecurity.vue +++ b/assets/admin/src/pages/extensions/ExtensionSecurity.vue @@ -154,32 +154,38 @@ onMounted(async () => { const d = res.data const svc = res.services || {} + const captcha = d.captcha || svc.captcha || {} + const captchaOn = !!(captcha.enabled && captcha.configured) + const captchaScenes = captcha.scenes || {} + const audit = d.content_audit || svc.audit || {} + const ipGeo = d.ip_geo || svc.ip_geo || {} + securityItems.value[0].value = d.email_gate_enabled ? '已开启' : '未开启' securityItems.value[0].color = d.email_gate_enabled ? '#52c41a' : '#8a909c' - securityItems.value[1].value = d.captcha_enabled ? '已开启' : '未开启' - securityItems.value[1].color = d.captcha_enabled ? '#52c41a' : '#8a909c' + securityItems.value[1].value = captchaOn ? '已开启' : '未开启' + securityItems.value[1].color = captchaOn ? '#52c41a' : '#8a909c' - if (svc.audit?.enabled) { - securityItems.value[2].value = svc.audit.configured ? '已配置' : '待补全' - securityItems.value[2].color = svc.audit.configured ? '#52c41a' : '#faad14' + if (audit.enabled) { + securityItems.value[2].value = audit.configured ? '已配置' : '待补全' + securityItems.value[2].color = audit.configured ? '#52c41a' : '#faad14' } else { securityItems.value[2].value = '未开启' securityItems.value[2].color = '#8a909c' } - if (svc.ip_geo?.enabled) { - securityItems.value[3].value = svc.ip_geo.configured ? '已配置' : '待补全' - securityItems.value[3].color = svc.ip_geo.configured ? '#52c41a' : '#faad14' + if (ipGeo.enabled) { + securityItems.value[3].value = ipGeo.configured ? '已配置' : '待补全' + securityItems.value[3].color = ipGeo.configured ? '#52c41a' : '#faad14' } else { securityItems.value[3].value = '未开启' securityItems.value[3].color = '#8a909c' } features.value[0].enabled = !!d.email_gate_enabled - features.value[1].enabled = !!d.captcha_comment_enabled - features.value[2].enabled = !!d.captcha_download_enabled - features.value[3].enabled = !!(svc.audit?.enabled ?? d.content_audit_enabled) - features.value[4].enabled = !!(svc.ip_geo?.enabled ?? d.ip_geo_enabled) + features.value[1].enabled = !!captchaScenes.comment + features.value[2].enabled = !!captcha.download_active + features.value[3].enabled = !!audit.enabled + features.value[4].enabled = !!ipGeo.enabled if (d.risk_switches) { riskSwitches.value = { ...riskSwitches.value, ...d.risk_switches } diff --git a/assets/admin/src/pages/extensions/ExtensionTools.vue b/assets/admin/src/pages/extensions/ExtensionTools.vue index 31e8515d..4d9d7598 100644 --- a/assets/admin/src/pages/extensions/ExtensionTools.vue +++ b/assets/admin/src/pages/extensions/ExtensionTools.vue @@ -136,8 +136,8 @@ onMounted(async () => { services.value[0].status = svc.sms?.provider || (s.sms_enabled ? '已启用' : '未配置') services.value[1].available = !!(svc.wechat?.available ?? s.wechat_enabled) services.value[1].status = svc.wechat?.provider || (s.wechat_enabled ? '已启用' : '未配置') - services.value[2].available = !!(svc.captcha?.available ?? s.captcha_enabled) - services.value[2].status = svc.captcha?.provider || (s.captcha_enabled ? '已启用' : '未配置') + services.value[2].available = !!(svc.captcha?.available ?? svc.captcha?.configured) + services.value[2].status = svc.captcha?.provider || (svc.captcha?.enabled ? '已启用' : '未配置') services.value[3].available = !!(svc.push?.available ?? s.baidu_push_enabled) services.value[3].status = svc.push?.provider || (s.baidu_push_enabled ? '已启用' : '未配置') services.value[4].available = !!(svc.audit?.available ?? s.content_audit_enabled) diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js index 8574a748..df4b0b8f 100644 --- a/assets/admin/src/schema/sections/permissions/index.js +++ b/assets/admin/src/schema/sections/permissions/index.js @@ -64,6 +64,7 @@ export const permissionsSchemas = { { label: '用户登录', value: 'login' }, { label: '用户注册', value: 'register' }, { label: '绑定第三方', value: 'bind' }, + { label: '绑定手机', value: 'bind_phone' }, { label: '找回密码', value: 'lostpassword' }, { label: '修改密码', value: 'change_password' }, { label: '更改邮箱', value: 'change_email' }, diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index 76cc7984..310ac0c6 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -443,7 +443,7 @@ m.classList.remove('is-open'); document.body.style.overflow = ''; var f = m.querySelector('form'); - var sceneMap = { 'pwd-modal': 'change_password', 'email-modal': 'change_email' }; + var sceneMap = { 'pwd-modal': 'change_password', 'email-modal': 'change_email', 'phone-modal': 'bind_phone' }; var scene = m.id && sceneMap[m.id] ? sceneMap[m.id] : ''; var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes ? window.AKSRTCaptchaState.scenes : null; if (f) { @@ -687,4 +687,93 @@ initNotifications(); initSettings(); } + + // 绑定手机 + (function initBindPhone() { + var form = document.getElementById('aksrt-bind-phone-form'); + if (!form) return; + form.addEventListener('submit', function (e) { + e.preventDefault(); + var phoneEl = form.querySelector('[name="phone"]'); + var codeEl = form.querySelector('[name="code"]'); + var phone = phoneEl ? String(phoneEl.value || '').trim() : ''; + var code = codeEl ? String(codeEl.value || '').trim() : ''; + if (!/^1[3-9]\d{9}$/.test(phone)) { + showToast('请输入正确的手机号'); + return; + } + if (!code) { + showToast('请输入短信验证码'); + return; + } + var submitBtn = form.querySelector('[type="submit"]'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = '绑定中...'; + } + function collectCaptcha(formEl) { + var out = {}; + [ + 'captcha_verified', 'tencent_ticket', 'tencent_randstr', 'tencent_sid', + 'cf_turnstile_response', 'geetest_lot_number', 'geetest_captcha_output', + 'geetest_pass_token', 'geetest_gen_time', 'aliyun_captcha_verify_param' + ].forEach(function (name) { + var el = formEl.querySelector('[name="' + name + '"]'); + if (el && el.value) out[name] = el.value; + }); + return out; + } + function resetCaptcha() { + try { + var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes; + if (store && store.bind_phone && typeof store.bind_phone.reset === 'function') { + store.bind_phone.reset(); + } + } catch (err) {} + } + function doBind() { + var captcha = collectCaptcha(form); + var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1'; + var nonce = (window.aksrtData && aksrtData.restNonce) || ''; + fetch(String(restRoot).replace(/\/$/, '') + '/auth/bind-phone', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': nonce + }, + body: JSON.stringify(Object.assign({ phone: phone, code: code }, captcha)) + }) + .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); }) + .then(function (res) { + if (res.ok && res.json && res.json.success) { + showToast((res.json.message) || '绑定成功'); + setTimeout(function () { window.location.reload(); }, 600); + return; + } + resetCaptcha(); + showToast((res.json && (res.json.message || res.json.error)) || '绑定失败'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = '确认绑定'; + } + }) + .catch(function () { + resetCaptcha(); + showToast('网络错误'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = '确认绑定'; + } + }); + } + var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes; + if (store && store.bind_phone && typeof store.bind_phone.verify === 'function') { + store.bind_phone.verify(doBind); + } else { + doBind(); + } + }); + })(); + })(); diff --git a/inc/classes/admin/class-aksrt-admin-extension-controller.php b/inc/classes/admin/class-aksrt-admin-extension-controller.php index e9142c53..84e5ba32 100644 --- a/inc/classes/admin/class-aksrt-admin-extension-controller.php +++ b/inc/classes/admin/class-aksrt-admin-extension-controller.php @@ -148,13 +148,6 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { $flat["{$key}_provider"] = (string) ($svc['provider'] ?? ''); } - // 保留旧布尔字段兼容 - $flat['sms_enabled'] = !empty($services['sms']['enabled']); - $flat['wechat_enabled'] = !empty($services['wechat']['enabled']); - $flat['captcha_enabled'] = !empty($services['captcha']['enabled']); - $flat['baidu_push_enabled'] = !empty($services['push']['enabled']); - $flat['content_audit_enabled'] = !empty($services['audit']['enabled']); - return $this->success([ 'data' => $flat, 'services' => $services, @@ -173,12 +166,11 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { $email_gate_enabled = false; } - $captcha_summary = function_exists('aksrt_get_captcha_status_summary') + $captcha = function_exists('aksrt_get_captcha_status_summary') ? aksrt_get_captcha_status_summary() - : ['enabled' => false, 'configured' => false, 'scenes' => []]; - $captcha_comment_enabled = !empty($captcha_summary['scenes']['comment']); + : ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []]; $features = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_cms_features() : []; - $captcha_download_enabled = !empty($features['download_captcha']) && !empty($captcha_summary['scenes']['download']); + $scenes = is_array($captcha['scenes'] ?? null) ? $captcha['scenes'] : []; $risk_switches = [ 'show_unapproved_comments' => !empty($settings['show_unapproved_comments']), @@ -188,17 +180,25 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { return $this->success([ 'data' => [ - 'email_gate_enabled' => $email_gate_enabled, - 'captcha_comment_enabled' => $captcha_comment_enabled, - 'captcha_download_enabled' => $captcha_download_enabled, - 'content_audit_enabled' => !empty($services['audit']['enabled']), - 'content_audit_configured' => !empty($services['audit']['configured']), - 'ip_geo_enabled' => !empty($services['ip_geo']['enabled']), - 'ip_geo_configured' => !empty($services['ip_geo']['configured']), - 'captcha_enabled' => !empty($captcha_summary['enabled']) && !empty($captcha_summary['configured']), - 'captcha_summary' => $captcha_summary, - 'risk_switches' => $risk_switches, - 'services' => $services, + 'email_gate_enabled' => $email_gate_enabled, + 'captcha' => [ + 'enabled' => !empty($captcha['enabled']), + 'provider' => (string) ($captcha['provider'] ?? ''), + 'configured' => !empty($captcha['configured']), + 'scenes' => $scenes, + 'download_required' => !empty($features['download_captcha']), + 'download_active' => !empty($features['download_captcha']) && !empty($scenes['download']), + ], + 'content_audit' => [ + 'enabled' => !empty($services['audit']['enabled']), + 'configured' => !empty($services['audit']['configured']), + ], + 'ip_geo' => [ + 'enabled' => !empty($services['ip_geo']['enabled']), + 'configured' => !empty($services['ip_geo']['configured']), + ], + 'risk_switches' => $risk_switches, + 'services' => $services, ], ]); } diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php index e011cc20..eb254028 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php @@ -37,6 +37,7 @@ class AKSRT_Admin_Schema_Permissions { ['label' => '用户登录', 'value' => 'login'], ['label' => '用户注册', 'value' => 'register'], ['label' => '绑定第三方', 'value' => 'bind'], + ['label' => '绑定手机', 'value' => 'bind_phone'], ['label' => '找回密码', 'value' => 'lostpassword'], ['label' => '修改密码', 'value' => 'change_password'], ['label' => '更改邮箱', 'value' => 'change_email'], diff --git a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php index d8603e6f..04330d84 100644 --- a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php +++ b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php @@ -425,6 +425,13 @@ trait AKSRT_Auth_Service_Part_1b { return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')]; } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('bind_phone', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; + } + } + // 验证 SMS 验证码 if (class_exists('AKSRT_SMS')) { $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code); diff --git a/inc/classes/class-aksrt-captcha.php b/inc/classes/class-aksrt-captcha.php index a3636287..2303f9d7 100644 --- a/inc/classes/class-aksrt-captcha.php +++ b/inc/classes/class-aksrt-captcha.php @@ -32,6 +32,7 @@ function aksrt_captcha_scenes(): array { 'login' => __('用户登录', 'aksrt'), 'register' => __('用户注册', 'aksrt'), 'bind' => __('绑定第三方', 'aksrt'), + 'bind_phone' => __('绑定手机', 'aksrt'), 'lostpassword' => __('找回密码', 'aksrt'), 'change_password' => __('修改密码', 'aksrt'), 'change_email' => __('更改邮箱', 'aksrt'), @@ -621,7 +622,22 @@ var triggerCaptcha=function(){ showTip('验证码加载中,请稍候', '#e67e2 function findForm(){ form=box.closest('form'); if(!form&&C.scene==='comment') form=document.getElementById('commentform'); - if(!form&&C.scene==='login') form=document.getElementById('aksrt-login-form')||document.querySelector('.auth-form'); + if(!form&&(C.scene==='login'||C.scene==='register')){ + form=document.getElementById('aksrt-login-form') + ||document.getElementById('aksrt-modal-login-form') + ||document.getElementById('sign-login-form') + ||document.getElementById('aksrt-phone-login-form') + ||document.getElementById('aksrt-modal-register-form') + ||document.getElementById('sign-register-form') + ||document.getElementById('aksrt-phone-register-form') + ||document.querySelector('.auth-form') + ||document.querySelector('.sign-form'); + } + if(!form&&C.scene==='bind_phone'){ + form=document.getElementById('aksrt-bind-phone-form') + ||document.querySelector('[data-aksrt-bind-phone-form]') + ||document.querySelector('.auth-form'); + } return form; } diff --git a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php index 6dfd2728..f5286d9d 100644 --- a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php +++ b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php @@ -266,6 +266,17 @@ trait AKSRT_Scripts_Part_1b { $version, true ); + // 绑定手机短信发送按钮依赖 sign-register 的 data-aksrt-sms-trigger 契约 + $sign_register_js = get_template_directory() . '/assets/js/sign-register.js'; + if (file_exists($sign_register_js)) { + wp_enqueue_script( + 'aksrt-sign-register', + $base_uri . '/assets/js/sign-register.js', + ['aksrt-route-user-center'], + (string) filemtime($sign_register_js), + true + ); + } } } diff --git a/inc/helpers/user-center-renderers/profile-account-modals.php b/inc/helpers/user-center-renderers/profile-account-modals.php index 029c8304..443527ff 100644 --- a/inc/helpers/user-center-renderers/profile-account-modals.php +++ b/inc/helpers/user-center-renderers/profile-account-modals.php @@ -96,3 +96,32 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ + +