Files
wordpress-theme-aksrt/assets/js/sign-register.js
T
kate522 02f77b756c fix(captcha): close human-verification loop end-to-end
Rewrite captcha core around aksrt_captcha_config only, require captcha
on login/comment/SMS/phone auth and related gates, wire frontend token
collection, and remove dead dual-source security settings plus slider enqueue.
2026-07-13 19:52:30 +08:00

411 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT Sign/Register — 统一认证交互脚本
*
* 对齐 zibll 的 sign-register.js:
* - 短信验证码倒计时(60 秒 + 服务端 remaining_time)
* - 手机号注册/登录表单交互
* - 人机验证联动
* - 密码可见性切换
* - 协议勾选校验
*/
;(() => {
'use strict'
const COUNTDOWN_KEY = 'aksrt_sms_cooldown'
const DEFAULT_COOLDOWN = 60
const REST_BASE = '/wp-json/aksrt/v1'
/**
* 初始化所有认证页面
*/
function initAuthPages() {
initSmsButtons()
initPasswordToggles()
initAgreementChecks()
initFormValidation()
initPhoneAuthTabs()
}
/* =================================================================
* SMS 验证码发送 + 倒计时
* ================================================================= */
function initSmsButtons() {
document.querySelectorAll('[data-aksrt-sms-trigger]').forEach(btn => {
const cooldown = parseInt(btn.dataset.cooldown || DEFAULT_COOLDOWN, 10)
// 恢复页面刷新后的倒计时
const stored = sessionStorage.getItem(COUNTDOWN_KEY)
if (stored) {
const remaining = Math.ceil((parseInt(stored, 10) - Date.now()) / 1000)
if (remaining > 0) {
startCountdown(btn, remaining)
}
}
btn.addEventListener('click', e => {
e.preventDefault()
if (btn.disabled) return
sendSmsCode(btn, cooldown)
})
})
}
function sendSmsCode(btn, cooldown) {
const phoneInput = document.querySelector(btn.dataset.aksrtSmsPhone || '[data-aksrt-phone-input]')
const phone = phoneInput ? phoneInput.value.trim() : ''
if (!/^1[3-9]\d{9}$/.test(phone)) {
showToast('请输入正确的手机号', 'warning')
if (phoneInput) phoneInput.focus()
return
}
// 人机验证联动
const captchaState = window.AKSRTCaptchaState
const scene = btn.dataset.aksrtSmsScene || 'register'
if (captchaState && captchaState.scenes && captchaState.scenes[scene]) {
captchaState.scenes[scene].verify(() => {
doSendSms(btn, phone, cooldown)
})
} else {
doSendSms(btn, phone, cooldown)
}
}
function doSendSms(btn, phone, cooldown) {
btn.disabled = true
const original = btn.textContent
btn.textContent = '发送中...'
// 收集人机验证字段
const form = btn.closest('form')
const captchaData = collectCaptchaFields(form)
const scene = btn.dataset.aksrtSmsScene || 'register'
fetch(REST_BASE + '/auth/send-sms-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, scene, ...captchaData }),
})
.then(r => r.json().then(data => ({ ok: r.ok, data })))
.then(({ ok, data }) => {
if (ok && data.success) {
showToast(data.message || '验证码已发送', 'success')
const seconds = (data.data && data.data.remaining_time) || cooldown
sessionStorage.setItem(COUNTDOWN_KEY, String(Date.now() + seconds * 1000))
startCountdown(btn, seconds)
} else {
const msg = (data.data && data.data.message) || data.message || '发送失败'
showToast(msg, 'error')
try {
const captchaState = window.AKSRTCaptchaState
if (captchaState && captchaState.scenes && captchaState.scenes[scene]) {
captchaState.scenes[scene].reset()
}
} catch (e) { /* ignore */ }
const remaining = data.data && data.data.remaining_time
if (remaining && remaining > 0) {
sessionStorage.setItem(COUNTDOWN_KEY, String(Date.now() + remaining * 1000))
startCountdown(btn, remaining)
} else {
btn.disabled = false
btn.textContent = original
}
}
})
.catch(() => {
showToast('网络错误,请重试', 'error')
btn.disabled = false
btn.textContent = original
})
}
function startCountdown(btn, seconds) {
let remaining = Math.max(1, Math.floor(seconds))
btn.disabled = true
btn.textContent = remaining + 's'
const timer = setInterval(() => {
remaining--
if (remaining <= 0) {
clearInterval(timer)
btn.disabled = false
btn.textContent = '获取验证码'
sessionStorage.removeItem(COUNTDOWN_KEY)
} else {
btn.textContent = remaining + 's'
}
}, 1000)
}
/* =================================================================
* 手机号注册/登录表单
* ================================================================= */
function initPhoneAuthTabs() {
document.querySelectorAll('[data-aksrt-auth-tab]').forEach(tab => {
tab.addEventListener('click', e => {
e.preventDefault()
const target = tab.dataset.aksrtAuthTab
switchAuthTab(target)
})
})
// 手机号注册表单
const phoneRegForm = document.getElementById('aksrt-phone-register-form')
if (phoneRegForm) {
phoneRegForm.addEventListener('submit', e => {
e.preventDefault()
handlePhoneRegister(phoneRegForm)
})
}
// 手机号登录表单
const phoneLoginForm = document.getElementById('aksrt-phone-login-form')
if (phoneLoginForm) {
phoneLoginForm.addEventListener('submit', e => {
e.preventDefault()
handlePhoneLogin(phoneLoginForm)
})
}
}
function switchAuthTab(tabName) {
document.querySelectorAll('[data-auth-panel]').forEach(panel => {
panel.style.display = panel.dataset.authPanel === tabName ? '' : 'none'
})
document.querySelectorAll('[data-aksrt-auth-tab]').forEach(tab => {
tab.classList.toggle('is-active', tab.dataset.aksrtAuthTab === tabName)
})
}
function handlePhoneRegister(form) {
const phone = form.querySelector('[name="phone"]')?.value?.trim()
const code = form.querySelector('[name="code"]')?.value?.trim()
const password = form.querySelector('[name="password"]')?.value
const confirm = form.querySelector('[name="password_confirm"]')?.value
const inviteCode = form.querySelector('[name="invite_code"]')?.value?.trim()
if (!phone || !code || !password) {
showToast('请填写完整信息', 'warning')
return
}
if (password !== confirm) {
showToast('两次密码不一致', 'warning')
return
}
if (password.length < 6) {
showToast('密码至少 6 位', 'warning')
return
}
const submitBtn = form.querySelector('[type="submit"]')
if (submitBtn) {
submitBtn.disabled = true
submitBtn.textContent = '注册中...'
}
const captchaData = collectCaptchaFields(form)
fetch(REST_BASE + '/auth/phone-register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code, password, invite_code: inviteCode, ...captchaData }),
})
.then(r => r.json().then(data => ({ ok: r.ok, data })))
.then(({ ok, data }) => {
if (ok && data.success) {
showToast(data.message || '注册成功', 'success')
setTimeout(() => {
window.location.href = data.data?.redirect || '/'
}, 500)
} else {
showToast(data.message || '注册失败', 'error')
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = '注册'
}
}
})
.catch(() => {
showToast('网络错误', 'error')
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = '注册'
}
})
}
function handlePhoneLogin(form) {
const phone = form.querySelector('[name="phone"]')?.value?.trim()
const password = form.querySelector('[name="password"]')?.value
const loginMode = form.querySelector('[name="login_mode"]')?.value || 'password'
if (!phone) {
showToast('请输入手机号', 'warning')
return
}
const submitBtn = form.querySelector('[type="submit"]')
if (submitBtn) {
submitBtn.disabled = true
submitBtn.textContent = '登录中...'
}
let endpoint, body
if (loginMode === 'code') {
const code = form.querySelector('[name="code"]')?.value?.trim()
if (!code) {
showToast('请输入验证码', 'warning')
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '登录' }
return
}
endpoint = '/auth/phone-code-login'
body = { phone, code }
} else {
if (!password) {
showToast('请输入密码', 'warning')
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '登录' }
return
}
endpoint = '/auth/phone-login'
body = { phone, password }
}
const captchaState = window.AKSRTCaptchaState
const doLogin = () => {
const captchaData = collectCaptchaFields(form)
fetch(REST_BASE + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, ...captchaData }),
})
.then(r => r.json().then(data => ({ ok: r.ok, data })))
.then(({ ok, data }) => {
if (ok && data.success) {
showToast('登录成功', 'success')
setTimeout(() => {
window.location.href = data.data?.redirect || '/'
}, 500)
} else {
showToast(data.message || '登录失败', 'error')
if (captchaState && captchaState.scenes && captchaState.scenes.login) {
captchaState.scenes.login.reset()
}
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = '登录'
}
}
})
.catch(() => {
showToast('网络错误', 'error')
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = '登录'
}
})
}
if (captchaState && captchaState.scenes && captchaState.scenes.login) {
captchaState.scenes.login.verify(doLogin)
} else {
doLogin()
}
}
/* =================================================================
* 辅助功能
* ================================================================= */
function collectCaptchaFields(form) {
if (!form) return {}
const fields = {}
const names = [
'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'
]
names.forEach(name => {
const el = form.querySelector('[name="' + name + '"]')
if (el && el.value) fields[name] = el.value
})
return fields
}
function initPasswordToggles() {
document.querySelectorAll('[data-aksrt-password-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.closest('.input-wrap')?.querySelector('input')
if (!input) return
const isPassword = input.type === 'password'
input.type = isPassword ? 'text' : 'password'
const icon = btn.querySelector('i, svg')
if (icon) {
if (icon.tagName === 'I') {
icon.className = isPassword ? 'fas fa-eye-slash' : 'fas fa-eye'
}
}
})
})
}
function initAgreementChecks() {
document.querySelectorAll('form[data-aksrt-auth-form]').forEach(form => {
const checkbox = form.querySelector('[data-aksrt-agreement]')
const submitBtn = form.querySelector('[type="submit"]')
if (checkbox && submitBtn) {
const update = () => { submitBtn.disabled = !checkbox.checked }
checkbox.addEventListener('change', update)
update()
}
})
}
function initFormValidation() {
document.querySelectorAll('form[data-aksrt-auth-form]').forEach(form => {
form.addEventListener('submit', e => {
const password = form.querySelector('[name="password"]')
const confirm = form.querySelector('[name="password_confirm"]')
if (password && confirm && password.value !== confirm.value) {
e.preventDefault()
showToast('两次密码不一致', 'warning')
confirm.focus()
return
}
})
})
}
function showToast(message, type) {
if (typeof window.aksrtShowToast === 'function') {
window.aksrtShowToast(message, type)
return
}
const toast = document.createElement('div')
toast.className = 'aksrt-auth-toast aksrt-auth-toast--' + (type || 'info')
toast.textContent = message
document.body.appendChild(toast)
setTimeout(() => toast.classList.add('aksrt-auth-toast--show'), 10)
setTimeout(() => {
toast.classList.remove('aksrt-auth-toast--show')
setTimeout(() => toast.remove(), 300)
}, 3000)
}
// 初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAuthPages)
} else {
initAuthPages()
}
// 暴露给外部
window.AKSRTSignRegister = {
init: initAuthPages,
sendSmsCode,
switchAuthTab,
showToast,
}
})()