fix(profile): align account dialogs and contracts
This commit is contained in:
@@ -624,6 +624,8 @@
|
||||
background: var(--main-bg-color, var(--surface-1, #fff));
|
||||
}
|
||||
|
||||
.aksrt-settings-modal [hidden] { display: none !important; }
|
||||
|
||||
.aksrt-settings-modal .modal-colorful-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -643,54 +645,50 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-settings-modal form {
|
||||
.aksrt-settings-modal > form {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
margin-top: 120px;
|
||||
padding: 18px 15px;
|
||||
}
|
||||
|
||||
.aksrt-settings-modal__fields { padding: 0 2px; }
|
||||
.aksrt-settings-modal .modal-field { position: relative; margin-bottom: 14px; }
|
||||
.aksrt-settings-modal .modal-field label { display: block; margin-bottom: 6px; color: var(--muted-2-color, #777); font-size: 12px; font-weight: 400; }
|
||||
.aksrt-settings-modal .modal-field input,
|
||||
.aksrt-settings-modal .readonly-display {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 38px 8px 11px;
|
||||
border: 1px solid var(--main-border-color, var(--color-line, rgba(50, 50, 50, .12)));
|
||||
border-radius: var(--mini-radius, 4px);
|
||||
background: var(--main-bg-color, var(--surface-1, #fff));
|
||||
color: var(--color-ink, #333);
|
||||
font-size: 13px;
|
||||
outline: 0;
|
||||
.aksrt-settings-modal__pane {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
margin-top: 120px;
|
||||
padding: 18px 15px;
|
||||
}
|
||||
.aksrt-settings-modal .readonly-display { padding-right: 11px; color: var(--muted-2-color, #777); word-break: break-all; }
|
||||
.aksrt-settings-modal .modal-field input:focus { border-color: var(--focus-color, var(--color-primary, #2997f7)); box-shadow: 0 0 0 2px var(--focus-color-opacity1, rgba(41, 151, 247, .12)); }
|
||||
|
||||
.aksrt-settings-modal__pane form,
|
||||
.aksrt-settings-modal .account-step form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.aksrt-settings-modal .account-step-list {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
margin-top: 120px;
|
||||
padding: 18px 15px 0;
|
||||
}
|
||||
|
||||
.aksrt-settings-modal .account-step-panels { position: relative; z-index: 0; }
|
||||
.aksrt-settings-modal .account-step { display: none; padding: 0 15px 18px; }
|
||||
.aksrt-settings-modal .account-step.is-active { display: block; }
|
||||
.aksrt-settings-modal .account-step > p { margin: 0 0 12px; }
|
||||
.aksrt-settings-modal .account-recovery-toggle { border: 0; background: transparent; cursor: pointer; }
|
||||
|
||||
.aksrt-settings-modal__fields { padding: 0 2px; }
|
||||
.aksrt-settings-modal .toggle-pwd { position: absolute; right: 7px; bottom: 4px; width: 30px; height: 30px; padding: 0; border: 0; background: transparent; color: var(--muted-2-color, #999); cursor: pointer; }
|
||||
.aksrt-settings-modal .toggle-pwd:hover { color: var(--color-ink, #333); }
|
||||
.aksrt-settings-modal .code-input-group { display: flex; align-items: stretch; gap: 7px; }
|
||||
.aksrt-settings-modal .code-input-group input { min-width: 0; padding-right: 11px; letter-spacing: 2px; }
|
||||
.aksrt-settings-modal .code-send-btn { min-width: 112px; padding: 6px 10px; white-space: nowrap; }
|
||||
.aksrt-settings-modal .code-send-btn.is-counting { pointer-events: none; opacity: .6; }
|
||||
.aksrt-settings-modal .pwd-strength { height: 3px; margin-top: 6px; border-radius: 2px; background: var(--muted-border-color, #eee); overflow: hidden; }
|
||||
.aksrt-settings-modal .pwd-strength__fill { width: 0; height: 100%; border-radius: inherit; transition: width .2s ease, background-color .2s ease; }
|
||||
.aksrt-settings-modal .pwd-strength__fill.level-weak { width: 33%; background: var(--color-error, #ef4444); }
|
||||
.aksrt-settings-modal .pwd-strength__fill.level-medium { width: 66%; background: var(--color-warning, #f59e0b); }
|
||||
.aksrt-settings-modal .pwd-strength__fill.level-strong { width: 100%; background: var(--color-success, #10b981); }
|
||||
.aksrt-settings-modal .pwd-strength__text { display: block; min-height: 16px; margin-top: 3px; color: var(--muted-2-color, #999); font-size: 11px; }
|
||||
.aksrt-settings-modal .pwd-strength__text.level-weak { color: var(--color-error, #ef4444); }
|
||||
.aksrt-settings-modal .pwd-strength__text.level-medium { color: var(--color-warning, #f59e0b); }
|
||||
.aksrt-settings-modal .pwd-strength__text.level-strong { color: var(--color-success, #10b981); }
|
||||
.aksrt-settings-modal .aksrt-captcha-wrap { margin: 12px 0 8px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.aksrt-modal-runtime.aksrt-user-settings-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
|
||||
max-height: calc(100dvh - env(safe-area-inset-bottom));
|
||||
}
|
||||
.aksrt-settings-modal .code-input-group { gap: 6px; }
|
||||
.aksrt-settings-modal .code-send-btn { min-width: 104px; }
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
var submitLabel = button ? button.innerHTML : '';
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<i class="fa fa-spinner fa-spin mr6" aria-hidden="true"></i>正在提交…';
|
||||
button.innerHTML = '<i class="fa fas fa-spinner fa-spin mr6" aria-hidden="true"></i>正在提交…';
|
||||
}
|
||||
toast('正在提交申请…', 'load', '', 'aksrt_identity_submit');
|
||||
window.AksrtRequest.request(config.restBase, {
|
||||
|
||||
+323
-363
@@ -349,7 +349,7 @@
|
||||
}
|
||||
|
||||
/* ==================== 个人信息保存 ==================== */
|
||||
|
||||
|
||||
|
||||
function initProfileForm(root) {
|
||||
root = root || document;
|
||||
@@ -718,7 +718,6 @@
|
||||
var runtime = window.AksrtModalRuntime;
|
||||
var template = document.querySelector('template[data-aksrt-settings-modal-template="' + kind + '"]');
|
||||
if (!runtime || typeof runtime.open !== 'function' || !template) return;
|
||||
var title = kind === 'password' ? '修改密码' : (kind === 'email' ? '更改邮箱' : '绑定手机');
|
||||
runtime.open({
|
||||
html: template.innerHTML,
|
||||
kind: 'account-settings',
|
||||
@@ -727,252 +726,349 @@
|
||||
onContent: function (_, session) {
|
||||
var shell = session && session.shell;
|
||||
if (!shell) return;
|
||||
shell.setAttribute('aria-label', title);
|
||||
var dialog = shell.querySelector('[data-account-dialog]');
|
||||
var title = dialog && dialog.querySelector('[data-account-password-title], .modal-colorful-header .text-center > div:last-child');
|
||||
if (title && title.textContent.trim()) shell.setAttribute('aria-label', title.textContent.trim());
|
||||
var firstInput = shell.querySelector('input:not([type="hidden"]), textarea, select');
|
||||
if (firstInput) firstInput.focus();
|
||||
bindModalFields(shell);
|
||||
initBindPhone(shell);
|
||||
bindDialog(shell);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function bindModalFields(scope) {
|
||||
if (!scope) return;
|
||||
scope.querySelectorAll('.toggle-pwd').forEach(function(btn) {
|
||||
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
btn.dataset.aksrtSettingsInitialized = 'true';
|
||||
btn.addEventListener('click', function() {
|
||||
var inp = scope.querySelector('#' + this.getAttribute('data-target'));
|
||||
if (!inp) return;
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
this.classList.toggle('is-visible');
|
||||
var icon = this.querySelector('i');
|
||||
if (icon) {
|
||||
icon.classList.toggle('fa-eye', inp.type === 'password');
|
||||
icon.classList.toggle('fa-eye-slash', inp.type === 'text');
|
||||
}
|
||||
function setStep(dialog, key) {
|
||||
dialog.querySelectorAll('[data-account-step]').forEach(function (panel) {
|
||||
panel.classList.toggle('is-active', panel.getAttribute('data-account-step') === key);
|
||||
});
|
||||
var panels = Array.prototype.slice.call(dialog.querySelectorAll('[data-account-step]'));
|
||||
var index = Math.max(0, panels.findIndex(function (panel) { return panel.getAttribute('data-account-step') === key; }));
|
||||
dialog.querySelectorAll('.account-step-list li').forEach(function (item, itemIndex) {
|
||||
item.classList.toggle('active', itemIndex === index);
|
||||
item.classList.toggle('done', itemIndex < index);
|
||||
});
|
||||
}
|
||||
|
||||
function resetCountdown(button) {
|
||||
if (!button) return;
|
||||
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
|
||||
button.__aksrtCountdownTimer = null;
|
||||
button.disabled = false;
|
||||
button.classList.remove('is-counting');
|
||||
button.textContent = button.getAttribute('data-idle-label') || '发送验证码';
|
||||
}
|
||||
|
||||
function startCountdown(button, seconds) {
|
||||
var idleText = button.textContent;
|
||||
var left = Number(seconds || 60);
|
||||
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
|
||||
button.disabled = true;
|
||||
button.classList.add('is-counting');
|
||||
button.setAttribute('data-idle-label', idleText);
|
||||
button.textContent = left + 's';
|
||||
button.__aksrtCountdownTimer = window.setInterval(function () {
|
||||
left -= 1;
|
||||
if (left <= 0) { resetCountdown(button); return; }
|
||||
button.textContent = left + 's';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function settingsRestRoot() {
|
||||
return String((window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || cfg.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function settingsRestNonce() {
|
||||
return (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || cfg.restNonce || '';
|
||||
}
|
||||
|
||||
function formHasRequiredValues(form) {
|
||||
var required = form.querySelectorAll('[required]');
|
||||
for (var i = 0; i < required.length; i++) {
|
||||
if (required[i].type === 'checkbox' && !required[i].checked) {
|
||||
required[i].focus();
|
||||
notyf(i18n.agreeTerms || '请先同意相关协议', 'warning');
|
||||
return false;
|
||||
}
|
||||
if (required[i].type !== 'checkbox' && !String(required[i].value || '').trim()) {
|
||||
required[i].focus();
|
||||
notyf(i18n.fillRequired || '请填写必填项', 'warning');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendEmailCode(button, form, scene) {
|
||||
if (!form || button.disabled) return;
|
||||
if (scene === 'change_email' && !String((form.querySelector('[name="new_email"]') || {}).value || '').trim()) {
|
||||
notyf('请输入邮箱', 'warning');
|
||||
return;
|
||||
}
|
||||
runCaptcha('change_email', form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) return null;
|
||||
var data = new URLSearchParams();
|
||||
data.set('action', 'aksrt_send_account_email_code');
|
||||
data.set('_settings_nonce', settingsNonce);
|
||||
data.set('scene', scene);
|
||||
new FormData(form).forEach(function (value, key) { if (value !== '' && !data.has(key)) data.set(key, value); });
|
||||
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
|
||||
button.disabled = true;
|
||||
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() });
|
||||
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
|
||||
if (!payload) return;
|
||||
if (!payload.success) throw new Error((payload.data && payload.data.message) || '发送失败');
|
||||
notyf((payload.data && payload.data.message) || '验证码已发送', 'success');
|
||||
startCountdown(button, payload.data && payload.data.countdown);
|
||||
}).catch(function (error) {
|
||||
resetCountdown(button);
|
||||
notyf(error.message || '发送失败', 'danger');
|
||||
}).finally(function () { resetCaptcha('change_email', form); });
|
||||
}
|
||||
|
||||
function sendPhoneCode(button, form, stage) {
|
||||
if (!form || button.disabled) return;
|
||||
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
|
||||
if (!phone) { notyf('请输入手机号', 'warning'); return; }
|
||||
runCaptcha('bind_phone', form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) return null;
|
||||
var restRoot = settingsRestRoot();
|
||||
var nonce = settingsRestNonce();
|
||||
var request = fetch(restRoot + '/user/phone-code', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
|
||||
body: JSON.stringify(Object.assign({ phone: phone, stage: stage }, captchaPayload || {}))
|
||||
});
|
||||
button.disabled = true;
|
||||
return request;
|
||||
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
|
||||
if (!result) return;
|
||||
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
|
||||
notyf(result.payload.message || '验证码已发送', 'success');
|
||||
startCountdown(button, result.payload.data && result.payload.data.countdown);
|
||||
}).catch(function (error) {
|
||||
resetCountdown(button);
|
||||
notyf(error.message || '发送失败', 'danger');
|
||||
}).finally(function () { resetCaptcha('bind_phone', form); });
|
||||
}
|
||||
|
||||
function postAjaxForm(form, action, scene, success) {
|
||||
if (!formHasRequiredValues(form)) return;
|
||||
var button = form.querySelector('[type="submit"]');
|
||||
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
|
||||
var original = label ? label.textContent : '';
|
||||
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
|
||||
runCaptcha(scene, form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
return null;
|
||||
}
|
||||
var data = new FormData(form);
|
||||
data.set('action', action);
|
||||
data.set('_settings_nonce', settingsNonce);
|
||||
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
|
||||
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: data });
|
||||
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
|
||||
if (!payload) return;
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
if (!payload.success) throw new Error((payload.data && payload.data.message) || '操作失败');
|
||||
notyf((payload.data && payload.data.message) || '操作成功', 'success');
|
||||
if (typeof success === 'function') success(payload);
|
||||
}).catch(function (error) {
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
notyf(error.message || '操作失败', 'danger');
|
||||
}).finally(function () { resetCaptcha(scene, form); });
|
||||
}
|
||||
|
||||
function postPhoneForm(form, endpoint, success) {
|
||||
if (!formHasRequiredValues(form)) return;
|
||||
var button = form.querySelector('[type="submit"]');
|
||||
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
|
||||
var original = label ? label.textContent : '';
|
||||
runCaptcha('bind_phone', form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) return null;
|
||||
var restRoot = settingsRestRoot();
|
||||
var nonce = settingsRestNonce();
|
||||
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
|
||||
var payload = {};
|
||||
new FormData(form).forEach(function (value, key) { payload[key] = value; });
|
||||
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
|
||||
return fetch(restRoot + endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, body: JSON.stringify(payload) });
|
||||
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
|
||||
if (!result) return;
|
||||
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '操作失败');
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
notyf(result.payload.message || '操作成功', 'success');
|
||||
if (typeof success === 'function') success(result.payload);
|
||||
}).catch(function (error) {
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
notyf(error.message || '操作失败', 'danger');
|
||||
}).finally(function () { resetCaptcha('bind_phone', form); });
|
||||
}
|
||||
|
||||
function sendRecoveryCode(button, form) {
|
||||
if (!form || button.disabled) return;
|
||||
var target = String((form.querySelector('[name="target"]') || {}).value || '').trim();
|
||||
if (!target) { notyf('请输入邮箱或手机号', 'warning'); return; }
|
||||
var dialog = form.closest('[data-account-dialog="password"]');
|
||||
button.disabled = true;
|
||||
runCaptcha('lostpassword', form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) { notyf('请先完成人机验证', 'warning'); resetCountdown(button); return null; }
|
||||
var payload = { scene: 'resetpassword', target: target, channel: dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '' };
|
||||
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
|
||||
return fetch(settingsRestRoot() + '/auth/code', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
|
||||
if (!result) return;
|
||||
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
|
||||
notyf((result.payload.data && result.payload.data.message) || result.payload.message || '验证码已发送', 'success');
|
||||
startCountdown(button, (result.payload.data && result.payload.data.countdown) || result.payload.countdown);
|
||||
}).catch(function (error) {
|
||||
resetCountdown(button);
|
||||
notyf(error.message || '发送失败', 'danger');
|
||||
}).finally(function () { resetCaptcha('lostpassword', form); });
|
||||
}
|
||||
|
||||
function postRecoveryForm(form) {
|
||||
if (!formHasRequiredValues(form)) return;
|
||||
var password = form.querySelector('[name="password"]');
|
||||
var confirm = form.querySelector('[name="password_confirm"]');
|
||||
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
|
||||
var button = form.querySelector('[type="submit"]');
|
||||
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
|
||||
var original = label ? label.textContent : '';
|
||||
var dialog = form.closest('[data-account-dialog="password"]');
|
||||
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
|
||||
runCaptcha('lostpassword', form).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
return null;
|
||||
}
|
||||
var payload = {};
|
||||
new FormData(form).forEach(function (value, key) { payload[key] = value; });
|
||||
payload.channel = dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '';
|
||||
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
|
||||
return fetch(settingsRestRoot() + '/auth/reset-password', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
|
||||
if (!result) return;
|
||||
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '重设密码失败');
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
notyf(result.payload.message || (result.payload.data && result.payload.data.message) || '密码已重置', 'success');
|
||||
window.setTimeout(function () {
|
||||
var redirect = result.payload.redirect || (result.payload.data && result.payload.data.redirect);
|
||||
if (redirect) window.location.assign(redirect);
|
||||
else window.location.reload();
|
||||
}, 700);
|
||||
}).catch(function (error) {
|
||||
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
|
||||
notyf(error.message || '重设密码失败', 'danger');
|
||||
}).finally(function () { resetCaptcha('lostpassword', form); });
|
||||
}
|
||||
|
||||
function bindDialog(shell) {
|
||||
var dialog = shell.querySelector('[data-account-dialog]');
|
||||
if (!dialog || dialog.dataset.aksrtBound === 'true') return;
|
||||
dialog.dataset.aksrtBound = 'true';
|
||||
dialog.querySelectorAll('.toggle-pwd').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var input = dialog.querySelector('#' + button.getAttribute('data-target'));
|
||||
if (!input) return;
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
var icon = button.querySelector('i');
|
||||
if (icon) { icon.classList.toggle('fa-eye', input.type === 'password'); icon.classList.toggle('fa-eye-slash', input.type === 'text'); }
|
||||
});
|
||||
});
|
||||
|
||||
var pInput = scope.querySelector('#pwd-new');
|
||||
var sBar = scope.querySelector('#pwd-strength-bar');
|
||||
var sTxt = scope.querySelector('#pwd-strength-text');
|
||||
if (pInput && sBar && sTxt) {
|
||||
pInput.addEventListener('input', function() {
|
||||
var v = this.value;
|
||||
var fill = sBar.querySelector('.pwd-strength__fill');
|
||||
if (!v) { fill.className = 'pwd-strength__fill'; sTxt.className = 'pwd-strength__text'; sTxt.textContent = ''; return; }
|
||||
var score = 0;
|
||||
if (v.length >= 6) score++;
|
||||
if (v.length >= 10) score++;
|
||||
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) score++;
|
||||
if (/\d/.test(v)) score++;
|
||||
if (/[^a-zA-Z0-9]/.test(v)) score++;
|
||||
var level = score <= 1 ? 'weak' : (score <= 3 ? 'medium' : 'strong');
|
||||
var label = score <= 1 ? '弱' : (score <= 3 ? '中' : '强');
|
||||
fill.className = 'pwd-strength__fill level-' + level;
|
||||
sTxt.className = 'pwd-strength__text level-' + level;
|
||||
sTxt.textContent = '密码强度: ' + label;
|
||||
var type = dialog.getAttribute('data-account-dialog');
|
||||
if (type === 'password') {
|
||||
var recoveryTarget = dialog.querySelector('#pwd-recovery-target');
|
||||
if (recoveryTarget && dialog.dataset.recoveryTarget) recoveryTarget.value = dialog.dataset.recoveryTarget;
|
||||
dialog.querySelectorAll('[data-account-recovery-open],[data-account-recovery-back]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var showRecovery = button.hasAttribute('data-account-recovery-open');
|
||||
dialog.querySelectorAll('[data-account-password-pane]').forEach(function (pane) {
|
||||
pane.hidden = pane.getAttribute('data-account-password-pane') === 'recovery' ? !showRecovery : showRecovery;
|
||||
});
|
||||
var title = dialog.querySelector('[data-account-password-title]');
|
||||
if (title) {
|
||||
title.textContent = title.getAttribute(showRecovery ? 'data-recovery-label' : 'data-change-label') || title.textContent;
|
||||
var shell = dialog.closest('.aksrt-user-settings-modal');
|
||||
if (shell) shell.setAttribute('aria-label', title.textContent);
|
||||
}
|
||||
var input = dialog.querySelector(showRecovery ? '#pwd-recovery-target' : '#pwd-old');
|
||||
if (input) input.focus();
|
||||
});
|
||||
});
|
||||
var passwordForm = dialog.querySelector('#pwd-form');
|
||||
if (passwordForm) passwordForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var password = passwordForm.querySelector('[name="password"]');
|
||||
var confirm = passwordForm.querySelector('[name="password2"]');
|
||||
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
|
||||
postAjaxForm(passwordForm, 'aksrt_change_password', 'change_password', function () { window.setTimeout(function () { window.location.reload(); }, 700); });
|
||||
});
|
||||
var recoveryForm = dialog.querySelector('#pwd-recovery-form');
|
||||
var recoveryCodeButton = dialog.querySelector('[data-account-recovery-send]');
|
||||
if (recoveryCodeButton && recoveryForm) recoveryCodeButton.addEventListener('click', function () { sendRecoveryCode(recoveryCodeButton, recoveryForm); });
|
||||
if (recoveryForm) recoveryForm.addEventListener('submit', function (event) { event.preventDefault(); postRecoveryForm(recoveryForm); });
|
||||
} else if (type === 'email') {
|
||||
dialog.querySelectorAll('.account-code-send').forEach(function (button) {
|
||||
button.addEventListener('click', function () { sendEmailCode(button, button.closest('form'), button.getAttribute('data-account-code-scene')); });
|
||||
});
|
||||
var oldForm = dialog.querySelector('#email-old-verify-form');
|
||||
var newForm = dialog.querySelector('#email-new-form');
|
||||
if (oldForm) oldForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
postAjaxForm(oldForm, 'aksrt_verify_account_email', 'change_email', function () { setStep(dialog, 'email-new'); });
|
||||
});
|
||||
if (newForm) newForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
postAjaxForm(newForm, 'aksrt_change_email', 'change_email', function () {
|
||||
if (dialog.querySelector('[data-account-step="email-success"]')) setStep(dialog, 'email-success');
|
||||
window.setTimeout(function () { window.location.reload(); }, 700);
|
||||
});
|
||||
});
|
||||
} else if (type === 'phone') {
|
||||
dialog.querySelectorAll('[data-aksrt-sms-trigger]').forEach(function (button) {
|
||||
button.addEventListener('click', function () { sendPhoneCode(button, button.closest('form'), button.getAttribute('data-aksrt-sms-scene')); });
|
||||
});
|
||||
var phoneOldForm = dialog.querySelector('#phone-old-verify-form');
|
||||
var phoneNewForm = dialog.querySelector('#aksrt-bind-phone-form');
|
||||
if (phoneOldForm) phoneOldForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
postPhoneForm(phoneOldForm, '/user/phone-verify', function () { setStep(dialog, 'phone-new'); });
|
||||
});
|
||||
if (phoneNewForm) phoneNewForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
postPhoneForm(phoneNewForm, '/user/phone', function () {
|
||||
if (dialog.querySelector('[data-account-step="phone-success"]')) setStep(dialog, 'phone-success');
|
||||
window.setTimeout(function () { window.location.reload(); }, 700);
|
||||
});
|
||||
});
|
||||
}
|
||||
bindSend('pwd-send-code', 'pwd-form', 'change_password', scope);
|
||||
bindSend('email-send-code', 'email-form', 'change_email', scope);
|
||||
submitForm('pwd-form', 'aksrt_change_password', scope);
|
||||
submitForm('email-form', 'aksrt_change_email', scope);
|
||||
}
|
||||
|
||||
function resetSettingsModal(shell) {
|
||||
if (!shell || !shell.classList.contains('aksrt-user-settings-modal')) return;
|
||||
shell.dataset.aksrtSettingsModalClosed = 'true';
|
||||
var forms = shell.querySelectorAll('form');
|
||||
var sceneMap = { 'pwd-form': 'change_password', 'email-form': 'change_email', 'aksrt-bind-phone-form': 'bind_phone' };
|
||||
forms.forEach(function(form) {
|
||||
form.reset();
|
||||
var scene = sceneMap[form.id];
|
||||
if (scene) resetCaptcha(scene, form);
|
||||
form.querySelectorAll('[data-aksrt-countdown]').forEach(function(button) {
|
||||
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
|
||||
button.__aksrtCountdownTimer = null;
|
||||
button.disabled = false;
|
||||
button.classList.remove('is-counting');
|
||||
button.textContent = button.getAttribute('data-idle-label') || button.textContent;
|
||||
});
|
||||
});
|
||||
var bar = shell.querySelector('#pwd-strength-bar');
|
||||
var fill = bar && bar.querySelector('.pwd-strength__fill');
|
||||
var text = shell.querySelector('#pwd-strength-text');
|
||||
if (fill) fill.className = 'pwd-strength__fill';
|
||||
if (text) { text.textContent = ''; text.className = 'pwd-strength__text'; }
|
||||
shell.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger],[data-account-recovery-send]').forEach(resetCountdown);
|
||||
shell.querySelectorAll('form').forEach(function (form) { form.reset(); });
|
||||
}
|
||||
|
||||
if (document.documentElement.dataset.aksrtSettingsModalCleanupInitialized !== 'true') {
|
||||
document.documentElement.dataset.aksrtSettingsModalCleanupInitialized = 'true';
|
||||
document.addEventListener('aksrt:modal:close', function(event) {
|
||||
var shell = event.detail && event.detail.shell;
|
||||
resetSettingsModal(shell);
|
||||
});
|
||||
document.addEventListener('aksrt:modal:close', function (event) { resetSettingsModal(event.detail && event.detail.shell); });
|
||||
}
|
||||
|
||||
root.querySelectorAll('[data-aksrt-settings-modal]').forEach(function(btn) {
|
||||
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
btn.dataset.aksrtSettingsInitialized = 'true';
|
||||
btn.addEventListener('click', function() {
|
||||
openSettingsModal(this.getAttribute('data-aksrt-settings-modal'));
|
||||
});
|
||||
root.querySelectorAll('[data-aksrt-settings-modal]').forEach(function (button) {
|
||||
if (button.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
button.dataset.aksrtSettingsInitialized = 'true';
|
||||
button.addEventListener('click', function () { openSettingsModal(button.getAttribute('data-aksrt-settings-modal')); });
|
||||
});
|
||||
|
||||
// 验证码发送
|
||||
function getSendBtnIdleText(btnId) {
|
||||
if (btnId === 'pwd-send-code') return i18n.sendToCurrentEmail || '发送到当前邮箱';
|
||||
if (btnId === 'email-send-code') return i18n.sendToNewEmail || '发送到新邮箱';
|
||||
return i18n.sendCode || '发送验证码';
|
||||
}
|
||||
|
||||
function bindSend(btnId, formId, scene, scope) {
|
||||
var container = scope || document;
|
||||
var b = container.querySelector ? container.querySelector('#' + btnId) : document.getElementById(btnId);
|
||||
if (!b) return;
|
||||
if (b.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
b.dataset.aksrtSettingsInitialized = 'true';
|
||||
b.addEventListener('click', function() {
|
||||
if (this.classList.contains('is-counting')) return;
|
||||
var self = this;
|
||||
var form = container.querySelector ? container.querySelector('#' + formId) : document.getElementById(formId);
|
||||
var toastId = scene === 'change_password'
|
||||
? 'aksrt-settings-password-code-toast'
|
||||
: 'aksrt-settings-email-code-toast';
|
||||
runCaptcha(scene, form).then(function(captchaPayload) {
|
||||
if (captchaPayload === null) return;
|
||||
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return;
|
||||
self.disabled = true;
|
||||
self.textContent = i18n.sending || '发送中...';
|
||||
notyf(i18n.sending || '正在发送验证码…', 'load', '', toastId);
|
||||
var data = new URLSearchParams();
|
||||
data.set('action', 'aksrt_send_email_code');
|
||||
data.set('_settings_nonce', settingsNonce);
|
||||
data.set('scene', scene || '');
|
||||
if (form) {
|
||||
new FormData(form).forEach(function(value, key) {
|
||||
if (data.has(key) || value === '' || value == null) return;
|
||||
data.set(key, value);
|
||||
});
|
||||
}
|
||||
Object.keys(captchaPayload || {}).forEach(function(key) { data.set(key, captchaPayload[key]); });
|
||||
fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() })
|
||||
.then(function(r) { return r.json(); }).then(function(res) {
|
||||
if (res.success) {
|
||||
notyf((res.data && res.data.message) || i18n.codeSent || '验证码已发送', 'success', 5, toastId);
|
||||
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return;
|
||||
var sec = 60;
|
||||
self.classList.add('is-counting');
|
||||
self.textContent = sec + 's 后重发';
|
||||
self.setAttribute('data-aksrt-countdown', '');
|
||||
self.setAttribute('data-idle-label', getSendBtnIdleText(btnId));
|
||||
var t = setInterval(function() {
|
||||
sec--;
|
||||
if (sec <= 0) {
|
||||
clearInterval(t);
|
||||
self.__aksrtCountdownTimer = null;
|
||||
self.disabled = false;
|
||||
self.classList.remove('is-counting');
|
||||
self.textContent = getSendBtnIdleText(btnId);
|
||||
} else {
|
||||
self.textContent = sec + 's 后重发';
|
||||
}
|
||||
}, 1000);
|
||||
self.__aksrtCountdownTimer = t;
|
||||
} else {
|
||||
notyf((res.data && res.data.message) || i18n.sendFailed || '发送失败', 'danger', 5, toastId);
|
||||
self.disabled = false;
|
||||
self.textContent = getSendBtnIdleText(btnId);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
|
||||
self.disabled = false;
|
||||
self.textContent = getSendBtnIdleText(btnId);
|
||||
})
|
||||
.finally(function() {
|
||||
resetCaptcha(scene, form);
|
||||
});
|
||||
}).catch(function() {
|
||||
resetCaptcha(scene, form);
|
||||
notyf('人机验证失败,请重试', 'danger', 5, toastId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
function submitForm(fid, act, scope) {
|
||||
var container = scope || document;
|
||||
var fm = container.querySelector ? container.querySelector('#' + fid) : document.getElementById(fid);
|
||||
if (!fm) return;
|
||||
if (fm.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
fm.dataset.aksrtSettingsInitialized = 'true';
|
||||
fm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = fm.querySelector('[type=submit]');
|
||||
var submitLabel = btn && btn.querySelector('[data-aksrt-settings-submit-label]');
|
||||
var orig = submitLabel ? submitLabel.textContent : '';
|
||||
var toastId = fid === 'pwd-form'
|
||||
? 'aksrt-settings-password-toast'
|
||||
: 'aksrt-settings-email-toast';
|
||||
var reqs = fm.querySelectorAll('[required]');
|
||||
for (var i = 0; i < reqs.length; i++) {
|
||||
if (!reqs[i].value.trim()) { reqs[i].focus(); notyf(i18n.fillRequired || '请填写必填项', 'warning'); return; }
|
||||
}
|
||||
if (fid === 'pwd-form') {
|
||||
var pwdNew = fm.querySelector('#pwd-new');
|
||||
var pwdConfirm = fm.querySelector('#pwd-new-confirm');
|
||||
if (pwdNew && pwdConfirm && pwdNew.value !== pwdConfirm.value) {
|
||||
pwdConfirm.focus();
|
||||
notyf(i18n.passwordMismatch || '两次输入的新密码不一致', 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (btn) { btn.disabled = true; setSettingsSubmitLabel(btn, i18n.submitting || '提交中...'); }
|
||||
notyf(i18n.submitting || '正在提交…', 'load', '', toastId);
|
||||
var scene = fid === 'pwd-form' ? 'change_password' : 'change_email';
|
||||
runCaptcha(scene, fm).then(function(captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
|
||||
return null;
|
||||
}
|
||||
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return null;
|
||||
var modalShell = fm.closest('.aksrt-user-settings-modal');
|
||||
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
|
||||
var d = new FormData(fm);
|
||||
d.set('action', act);
|
||||
d.set('_settings_nonce', settingsNonce);
|
||||
Object.keys(captchaPayload || {}).forEach(function(key) { d.set(key, captchaPayload[key]); });
|
||||
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: d });
|
||||
})
|
||||
.then(function(r) { return r ? r.json() : null; })
|
||||
.then(function(r) {
|
||||
if (!r) return;
|
||||
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
|
||||
if (r.success) {
|
||||
notyf((r.data && r.data.message) || '操作成功', 'success', 5, toastId);
|
||||
var activeModal = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive();
|
||||
if (activeModal && activeModal.classList.contains('aksrt-user-settings-modal') && activeModal.contains(fm)) {
|
||||
window.AksrtModalRuntime.close('settings-submit');
|
||||
}
|
||||
if (r.data && r.data.redirect_to) setTimeout(function() { window.location.href = r.data.redirect_to; }, 2500);
|
||||
} else {
|
||||
notyf((r.data && r.data.message) || i18n.operationFailed || '操作失败', 'danger', 5, toastId);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
|
||||
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
|
||||
})
|
||||
.finally(function() {
|
||||
resetCaptcha(scene, fm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
|
||||
root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
|
||||
if (link.dataset.aksrtBindCaptchaInitialized === 'true') return;
|
||||
link.dataset.aksrtBindCaptchaInitialized = 'true';
|
||||
link.addEventListener('click', function(event) {
|
||||
@@ -1134,145 +1230,9 @@
|
||||
initNotifications();
|
||||
initNotificationPreferences();
|
||||
initSettings();
|
||||
initBindPhone();
|
||||
initSettingsCallbackToast();
|
||||
});
|
||||
|
||||
|
||||
// 绑定手机
|
||||
function initBindPhone(root) {
|
||||
root = root || document;
|
||||
var form = root.id === 'aksrt-bind-phone-form' ? root : root.querySelector('#aksrt-bind-phone-form');
|
||||
if (!form) return;
|
||||
if (form.dataset.aksrtSettingsInitialized === 'true') return;
|
||||
form.dataset.aksrtSettingsInitialized = 'true';
|
||||
function isValidSmsPhone(phone) {
|
||||
if (!window.aksrtData || aksrtData.smsNumberLimitCh !== false) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
}
|
||||
return /^(?:\d{5,}|\+\d{6,}|\+?\d{2,}[-\s]\d{2,}[0-9\-\s]{0,15})$/.test(phone);
|
||||
}
|
||||
var sendButton = form.querySelector('[data-aksrt-sms-trigger]');
|
||||
if (sendButton) sendButton.addEventListener('click', function () {
|
||||
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
|
||||
if (!isValidSmsPhone(phone)) { notyf('请输入正确的手机号', 'warning'); return; }
|
||||
var toastId = 'aksrt-settings-phone-code-toast';
|
||||
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
|
||||
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
|
||||
sendButton.disabled = true;
|
||||
notyf('正在发送验证码…', 'load', '', toastId);
|
||||
runCaptcha('bind_phone', form).then(function(captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
sendButton.disabled = false;
|
||||
return null;
|
||||
}
|
||||
var modalShell = form.closest('.aksrt-user-settings-modal');
|
||||
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
|
||||
return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone-code', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
|
||||
body: JSON.stringify(Object.assign({ phone: phone }, captchaPayload || {}))
|
||||
});
|
||||
}).then(function (r) { return r ? r.json().then(function (j) { return { ok: r.ok, json: j }; }) : null; }).then(function (res) {
|
||||
if (!res) return;
|
||||
if (!res.ok || !res.json.success) throw new Error(res.json.message || '发送失败');
|
||||
notyf(res.json.message || '验证码已发送', 'success', 5, toastId);
|
||||
var modalShell = form.closest('.aksrt-user-settings-modal');
|
||||
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return;
|
||||
var left = Number((res.json.data && res.json.data.countdown) || 60);
|
||||
sendButton.setAttribute('data-aksrt-countdown', '');
|
||||
sendButton.setAttribute('data-idle-label', '获取验证码');
|
||||
sendButton.classList.add('is-counting');
|
||||
var timer = setInterval(function () {
|
||||
left -= 1;
|
||||
sendButton.textContent = left + 's';
|
||||
if (left <= 0) {
|
||||
clearInterval(timer);
|
||||
sendButton.__aksrtCountdownTimer = null;
|
||||
sendButton.disabled = false;
|
||||
sendButton.classList.remove('is-counting');
|
||||
sendButton.textContent = sendButton.getAttribute('data-idle-label');
|
||||
}
|
||||
}, 1000);
|
||||
sendButton.__aksrtCountdownTimer = timer;
|
||||
}).catch(function (error) { sendButton.disabled = false; notyf(error.message || '发送失败', 'danger', 5, toastId); })
|
||||
.finally(function () { resetCaptcha('bind_phone', form); });
|
||||
});
|
||||
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 (!isValidSmsPhone(phone)) {
|
||||
notyf('请输入正确的手机号', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
notyf('请输入短信验证码', 'warning');
|
||||
return;
|
||||
}
|
||||
var submitBtn = form.querySelector('[type="submit"]');
|
||||
var submitLabel = submitBtn && submitBtn.querySelector('[data-aksrt-settings-submit-label]');
|
||||
var submitIdleText = submitLabel ? submitLabel.textContent : '';
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
setSettingsSubmitLabel(submitBtn, '绑定中...');
|
||||
}
|
||||
function doBind(captcha) {
|
||||
var toastId = 'aksrt-settings-phone-bind-toast';
|
||||
notyf('正在绑定手机…', 'load', '', toastId);
|
||||
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
|
||||
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
|
||||
return fetch(String(restRoot).replace(/\/$/, '') + '/user/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) {
|
||||
notyf(res.json.message || '绑定成功', 'success', 5, toastId);
|
||||
var activeModal = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive();
|
||||
if (activeModal && activeModal.classList.contains('aksrt-user-settings-modal') && activeModal.contains(form)) {
|
||||
window.AksrtModalRuntime.close('settings-submit');
|
||||
}
|
||||
setTimeout(function () { window.location.reload(); }, 600);
|
||||
return;
|
||||
}
|
||||
notyf((res.json && (res.json.message || res.json.error)) || '绑定失败', 'danger', 5, toastId);
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
setSettingsSubmitLabel(submitBtn, submitIdleText);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
notyf('网络错误', 'danger', 5, toastId);
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
setSettingsSubmitLabel(submitBtn, submitIdleText);
|
||||
}
|
||||
});
|
||||
}
|
||||
runCaptcha('bind_phone', form).then(function(captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
setSettingsSubmitLabel(submitBtn, submitIdleText);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var modalShell = form.closest('.aksrt-user-settings-modal');
|
||||
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
|
||||
return doBind(captchaPayload);
|
||||
}).finally(function() {
|
||||
resetCaptcha('bind_phone', form);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -81,7 +81,7 @@ $config = [
|
||||
</div>
|
||||
<div class="but-average modal-buts uc-auth-modal__footer">
|
||||
<button type="button" class="but" data-aksrt-modal-close><?php esc_html_e('取消', 'aksrt'); ?></button>
|
||||
<button type="submit" class="but c-blue"><i class="fa fa-check mr6" aria-hidden="true"></i><?php esc_html_e('确认提交', 'aksrt'); ?></button>
|
||||
<button type="submit" class="but c-blue"><i class="fa fas fa-check mr6" aria-hidden="true"></i><?php esc_html_e('确认提交', 'aksrt'); ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* User center account settings rows and runtime modal templates.
|
||||
* User center account settings rows and reference-theme account dialogs.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$account_user = isset($target_user) && $target_user instanceof WP_User
|
||||
? $target_user
|
||||
: wp_get_current_user();
|
||||
$account_user = isset($target_user) && $target_user instanceof WP_User ? $target_user : wp_get_current_user();
|
||||
$account_user_id = $account_user instanceof WP_User ? (int) $account_user->ID : (int) ($user_id ?? 0);
|
||||
$account_email = $account_user instanceof WP_User ? (string) $account_user->user_email : '';
|
||||
$account_phone = $account_user_id > 0 ? (string) get_user_meta($account_user_id, 'phone_number', true) : '';
|
||||
$user_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$sms_settings = class_exists('\\AKSRT\\Sms\\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
|
||||
$account_oauth_new = $account_user_id > 0 && get_user_meta($account_user_id, 'oauth_new', true);
|
||||
$user_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$sms_settings = class_exists('\AKSRT\Sms\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
|
||||
$phone_available = !empty($user_settings['bind_phone']) && (($sms_settings['sms_sdk'] ?? 'null') !== 'null');
|
||||
$phone_visible = $account_phone !== '' || $phone_available;
|
||||
$phone_label = $account_phone !== '' ? $account_phone : __('暂未绑定', 'aksrt');
|
||||
$email_action = $account_email !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt');
|
||||
$phone_action = $account_phone !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt');
|
||||
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false, 'label_html' => ''];
|
||||
$email_verify_required = $account_email !== '' && !empty($user_settings['email_set_captch']);
|
||||
$reset_channel = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::reset_password_verification_type() : 'email';
|
||||
$sign_in_available = !class_exists('\\AKSRT\\Settings\\Settings') || (!\AKSRT\Settings\Settings::close_sign() && !\AKSRT\Settings\Settings::close_signin());
|
||||
$can_recover_password = $sign_in_available && ($reset_channel === 'email_phone'
|
||||
? ($account_email !== '' || $account_phone !== '')
|
||||
: ($reset_channel === 'phone' ? $account_phone !== '' : $account_email !== ''));
|
||||
$recovery_target = $reset_channel === 'phone' ? $account_phone : ($account_email !== '' ? $account_email : $account_phone);
|
||||
$recovery_channel = $reset_channel === 'email_phone' ? '' : $reset_channel;
|
||||
|
||||
$render_row = static function (string $icon, string $title, string $value, string $action, string $modal, string $icon_tone, string $action_tone, bool $interactive = true): void {
|
||||
?>
|
||||
@@ -28,116 +32,122 @@ $render_row = static function (string $icon, string $title, string $value, strin
|
||||
<span class="aksrt-account-row__icon aksrt-account-row__icon--<?php echo esc_attr($icon_tone); ?>"><i class="fa fas <?php echo esc_attr($icon); ?>" aria-hidden="true"></i></span>
|
||||
<strong class="aksrt-account-row__copy"><?php echo esc_html($title); ?></strong>
|
||||
</div>
|
||||
<?php if ($value !== '') : ?>
|
||||
<div class="aksrt-account-row__value" title="<?php echo esc_attr($value); ?>"><?php echo esc_html($value); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($value !== '') : ?><div class="aksrt-account-row__value" title="<?php echo esc_attr($value); ?>"><?php echo esc_html($value); ?></div><?php endif; ?>
|
||||
<?php if ($interactive) : ?>
|
||||
<button type="button" class="but hollow <?php echo esc_attr($action_tone); ?> aksrt-account-row__action" data-aksrt-settings-modal="<?php echo esc_attr($modal); ?>">
|
||||
<?php echo esc_html($action); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<span class="aksrt-account-row__action aksrt-account-row__action--unavailable"><?php esc_html_e('暂不可修改', 'aksrt'); ?></span>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="but hollow <?php echo esc_attr($action_tone); ?> aksrt-account-row__action" data-aksrt-settings-modal="<?php echo esc_attr($modal); ?>"><?php echo esc_html($action); ?></button>
|
||||
<?php else : ?><span class="aksrt-account-row__action aksrt-account-row__action--unavailable"><?php esc_html_e('暂不可修改', 'aksrt'); ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
};
|
||||
|
||||
$render_email_form = static function () use ($user_settings, $agreement): void {
|
||||
?>
|
||||
<form id="email-new-form">
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="email" id="email-new" name="new_email" class="line-form-input" required><div class="scale-placeholder"><?php esc_html_e('请输入邮箱', 'aksrt'); ?></div><i class="line-form-line"></i></div>
|
||||
<?php if (!empty($user_settings['email_set_captch'])) : ?><div class="relative line-form mb10 modal-line-field"><input type="text" id="email-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-account-code-scene="change_email"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php endif; ?>
|
||||
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_email'); ?><?php endif; ?>
|
||||
<?php if (!empty($agreement['enabled'])) : ?><label class="form-checkbox muted-color mb20"><input type="checkbox" name="agree_terms" value="1"<?php echo !empty($agreement['required']) ? ' required' : ' checked'; ?>> <?php echo wp_kses_post($agreement['label_html']); ?></label><?php endif; ?>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <?php esc_html_e('确认提交', 'aksrt'); ?></button>
|
||||
</form>
|
||||
<?php
|
||||
};
|
||||
|
||||
$render_phone_form = static function () use ($agreement): void {
|
||||
?>
|
||||
<form id="aksrt-bind-phone-form">
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="tel" id="bind-phone-input" name="phone" inputmode="tel" autocomplete="tel" required><div class="scale-placeholder"><?php esc_html_e('手机号', 'aksrt'); ?></div><i class="line-form-line"></i></div>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="text" id="bind-phone-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn" data-aksrt-sms-trigger data-aksrt-sms-scene="bind_new_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div>
|
||||
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?>
|
||||
<?php if (!empty($agreement['enabled'])) : ?><label class="form-checkbox muted-color mb20"><input type="checkbox" name="agree_terms" value="1"<?php echo !empty($agreement['required']) ? ' required' : ' checked'; ?>> <?php echo wp_kses_post($agreement['label_html']); ?></label><?php endif; ?>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <?php esc_html_e('确认提交', 'aksrt'); ?></button>
|
||||
</form>
|
||||
<?php
|
||||
};
|
||||
?>
|
||||
<section class="aksrt-account-settings" data-aksrt-settings-root data-aksrt-captcha-scope>
|
||||
<div class="aksrt-account-settings__section">
|
||||
<div class="aksrt-account-settings__heading">
|
||||
<div>
|
||||
<h3 class="title-h-left"><b><?php esc_html_e('绑定邮箱', 'aksrt'); ?></b></h3>
|
||||
<p><?php esc_html_e('绑定邮箱账号,及时接收订单、审核等重要信息', 'aksrt'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-account-list">
|
||||
<?php $render_row('fa-envelope', __('邮箱', 'aksrt'), $account_email !== '' ? $account_email : __('暂未绑定', 'aksrt'), $email_action, 'email', 'blue', $account_email !== '' ? 'c-yellow-2' : 'c-blue'); ?>
|
||||
</div>
|
||||
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('绑定邮箱', 'aksrt'); ?></b></h3><p><?php esc_html_e('绑定邮箱账号,及时接收订单、审核等重要信息', 'aksrt'); ?></p></div></div>
|
||||
<div class="aksrt-account-list"><?php $render_row('fa-envelope', __('邮箱', 'aksrt'), $account_email !== '' ? $account_email : __('暂未绑定', 'aksrt'), $account_email !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt'), 'email', 'blue', $account_email !== '' ? 'c-yellow-2' : 'c-blue'); ?></div>
|
||||
</div>
|
||||
|
||||
<?php if ($phone_visible) : ?>
|
||||
<?php if ($account_phone !== '' || $phone_available) : ?>
|
||||
<div class="aksrt-account-settings__section">
|
||||
<div class="aksrt-account-settings__heading">
|
||||
<div>
|
||||
<h3 class="title-h-left"><b><?php esc_html_e('绑定手机', 'aksrt'); ?></b></h3>
|
||||
<p><?php esc_html_e('绑定手机号,提高账户安全性', 'aksrt'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-account-list">
|
||||
<?php $render_row('fa-phone', __('手机号', 'aksrt'), $phone_label, $phone_action, 'phone', 'blue-2', $account_phone !== '' ? 'c-yellow' : 'c-blue', $phone_available); ?>
|
||||
</div>
|
||||
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('绑定手机', 'aksrt'); ?></b></h3><p><?php esc_html_e('绑定手机号,提高账户安全性', 'aksrt'); ?></p></div></div>
|
||||
<div class="aksrt-account-list"><?php $render_row('fa-phone', __('手机号', 'aksrt'), $account_phone !== '' ? $account_phone : __('暂未绑定', 'aksrt'), $account_phone !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt'), 'phone', 'blue-2', $account_phone !== '' ? 'c-yellow' : 'c-blue', $phone_available); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require __DIR__ . '/settings-bindings.php'; ?>
|
||||
|
||||
<div class="aksrt-account-settings__section">
|
||||
<div class="aksrt-account-settings__heading">
|
||||
<div>
|
||||
<h3 class="title-h-left"><b><?php esc_html_e('账户密码', 'aksrt'); ?></b></h3>
|
||||
<p><?php esc_html_e('定期修改密码有助于账户安全', 'aksrt'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-account-list">
|
||||
<?php $render_row('fa-unlock-alt', __('账户密码', 'aksrt'), '', __('修改密码', 'aksrt'), 'password', 'purple', 'c-blue-2'); ?>
|
||||
</div>
|
||||
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('账户密码', 'aksrt'); ?></b></h3><p><?php echo $account_oauth_new ? esc_html__('您还未设置过密码,请在此设置新密码', 'aksrt') : esc_html__('定期修改密码有助于账户安全', 'aksrt'); ?></p></div></div>
|
||||
<div class="aksrt-account-list"><?php $render_row('fa-unlock-alt', __('账户密码', 'aksrt'), '', $account_oauth_new ? __('设置新密码', 'aksrt') : __('修改密码', 'aksrt'), 'password', 'purple', 'c-blue-2'); ?></div>
|
||||
</div>
|
||||
|
||||
<template data-aksrt-settings-modal-template="password">
|
||||
<div class="aksrt-settings-modal">
|
||||
<div class="modal-colorful-header colorful-bg jb-yellow">
|
||||
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
|
||||
<div class="colorful-make"></div>
|
||||
<div class="text-center"><div class="em2x"><i class="fa fas fa-unlock-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php esc_html_e('修改密码', 'aksrt'); ?></div></div>
|
||||
<div class="aksrt-settings-modal" data-account-dialog="password" data-recovery-channel="<?php echo esc_attr($recovery_channel); ?>" data-recovery-target="<?php echo esc_attr($recovery_target); ?>">
|
||||
<div class="modal-colorful-header colorful-bg jb-yellow"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-unlock-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10" data-account-password-title data-change-label="<?php echo esc_attr($account_oauth_new ? __('设置密码', 'aksrt') : __('修改密码', 'aksrt')); ?>" data-recovery-label="<?php esc_attr_e('重设密码', 'aksrt'); ?>"><?php echo $account_oauth_new ? esc_html__('设置密码', 'aksrt') : esc_html__('修改密码', 'aksrt'); ?></div></div></div>
|
||||
<div class="aksrt-settings-modal__pane" data-account-password-pane="change">
|
||||
<form id="pwd-form" autocomplete="off" novalidate>
|
||||
<div class="aksrt-settings-modal__fields">
|
||||
<?php if (!$account_oauth_new) : ?><div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-old" name="passwordold" class="line-form-input" tabindex="1" required><div class="scale-placeholder"><?php esc_html_e('请输入原密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-old" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div><?php endif; ?>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-new" name="password" class="line-form-input" tabindex="2" required minlength="6"><div class="scale-placeholder"><?php echo $account_oauth_new ? esc_html__('设置新密码', 'aksrt') : esc_html__('请输入新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-confirm" name="password2" class="line-form-input" tabindex="3" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('请再次输入新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
|
||||
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_password'); ?><?php endif; ?>
|
||||
</div>
|
||||
<?php if ($account_oauth_new) : ?><input type="hidden" name="oauth_new" value="1"><?php endif; ?>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
|
||||
</form>
|
||||
<?php if (!$account_oauth_new && $can_recover_password) : ?><div class="text-right mt6"><button type="button" class="em09 muted-2-color account-recovery-toggle" data-account-recovery-open><?php esc_html_e('忘记原密码?点击重设密码', 'aksrt'); ?></button></div><?php endif; ?>
|
||||
</div>
|
||||
<form id="pwd-form" autocomplete="off" novalidate>
|
||||
<div class="aksrt-settings-modal__fields">
|
||||
<div class="modal-field"><label for="pwd-old"><?php esc_html_e('原密码', 'aksrt'); ?></label><input type="password" id="pwd-old" name="old_password" required minlength="1"><button type="button" class="toggle-pwd" data-target="pwd-old" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
|
||||
<div class="modal-field"><label for="pwd-new"><?php esc_html_e('新密码', 'aksrt'); ?></label><input type="password" id="pwd-new" name="new_password" required minlength="6" placeholder="<?php esc_attr_e('至少6位字符', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="pwd-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button><div class="pwd-strength" id="pwd-strength-bar"><div class="pwd-strength__fill"></div></div><span class="pwd-strength__text" id="pwd-strength-text"></span></div>
|
||||
<div class="modal-field"><label for="pwd-new-confirm"><?php esc_html_e('确认新密码', 'aksrt'); ?></label><input type="password" id="pwd-new-confirm" name="new_password_confirm" required minlength="6" placeholder="<?php esc_attr_e('再次输入新密码', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="pwd-new-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
|
||||
<div class="modal-field modal-field--code"><label for="pwd-code"><?php esc_html_e('当前邮箱验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="pwd-code" name="email_code" maxlength="6" inputmode="numeric" pattern="[0-9]*" placeholder="<?php esc_attr_e('6位数字验证码', 'aksrt'); ?>" required><button type="button" class="but hollow code-send-btn" id="pwd-send-code"><?php esc_html_e('发送到当前邮箱', 'aksrt'); ?></button></div></div>
|
||||
<?php aksrt_render_captcha('change_password'); ?>
|
||||
</div>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block" id="pwd-submit"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
|
||||
</form>
|
||||
<?php if (!$account_oauth_new && $can_recover_password) : ?>
|
||||
<div class="aksrt-settings-modal__pane" data-account-password-pane="recovery" hidden>
|
||||
<div class="text-right mb10"><button type="button" class="em09 but p2-10 account-recovery-toggle" data-account-recovery-back><i class="fa fas fa-angle-left em12" aria-hidden="true"></i> <?php esc_html_e('返回使用原密码修改密码', 'aksrt'); ?></button></div>
|
||||
<form id="pwd-recovery-form" autocomplete="off" novalidate>
|
||||
<div class="aksrt-settings-modal__fields">
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="text" id="pwd-recovery-target" name="target" value="<?php echo esc_attr($recovery_target); ?>" class="line-form-input" autocomplete="username" required><div class="scale-placeholder"><?php esc_html_e('邮箱或手机号', 'aksrt'); ?></div><i class="line-form-line"></i></div>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="text" id="pwd-recovery-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn" data-account-recovery-send><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div>
|
||||
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('lostpassword'); ?><?php endif; ?>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-recovery-new" name="password" class="line-form-input" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('设置新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-recovery-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
|
||||
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-recovery-confirm" name="password_confirm" class="line-form-input" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('重复密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-recovery-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
|
||||
</div>
|
||||
<button type="submit" class="but jb-green padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-aksrt-settings-modal-template="email">
|
||||
<div class="aksrt-settings-modal">
|
||||
<div class="modal-colorful-header colorful-bg jb-blue">
|
||||
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
|
||||
<div class="colorful-make"></div>
|
||||
<div class="text-center"><div class="em2x"><i class="fa fas fa-envelope" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_email !== '' ? __('更改邮箱', 'aksrt') : __('绑定邮箱', 'aksrt')); ?></div></div>
|
||||
</div>
|
||||
<form id="email-form" autocomplete="off" novalidate>
|
||||
<div class="aksrt-settings-modal__fields">
|
||||
<?php if ($account_email !== '') : ?><div class="modal-field"><label><?php esc_html_e('当前邮箱', 'aksrt'); ?></label><div class="readonly-display"><?php echo esc_html($account_email); ?></div></div><?php endif; ?>
|
||||
<div class="modal-field"><label for="email-current-password"><?php esc_html_e('当前密码', 'aksrt'); ?></label><input type="password" id="email-current-password" name="current_password" required minlength="1" placeholder="<?php esc_attr_e('请输入当前登录密码', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="email-current-password" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
|
||||
<div class="modal-field"><label for="email-new"><?php esc_html_e('新邮箱', 'aksrt'); ?></label><input type="email" id="email-new" name="new_email" required placeholder="<?php esc_attr_e('请输入新的邮箱地址', 'aksrt'); ?>"></div>
|
||||
<div class="modal-field modal-field--code"><label for="email-code"><?php esc_html_e('新邮箱验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="email-code" name="email_code" maxlength="6" inputmode="numeric" pattern="[0-9]*" placeholder="<?php esc_attr_e('6位数字验证码', 'aksrt'); ?>" required><button type="button" class="but hollow code-send-btn" id="email-send-code"><?php esc_html_e('发送到新邮箱', 'aksrt'); ?></button></div><p class="settings-hint settings-hint--bottom"><?php esc_html_e('验证码将发送到你填写的新邮箱,验证通过后才会正式切换。', 'aksrt'); ?></p></div>
|
||||
<?php aksrt_render_captcha('change_email'); ?>
|
||||
<div class="aksrt-settings-modal" data-account-dialog="email">
|
||||
<div class="modal-colorful-header colorful-bg jb-blue"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-envelope" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_email !== '' ? __('修改邮箱', 'aksrt') : __('绑定邮箱', 'aksrt')); ?></div></div></div>
|
||||
<?php if ($email_verify_required) : ?>
|
||||
<ul class="mb20 step-simple account-step-list" data-account-step-list="email"><li class="active"><span><?php esc_html_e('验证老邮箱', 'aksrt'); ?></span></li><li><span><?php esc_html_e('验证新邮箱', 'aksrt'); ?></span></li><li><span><?php esc_html_e('修改成功', 'aksrt'); ?></span></li></ul>
|
||||
<div class="account-step-panels">
|
||||
<div class="account-step is-active" data-account-step="email-old"><p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老邮箱 %s', 'aksrt'), '<span class="badg">' . esc_html($account_email) . '</span>'); ?></p><form id="email-old-verify-form"><div class="relative line-form mb10 modal-line-field"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-account-code-scene="verify_email"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_email'); ?><?php endif; ?><button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button></form></div>
|
||||
<div class="account-step" data-account-step="email-new"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新邮箱账号', 'aksrt'); ?></p><?php $render_email_form(); ?></div>
|
||||
<div class="account-step" data-account-step="email-success"><div class="text-center padding-h10"><i class="fa fas fa-check-circle c-green em3x mb10" aria-hidden="true"></i><p><?php esc_html_e('邮箱修改成功', 'aksrt'); ?></p></div></div>
|
||||
</div>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block" id="email-submit"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
|
||||
</form>
|
||||
<?php else : ?>
|
||||
<div class="aksrt-settings-modal__pane"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新邮箱账号', 'aksrt'); ?></p><?php $render_email_form(); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<?php if ($phone_available) : ?>
|
||||
<template data-aksrt-settings-modal-template="phone">
|
||||
<div class="aksrt-settings-modal">
|
||||
<div class="modal-colorful-header colorful-bg jb-blue">
|
||||
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
|
||||
<div class="colorful-make"></div>
|
||||
<div class="text-center"><div class="em2x"><i class="fa fas fa-mobile-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_phone !== '' ? __('修改手机', 'aksrt') : __('绑定手机', 'aksrt')); ?></div></div>
|
||||
</div>
|
||||
<form id="aksrt-bind-phone-form" autocomplete="off" novalidate data-aksrt-bind-phone-form>
|
||||
<div class="aksrt-settings-modal__fields">
|
||||
<div class="modal-field"><label for="bind-phone-input"><?php esc_html_e('手机号', 'aksrt'); ?></label><input type="tel" id="bind-phone-input" name="phone" data-aksrt-phone-input inputmode="tel" autocomplete="tel" required placeholder="<?php esc_attr_e('请输入手机号', 'aksrt'); ?>"></div>
|
||||
<div class="modal-field modal-field--code"><label for="bind-phone-code"><?php esc_html_e('短信验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="bind-phone-code" name="code" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required placeholder="<?php esc_attr_e('请输入验证码', 'aksrt'); ?>"><button type="button" class="but hollow code-send-btn" data-aksrt-sms-trigger data-aksrt-sms-scene="bind_phone" data-aksrt-sms-phone="#bind-phone-input"><?php esc_html_e('获取验证码', 'aksrt'); ?></button></div></div>
|
||||
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?>
|
||||
<div class="aksrt-settings-modal" data-account-dialog="phone">
|
||||
<div class="modal-colorful-header colorful-bg jb-blue"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-phone" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_phone !== '' ? __('修改手机', 'aksrt') : __('绑定手机', 'aksrt')); ?></div></div></div>
|
||||
<?php if ($account_phone !== '') : ?>
|
||||
<ul class="mb20 step-simple account-step-list" data-account-step-list="phone"><li class="active"><span><?php esc_html_e('验证老手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('验证新手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('修改成功', 'aksrt'); ?></span></li></ul>
|
||||
<div class="account-step-panels">
|
||||
<div class="account-step is-active" data-account-step="phone-old"><p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老手机号 %s', 'aksrt'), '<br><span class="badg">' . esc_html($account_phone) . '</span>'); ?></p><form id="phone-old-verify-form"><input type="hidden" name="phone" value="<?php echo esc_attr($account_phone); ?>"><div class="relative line-form mb10 modal-line-field"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-aksrt-sms-trigger data-aksrt-sms-scene="verify_old_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?><button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button></form></div>
|
||||
<div class="account-step" data-account-step="phone-new"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新手机号码', 'aksrt'); ?></p><?php $render_phone_form(); ?></div>
|
||||
<div class="account-step" data-account-step="phone-success"><div class="text-center padding-h10"><i class="fa fas fa-check-circle c-green em3x mb10" aria-hidden="true"></i><p><?php esc_html_e('手机号修改成功', 'aksrt'); ?></p></div></div>
|
||||
</div>
|
||||
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
|
||||
</form>
|
||||
<?php else : ?>
|
||||
<div class="aksrt-settings-modal__pane"><p class="muted-2-color"><?php esc_html_e('请输入您需要绑定的手机号', 'aksrt'); ?></p><?php $render_phone_form(); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</template>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1,322 +1,365 @@
|
||||
<?php
|
||||
/**
|
||||
* User profile/password/email AJAX handlers
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
/** User profile and account settings AJAX handlers. */
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_account_captcha_error_data')) :
|
||||
function aksrt_account_captcha_error_data($result): array {
|
||||
if (is_wp_error($result)) {
|
||||
return [
|
||||
'code' => $result->get_error_code() ?: 'aksrt_captcha_failed',
|
||||
'message' => $result->get_error_message() ?: __('人机验证失败,请重试', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'code' => 'aksrt_captcha_failed',
|
||||
'message' => __('人机验证失败,请重试', 'aksrt'),
|
||||
];
|
||||
if (!function_exists('aksrt_account_captcha_error_data')) {
|
||||
function aksrt_account_captcha_error_data($result): array {
|
||||
return [
|
||||
'code' => is_wp_error($result) ? ($result->get_error_code() ?: 'aksrt_captcha_failed') : 'aksrt_captcha_failed',
|
||||
'message' => is_wp_error($result)
|
||||
? ($result->get_error_message() ?: __('人机验证失败,请重试', 'aksrt'))
|
||||
: __('人机验证失败,请重试', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_save_profile', function () {
|
||||
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
add_action('wp_ajax_aksrt_save_profile', function (): void {
|
||||
if (
|
||||
!isset($_POST['_profile_nonce'])
|
||||
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')
|
||||
) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$result = function_exists('aksrt_save_user_center_profile')
|
||||
? aksrt_save_user_center_profile($user_id, $_POST)
|
||||
: new WP_Error('aksrt_profile_unavailable', __('保存失败', 'aksrt'));
|
||||
$user_id = (int) ($_POST['user_id'] ?? 0);
|
||||
$result = function_exists('aksrt_save_user_center_profile')
|
||||
? aksrt_save_user_center_profile($user_id, $_POST)
|
||||
: new WP_Error('aksrt_profile_unavailable', __('保存失败', 'aksrt'));
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
|
||||
if (is_wp_error($result)) wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 发送邮箱验证码(修改密码/更改邮箱通用)
|
||||
// ============================================================
|
||||
|
||||
if (!function_exists('aksrt_get_email_code_key')) :
|
||||
function aksrt_get_email_code_key(int $user_id, string $scene): string {
|
||||
return 'aksrt_email_code_' . $scene . '_' . $user_id;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_email_code_rate_key')) :
|
||||
function aksrt_get_email_code_rate_key(int $user_id, string $scene, string $target = ''): string {
|
||||
$suffix = $target !== '' ? '_' . md5(strtolower($target)) : '';
|
||||
return 'aksrt_email_code_rate_' . $scene . '_' . $user_id . $suffix;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_store_email_code')) :
|
||||
function aksrt_store_email_code(int $user_id, string $scene, string $code, string $target = ''): void {
|
||||
set_transient(
|
||||
aksrt_get_email_code_key($user_id, $scene),
|
||||
[
|
||||
'hash' => wp_hash_password($code),
|
||||
'target' => $target,
|
||||
'issued_at' => time(),
|
||||
],
|
||||
5 * MINUTE_IN_SECONDS
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_consume_email_code')) :
|
||||
function aksrt_consume_email_code(int $user_id, string $scene, string $code, string $target = ''): bool {
|
||||
$key = aksrt_get_email_code_key($user_id, $scene);
|
||||
$stored = get_transient($key);
|
||||
if (!is_array($stored) || empty($stored['hash']) || !wp_check_password($code, $stored['hash'])) {
|
||||
return false;
|
||||
}
|
||||
if ($target !== '' && strtolower((string) ($stored['target'] ?? '')) !== strtolower($target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_transient($key);
|
||||
return true;
|
||||
function aksrt_account_code_key(int $user_id, string $scene, string $target): string {
|
||||
return 'aksrt_account_code_' . $user_id . '_' . sanitize_key($scene) . '_' . md5(strtolower(trim($target)));
|
||||
}
|
||||
|
||||
endif;
|
||||
function aksrt_account_store_code(int $user_id, string $scene, string $target, string $code): void {
|
||||
set_transient(
|
||||
aksrt_account_code_key($user_id, $scene, $target),
|
||||
['hash' => wp_hash_password($code), 'attempts' => 0],
|
||||
5 * MINUTE_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
add_action('wp_ajax_aksrt_send_email_code', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
function aksrt_account_consume_code(int $user_id, string $scene, string $target, string $code, bool $consume = true): bool {
|
||||
$key = aksrt_account_code_key($user_id, $scene, $target);
|
||||
$stored = get_transient($key);
|
||||
if (!is_array($stored) || empty($stored['hash'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
$attempts = (int) ($stored['attempts'] ?? 0) + 1;
|
||||
if ($attempts > 6) {
|
||||
delete_transient($key);
|
||||
return false;
|
||||
}
|
||||
|
||||
$scene = isset( $_POST['scene'] ) ? sanitize_key( (string) $_POST['scene'] ) : '';
|
||||
if ( ! in_array( $scene, array( 'change_password', 'change_email' ), true ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '无效的验证场景', 'aksrt' ) ) );
|
||||
}
|
||||
if (!wp_check_password($code, (string) $stored['hash'])) {
|
||||
$stored['attempts'] = $attempts;
|
||||
set_transient($key, $stored, 5 * MINUTE_IN_SECONDS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( $scene, wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
|
||||
}
|
||||
}
|
||||
if ($consume) {
|
||||
delete_transient($key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
$target_email = '';
|
||||
$success_message = '';
|
||||
function aksrt_account_proof_key(int $user_id, string $kind): string {
|
||||
return 'aksrt_account_proof_' . sanitize_key($kind) . '_' . $user_id;
|
||||
}
|
||||
|
||||
if ( $scene === 'change_password' ) {
|
||||
$target_email = $current_user->user_email;
|
||||
if ( empty( $target_email ) || ! is_email( $target_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前账号未绑定有效邮箱', 'aksrt' ) ) );
|
||||
}
|
||||
$success_message = __( '验证码已发送到您的注册邮箱', 'aksrt' );
|
||||
} else {
|
||||
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
|
||||
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
|
||||
function aksrt_account_set_proof(int $user_id, string $kind, string $target): void {
|
||||
set_transient(
|
||||
aksrt_account_proof_key($user_id, $kind),
|
||||
['target' => strtolower(trim($target))],
|
||||
30 * MINUTE_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '请输入有效的新邮箱地址', 'aksrt' ) ) );
|
||||
}
|
||||
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
|
||||
}
|
||||
if ( email_exists( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
|
||||
}
|
||||
function aksrt_account_consume_proof(int $user_id, string $kind, string $target, bool $consume = true): bool {
|
||||
$key = aksrt_account_proof_key($user_id, $kind);
|
||||
$proof = get_transient($key);
|
||||
if (!is_array($proof) || strtolower(trim((string) ($proof['target'] ?? ''))) !== strtolower(trim($target))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target_email = $new_email;
|
||||
$success_message = __( '验证码已发送到新邮箱,请完成验证后再提交', 'aksrt' );
|
||||
}
|
||||
if ($consume) {
|
||||
delete_transient($key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 频率限制:60秒内不能重复发送
|
||||
$rate_key = aksrt_get_email_code_rate_key( $current_user->ID, $scene, $target_email );
|
||||
if ( get_transient( $rate_key ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '操作过于频繁,请稍后再试', 'aksrt' ) ) );
|
||||
}
|
||||
set_transient( $rate_key, true, 60 );
|
||||
function aksrt_account_rate_limited(int $user_id, string $scene): bool {
|
||||
$key = 'aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id;
|
||||
$count = (int) get_transient($key);
|
||||
if ($count >= 8) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 生成验证码并同步发送,只有真正发信成功后才写入验证码缓存。
|
||||
$code = str_pad( (string) random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );
|
||||
set_transient($key, $count + 1, 10 * MINUTE_IN_SECONDS);
|
||||
return false;
|
||||
}
|
||||
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$sent = \AKSRT\Settings\Settings::send_event_email(
|
||||
'change_code',
|
||||
$target_email,
|
||||
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{code}' => $code ]
|
||||
);
|
||||
function aksrt_account_reset_rate(int $user_id, string $scene): void {
|
||||
delete_transient('aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id);
|
||||
}
|
||||
|
||||
if ( ! $sent ) {
|
||||
delete_transient( $rate_key );
|
||||
error_log( '[AKSRT] 验证码邮件发送失败 user_id=' . $current_user->ID . ' email=' . $target_email . ' scene=' . $scene );
|
||||
wp_send_json_error( array( 'message' => __( '验证码邮件发送失败,请稍后重试', 'aksrt' ) ) );
|
||||
}
|
||||
function aksrt_account_email_verification_required(): bool {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
return !empty($settings['email_set_captch']);
|
||||
}
|
||||
|
||||
aksrt_store_email_code( $current_user->ID, $scene, $code, $target_email );
|
||||
add_action('wp_ajax_aksrt_send_account_email_code', function (): void {
|
||||
if (
|
||||
!isset($_POST['_settings_nonce'])
|
||||
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
|
||||
) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_success( array( 'message' => $success_message ) );
|
||||
$user_id = get_current_user_id();
|
||||
$user = wp_get_current_user();
|
||||
if (!$user_id || !($user instanceof WP_User)) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$scene = sanitize_key((string) wp_unslash($_POST['scene'] ?? ''));
|
||||
if (!in_array($scene, ['verify_email', 'change_email'], true)) {
|
||||
wp_send_json_error(['message' => __('无效的验证场景', 'aksrt')]);
|
||||
}
|
||||
if (!aksrt_account_email_verification_required()) {
|
||||
wp_send_json_error(['message' => __('邮箱验证码验证未开启', 'aksrt')]);
|
||||
}
|
||||
|
||||
$target = $scene === 'verify_email'
|
||||
? (string) $user->user_email
|
||||
: sanitize_email((string) wp_unslash($_POST['new_email'] ?? ''));
|
||||
if (!is_email($target)) {
|
||||
wp_send_json_error(['message' => __('邮箱格式错误', 'aksrt')]);
|
||||
}
|
||||
if ($scene === 'change_email') {
|
||||
if (strtolower($target) === strtolower((string) $user->user_email)) {
|
||||
wp_send_json_error(['message' => __('修改邮箱不能与现邮箱相同', 'aksrt')]);
|
||||
}
|
||||
if (email_exists($target)) {
|
||||
wp_send_json_error(['message' => __('该邮箱已有绑定账号', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
|
||||
if ($captcha !== true) {
|
||||
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
|
||||
}
|
||||
}
|
||||
$rate_key = 'aksrt_account_email_rate_' . $user_id . '_' . $scene . '_' . md5(strtolower($target));
|
||||
if (get_transient($rate_key)) {
|
||||
wp_send_json_error(['message' => __('操作过于频繁,请稍后再试', 'aksrt')]);
|
||||
}
|
||||
set_transient($rate_key, true, 60);
|
||||
|
||||
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$sent = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::send_event_email('change_code', $target, [
|
||||
'{username}' => $user->display_name,
|
||||
'{site_name}' => get_bloginfo('name'),
|
||||
'{code}' => $code,
|
||||
])
|
||||
: false;
|
||||
if (!$sent) {
|
||||
delete_transient($rate_key);
|
||||
wp_send_json_error(['message' => __('验证码邮件发送失败,请稍后重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
aksrt_account_store_code($user_id, $scene, $target, $code);
|
||||
wp_send_json_success([
|
||||
'message' => $scene === 'verify_email'
|
||||
? __('验证码已发送到您的老邮箱', 'aksrt')
|
||||
: __('验证码已发送到新邮箱', 'aksrt'),
|
||||
'countdown' => 60,
|
||||
]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 修改密码
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_change_password', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
add_action('wp_ajax_aksrt_verify_account_email', function (): void {
|
||||
if (
|
||||
!isset($_POST['_settings_nonce'])
|
||||
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
|
||||
) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$user = wp_get_current_user();
|
||||
if (!$user_id || !($user instanceof WP_User) || !is_email((string) $user->user_email)) {
|
||||
wp_send_json_error(['message' => __('当前账号未绑定有效邮箱', 'aksrt')]);
|
||||
}
|
||||
if (!aksrt_account_email_verification_required()) {
|
||||
wp_send_json_error(['message' => __('邮箱验证码验证未开启', 'aksrt')]);
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
|
||||
if ($captcha !== true) {
|
||||
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
|
||||
}
|
||||
}
|
||||
|
||||
$old_password = isset( $_POST['old_password'] ) ? trim( wp_unslash( $_POST['old_password'] ) ) : '';
|
||||
$new_password = isset( $_POST['new_password'] ) ? trim( wp_unslash( $_POST['new_password'] ) ) : '';
|
||||
$new_password_confirm = isset( $_POST['new_password_confirm'] ) ? trim( wp_unslash( $_POST['new_password_confirm'] ) ) : '';
|
||||
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
|
||||
$code = sanitize_text_field((string) wp_unslash($_POST['code'] ?? ''));
|
||||
if (!aksrt_account_consume_code($user_id, 'verify_email', (string) $user->user_email, $code)) {
|
||||
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
|
||||
}
|
||||
|
||||
// 验证原密码
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! $user || ! wp_check_password( $old_password, $user->data->user_pass, $user_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '原密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 验证新密码强度
|
||||
if ( mb_strlen( $new_password ) < 6 ) {
|
||||
wp_send_json_error( array( 'message' => __( '新密码长度至少6位', 'aksrt' ) ) );
|
||||
}
|
||||
if ( mb_strlen( $new_password ) > 128 ) {
|
||||
wp_send_json_error( array( 'message' => __( '新密码过长', 'aksrt' ) ) );
|
||||
}
|
||||
if ( $new_password !== $new_password_confirm ) {
|
||||
wp_send_json_error( array( 'message' => __( '两次输入的新密码不一致', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 人机验证
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( 'change_password', wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if ( ! aksrt_consume_email_code( $user_id, 'change_password', $email_code, $user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 设置新密码
|
||||
wp_set_password( $new_password, $user_id );
|
||||
|
||||
if ( class_exists( 'WP_Session_Tokens' ) ) {
|
||||
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
|
||||
}
|
||||
|
||||
// 清除登录态
|
||||
wp_clear_auth_cookie();
|
||||
|
||||
wp_send_json_success( array(
|
||||
'message' => __( '密码修改成功,即将跳转到登录页…', 'aksrt' ),
|
||||
'redirect_to' => aksrt_get_login_url(),
|
||||
));
|
||||
aksrt_account_set_proof($user_id, 'email_old', (string) $user->user_email);
|
||||
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 更改邮箱
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_change_email', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
add_action('wp_ajax_aksrt_change_password', function (): void {
|
||||
if (
|
||||
!isset($_POST['_settings_nonce'])
|
||||
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
|
||||
) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
if (!$user_id) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
$user = get_userdata($user_id);
|
||||
if (!($user instanceof WP_User)) {
|
||||
wp_send_json_error(['message' => __('账号信息读取失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
|
||||
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
|
||||
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
|
||||
$oauth_new = (bool) get_user_meta($user_id, 'oauth_new', true);
|
||||
$password = trim((string) wp_unslash($_POST['password'] ?? ''));
|
||||
$password2 = trim((string) wp_unslash($_POST['password2'] ?? ''));
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('change_password', wp_unslash($_POST));
|
||||
if ($captcha !== true) {
|
||||
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
|
||||
}
|
||||
}
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
if (!$oauth_new) {
|
||||
$old_password = trim((string) wp_unslash($_POST['passwordold'] ?? ''));
|
||||
if ($old_password === '') {
|
||||
wp_send_json_error(['message' => __('请输入原密码', 'aksrt')]);
|
||||
}
|
||||
if ($old_password === $password) {
|
||||
wp_send_json_error(['message' => __('新密码和原密码不能相同', 'aksrt')]);
|
||||
}
|
||||
if (aksrt_account_rate_limited($user_id, 'change_password')) {
|
||||
wp_send_json_error(['message' => __('错误次数过多,请稍后再试', 'aksrt')]);
|
||||
}
|
||||
if (!wp_check_password($old_password, $user->user_pass, $user_id)) {
|
||||
wp_send_json_error(['message' => __('原密码错误', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证新邮箱格式
|
||||
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '请输入有效的邮箱地址', 'aksrt' ) ) );
|
||||
}
|
||||
$valid = class_exists('\AKSRT\Auth\Service')
|
||||
? \AKSRT\Auth\Service::validate_password($password, $password2)
|
||||
: __('认证服务不可用', 'aksrt');
|
||||
if ($valid !== true) {
|
||||
wp_send_json_error(['message' => (string) $valid]);
|
||||
}
|
||||
|
||||
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
|
||||
}
|
||||
$result = wp_update_user(['ID' => $user_id, 'user_pass' => $password]);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被使用
|
||||
if ( email_exists( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 人机验证
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( 'change_email', wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if ( ! aksrt_consume_email_code( $user_id, 'change_email', $email_code, $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 更新邮箱
|
||||
$old_email = $current_user->user_email;
|
||||
$update_result = wp_update_user( [
|
||||
'ID' => $user_id,
|
||||
'user_email' => $new_email,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $update_result ) ) {
|
||||
wp_send_json_error( array( 'message' => $update_result->get_error_message() ?: __( '邮箱更新失败,请稍后重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
if ( is_email( $old_email ) ) {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
\AKSRT\Settings\Settings::send_event_email(
|
||||
'change_notice',
|
||||
$old_email,
|
||||
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{new_email}' => $new_email ]
|
||||
);
|
||||
}
|
||||
|
||||
$verify_url = '';
|
||||
$verify_message = '';
|
||||
update_user_meta( $user_id, 'aksrt_email_verified', 1 );
|
||||
|
||||
if ( class_exists( 'WP_Session_Tokens' ) ) {
|
||||
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
|
||||
}
|
||||
|
||||
// 清除登录态
|
||||
wp_clear_auth_cookie();
|
||||
|
||||
wp_send_json_success( array(
|
||||
'message' => $verify_message !== ''
|
||||
? __( '邮箱修改成功,请前往新邮箱完成验证后重新登录…', 'aksrt' )
|
||||
: __( '邮箱修改成功,即将跳转到登录页…', 'aksrt' ),
|
||||
'redirect_to' => $verify_url !== '' ? $verify_url : aksrt_get_login_url(),
|
||||
'verify_url' => $verify_url,
|
||||
));
|
||||
delete_user_meta($user_id, 'oauth_new');
|
||||
aksrt_account_reset_rate($user_id, 'change_password');
|
||||
wp_send_json_success([
|
||||
'message' => $oauth_new
|
||||
? __('密码设置成功', 'aksrt')
|
||||
: __('修改成功,下次请使用新密码登录', 'aksrt'),
|
||||
'reload' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_change_email', function (): void {
|
||||
if (
|
||||
!isset($_POST['_settings_nonce'])
|
||||
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
|
||||
) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$user = wp_get_current_user();
|
||||
if (!$user_id || !($user instanceof WP_User)) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$new_email = sanitize_email((string) wp_unslash($_POST['new_email'] ?? ''));
|
||||
if (!is_email($new_email)) {
|
||||
wp_send_json_error(['message' => __('邮箱格式错误', 'aksrt')]);
|
||||
}
|
||||
if ($user->user_email !== '' && strtolower($new_email) === strtolower((string) $user->user_email)) {
|
||||
wp_send_json_error(['message' => __('修改邮箱不能与现邮箱相同', 'aksrt')]);
|
||||
}
|
||||
if (email_exists($new_email)) {
|
||||
wp_send_json_error(['message' => __('该邮箱已有绑定账号', 'aksrt')]);
|
||||
}
|
||||
|
||||
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false];
|
||||
if (!empty($agreement['required']) && empty($_POST['agree_terms'])) {
|
||||
wp_send_json_error(['message' => __('请先同意相关协议', 'aksrt')]);
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
|
||||
if ($captcha !== true) {
|
||||
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
|
||||
}
|
||||
}
|
||||
|
||||
$verification_required = aksrt_account_email_verification_required();
|
||||
$old_email = (string) $user->user_email;
|
||||
if (
|
||||
$old_email !== ''
|
||||
&& $verification_required
|
||||
&& !aksrt_account_consume_proof($user_id, 'email_old', $old_email, false)
|
||||
) {
|
||||
wp_send_json_error(['message' => __('请先验证老邮箱', 'aksrt')]);
|
||||
}
|
||||
if (
|
||||
$verification_required
|
||||
&& !aksrt_account_consume_code(
|
||||
$user_id,
|
||||
'change_email',
|
||||
$new_email,
|
||||
sanitize_text_field((string) wp_unslash($_POST['code'] ?? '')),
|
||||
false
|
||||
)
|
||||
) {
|
||||
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = wp_update_user(['ID' => $user_id, 'user_email' => $new_email]);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message() ?: __('邮箱更新失败,请稍后重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
if ($old_email !== '' && $verification_required) {
|
||||
delete_transient(aksrt_account_proof_key($user_id, 'email_old'));
|
||||
}
|
||||
if ($verification_required) {
|
||||
delete_transient(aksrt_account_code_key($user_id, 'change_email', $new_email));
|
||||
}
|
||||
update_user_meta($user_id, 'aksrt_email_verified', 1);
|
||||
if ($old_email !== '' && is_email($old_email) && class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::send_event_email('change_notice', $old_email, [
|
||||
'{username}' => $user->display_name,
|
||||
'{site_name}' => get_bloginfo('name'),
|
||||
'{new_email}' => $new_email,
|
||||
]);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => $old_email !== '' ? __('邮箱修改成功', 'aksrt') : __('邮箱绑定成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -205,6 +205,11 @@ class UserController extends ControllerBase {
|
||||
'callback' => [$this, 'user_phone_code'],
|
||||
'permission_callback' => static function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/phone-verify', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'user_verify_phone'],
|
||||
'permission_callback' => static function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/phone', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'user_bind_phone'],
|
||||
|
||||
@@ -80,7 +80,7 @@ trait UserControllerAuthPhoneOperations {
|
||||
], $ttl);
|
||||
}
|
||||
|
||||
private function auth_consume_code(string $scene, string $target, string $code): bool {
|
||||
private function auth_consume_code(string $scene, string $target, string $code, bool $consume = true): bool {
|
||||
$key = $this->auth_code_key($scene, $target);
|
||||
$stored = get_transient($key);
|
||||
if (!is_array($stored) || empty($stored['hash'])) {
|
||||
@@ -101,7 +101,9 @@ trait UserControllerAuthPhoneOperations {
|
||||
set_transient($key, $stored, $remaining);
|
||||
return false;
|
||||
}
|
||||
delete_transient($key);
|
||||
if ($consume) {
|
||||
delete_transient($key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -459,38 +461,85 @@ trait UserControllerAuthPhoneOperations {
|
||||
|
||||
public function user_phone_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
}
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
|
||||
$stage = sanitize_key((string) ($payload['stage'] ?? 'bind_new_phone'));
|
||||
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
|
||||
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) {
|
||||
return $this->error(__('手机号格式不正确', 'aksrt'));
|
||||
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt'));
|
||||
$user_id = get_current_user_id();
|
||||
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
|
||||
if ($stage === 'verify_old_phone') {
|
||||
if ($current_phone === '' || !hash_equals($current_phone, $phone)) return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409);
|
||||
} elseif ($stage === 'bind_new_phone') {
|
||||
if ($current_phone !== '' && !hash_equals($current_phone, $phone)) {
|
||||
$proof = get_transient('aksrt_account_phone_old_' . $user_id);
|
||||
if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403);
|
||||
}
|
||||
$users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']);
|
||||
if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409);
|
||||
if ($current_phone !== '' && hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409);
|
||||
} else {
|
||||
return $this->error(__('无效的验证步骤', 'aksrt'));
|
||||
}
|
||||
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
|
||||
if ($captcha_error) return $captcha_error;
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) return $this->error(__('短信服务不可用', 'aksrt'), 503);
|
||||
$rate_subject = $stage . '|' . $user_id . '|' . $phone;
|
||||
if ($this->auth_rate_limited('account-phone-code', $rate_subject, 5, HOUR_IN_SECONDS)) return $this->error(__('验证码请求过于频繁,请稍后再试', 'aksrt'), 429);
|
||||
$code = (string) random_int(100000, 999999);
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->send_code($phone, $code);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt')));
|
||||
}
|
||||
$this->auth_store_code('bind_phone', $phone, $code, 30 * MINUTE_IN_SECONDS);
|
||||
if (empty($result['success'])) return $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt')));
|
||||
$this->auth_store_code('account_' . $stage . '_' . $user_id, $phone, $code, 30 * MINUTE_IN_SECONDS);
|
||||
return $this->success(['message' => __('验证码短信已发送', 'aksrt'), 'countdown' => (int) ($result['remaining_time'] ?? 60)]);
|
||||
}
|
||||
|
||||
public function user_verify_phone(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
|
||||
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
|
||||
if ($captcha_error) return $captcha_error;
|
||||
$user_id = get_current_user_id();
|
||||
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
|
||||
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($payload['code'] ?? ''));
|
||||
if ($current_phone === '' || !hash_equals($current_phone, $phone)) return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409);
|
||||
if (!$this->auth_consume_code('account_verify_old_phone_' . $user_id, $current_phone, $code)) return $this->error(__('验证码错误或已过期', 'aksrt'));
|
||||
set_transient('aksrt_account_phone_old_' . $user_id, ['phone' => $current_phone], 30 * MINUTE_IN_SECONDS);
|
||||
return $this->success(['message' => __('手机号验证成功', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function user_bind_phone(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
|
||||
if ($captcha_error) return $captcha_error;
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
|
||||
$user_id = get_current_user_id();
|
||||
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
|
||||
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($payload['code'] ?? ''));
|
||||
if (!$this->auth_consume_code('bind_phone', $phone, $code)) {
|
||||
return $this->error(__('验证码错误或已过期', 'aksrt'));
|
||||
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt'));
|
||||
if ($current_phone !== '') {
|
||||
$proof = get_transient('aksrt_account_phone_old_' . $user_id);
|
||||
if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403);
|
||||
if (hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409);
|
||||
}
|
||||
$users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']);
|
||||
if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409);
|
||||
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false];
|
||||
if (!empty($agreement['required']) && empty($payload['agree_terms'])) return $this->error(__('请先同意相关协议', 'aksrt'), 400);
|
||||
$code_scene = 'account_bind_new_phone_' . $user_id;
|
||||
if (!$this->auth_consume_code($code_scene, $phone, $code, false)) return $this->error(__('验证码错误或已过期', 'aksrt'));
|
||||
if (!class_exists('\AKSRT\Auth\Service')) return $this->error(__('认证服务不可用', 'aksrt'), 500);
|
||||
$result = \AKSRT\Auth\Service::bind_phone($payload, true);
|
||||
return !empty($result['success']) ? $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]) : $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt')));
|
||||
if (empty($result['success'])) return $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt')));
|
||||
delete_transient($this->auth_code_key($code_scene, $phone));
|
||||
delete_transient('aksrt_account_phone_old_' . $user_id);
|
||||
return $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function test_sms_send(\WP_REST_Request $request): \WP_REST_Response {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
test('account dialogs expose configured email steps, phone steps, and reference password branches', () => {
|
||||
const template = read('inc/helpers/user-center-renderers/settings-account.php');
|
||||
const javascript = read('assets/js/routes/user-center.js');
|
||||
const css = read('assets/css/routes/user-center/settings.css');
|
||||
|
||||
assert.match(template, /\$email_verify_required = \$account_email !== '' && !empty\(\$user_settings\['email_set_captch'\]\)/);
|
||||
assert.match(template, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"[\s\S]*?data-account-step="email-success"/);
|
||||
assert.match(template, /if \(\$email_verify_required\)/);
|
||||
assert.match(template, /if \(\$email_verify_required\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
|
||||
assert.match(template, /if \(!empty\(\$user_settings\['email_set_captch'\]\)\)/);
|
||||
assert.match(template, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"[\s\S]*?data-account-step="phone-success"/);
|
||||
assert.match(template, /if \(\$account_phone !== ''\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
|
||||
assert.match(template, /data-aksrt-sms-scene="verify_old_phone"/);
|
||||
assert.match(template, /data-aksrt-sms-scene="bind_new_phone"/);
|
||||
assert.match(template, /if \(!\$account_oauth_new\)[\s\S]*?name="passwordold"/);
|
||||
assert.match(template, /if \(!\$account_oauth_new && \$can_recover_password\)/);
|
||||
assert.match(template, /data-account-password-pane="recovery"/);
|
||||
assert.match(javascript, /\/auth\/code/);
|
||||
assert.match(javascript, /\/auth\/reset-password/);
|
||||
assert.doesNotMatch(template + javascript + css, /pwd-strength|pwd-send-code|email-send-code/);
|
||||
|
||||
const emailConditionStart = template.indexOf('<?php if ($email_verify_required) : ?>');
|
||||
const emailElse = template.indexOf('<?php else : ?>', emailConditionStart);
|
||||
const emailEnd = template.indexOf('<?php endif; ?>', emailElse);
|
||||
const emailSteps = template.slice(emailConditionStart, emailElse);
|
||||
const emailDirect = template.slice(emailElse, emailEnd);
|
||||
const phoneConditionStart = template.indexOf('<?php if ($account_phone !== \'\') : ?>');
|
||||
const phoneElse = template.indexOf('<?php else : ?>', phoneConditionStart);
|
||||
const phoneEnd = template.indexOf('<?php endif; ?>', phoneElse);
|
||||
const phoneSteps = template.slice(phoneConditionStart, phoneElse);
|
||||
const phoneDirect = template.slice(phoneElse, phoneEnd);
|
||||
assert.match(emailSteps, /account-step-list/);
|
||||
assert.doesNotMatch(emailDirect, /account-step-list|data-account-step=/);
|
||||
assert.match(phoneSteps, /account-step-list/);
|
||||
assert.doesNotMatch(phoneDirect, /account-step-list|data-account-step=/);
|
||||
});
|
||||
|
||||
test('account PHP harness exercises email and password state transitions', () => {
|
||||
const harness = path.join(root, 'tests', 'widget-system', 'account-settings-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /Account settings harness passed/);
|
||||
});
|
||||
|
||||
test('email update keeps both proofs through validation failures and consumes them only after the write', () => {
|
||||
const ajax = read('inc/modules/user-parts/account-ajax.php');
|
||||
const send = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_send_account_email_code'"), ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"));
|
||||
const verify = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"));
|
||||
const change = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
|
||||
|
||||
assert.match(send, /\['verify_email', 'change_email'\]/);
|
||||
assert.match(send, /email_exists\(\$target\)/);
|
||||
assert.match(verify, /aksrt_require_captcha\(\s*'change_email'/);
|
||||
assert.match(verify, /aksrt_account_set_proof\(\$user_id, 'email_old'/);
|
||||
assert.match(change, /aksrt_account_email_verification_required\(\)/);
|
||||
assert.match(change, /aksrt_account_consume_proof\([^;]+false\)/);
|
||||
assert.match(change, /aksrt_account_consume_code\([\s\S]*?false\s*\)/);
|
||||
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_proof_key"));
|
||||
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_code_key"));
|
||||
assert.match(change, /\$agreement\['required'\]/);
|
||||
assert.doesNotMatch(ajax, /wp_ajax_aksrt_send_email_code/);
|
||||
});
|
||||
|
||||
test('phone replacement requires old-number proof and a new-number code while reusing bind side effects', () => {
|
||||
const controller = read('src/Admin/UserControllerAuthPhoneOperations.php');
|
||||
const routes = read('src/Admin/UserController.php');
|
||||
const binding = read('src/Auth/ServicePart1b.php');
|
||||
const code = controller.slice(controller.indexOf('public function user_phone_code('), controller.indexOf('public function user_verify_phone('));
|
||||
const verify = controller.slice(controller.indexOf('public function user_verify_phone('), controller.indexOf('public function user_bind_phone('));
|
||||
const bind = controller.slice(controller.indexOf('public function user_bind_phone('), controller.indexOf('public function test_sms_send('));
|
||||
|
||||
assert.match(routes, /\/user\/phone-verify[\s\S]*?user_verify_phone/);
|
||||
assert.match(code, /verify_old_phone/);
|
||||
assert.match(code, /bind_new_phone/);
|
||||
assert.match(verify, /auth_consume_code\('account_verify_old_phone_' \./);
|
||||
assert.match(verify, /aksrt_account_phone_old_/);
|
||||
assert.match(bind, /aksrt_account_phone_old_/);
|
||||
assert.match(bind, /auth_consume_code\(\$code_scene, \$phone, \$code, false\)/);
|
||||
assert.ok(bind.indexOf("Auth\\Service::bind_phone") < bind.indexOf("delete_transient($this->auth_code_key($code_scene, $phone))"));
|
||||
assert.match(bind, /aksrt_get_auth_agreement_view/);
|
||||
assert.match(bind, /Auth\\Service::bind_phone\(\$payload, true\)/);
|
||||
assert.match(binding, /aksrt_user_phone_changed/);
|
||||
assert.doesNotMatch(controller, /auth_consume_code\('bind_phone'/);
|
||||
});
|
||||
|
||||
test('password change retains old-password limits and captcha; recovery uses shared auth endpoints', () => {
|
||||
const ajax = read('inc/modules/user-parts/account-ajax.php');
|
||||
const javascript = read('assets/js/routes/user-center.js');
|
||||
const password = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
|
||||
|
||||
assert.match(password, /get_user_meta\(\$user_id, 'oauth_new', true\)/);
|
||||
assert.match(password, /wp_check_password\(\$old/);
|
||||
assert.match(password, /aksrt_account_rate_limited\(\$user_id, 'change_password'\)/);
|
||||
assert.match(password, /aksrt_require_captcha\('change_password'/);
|
||||
assert.match(password, /Auth\\Service::validate_password\(\$password, \$password2\)/);
|
||||
assert.doesNotMatch(password, /email.*code|password.*strength/i);
|
||||
assert.match(javascript, /scene: 'resetpassword', target: target, channel:/);
|
||||
assert.match(javascript, /\/auth\/code/);
|
||||
assert.match(javascript, /\/auth\/reset-password/);
|
||||
assert.match(javascript, /data-recovery-channel/);
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/** Account email and password transition contract harness. */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('MINUTE_IN_SECONDS', 60);
|
||||
$GLOBALS['aksrt_account_settings'] = ['email_set_captch' => true];
|
||||
$GLOBALS['aksrt_account_user_id'] = 7;
|
||||
$GLOBALS['aksrt_account_user'] = null;
|
||||
$GLOBALS['aksrt_account_meta'] = [];
|
||||
$GLOBALS['aksrt_account_transients'] = [];
|
||||
$GLOBALS['aksrt_account_actions'] = [];
|
||||
$GLOBALS['aksrt_account_mail'] = [];
|
||||
$GLOBALS['aksrt_account_existing_emails'] = [];
|
||||
$GLOBALS['aksrt_account_captcha_calls'] = [];
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = false;
|
||||
$GLOBALS['aksrt_account_update_fail'] = false;
|
||||
|
||||
final class WP_User {
|
||||
public function __construct(
|
||||
public int $ID,
|
||||
public string $user_email,
|
||||
public string $display_name,
|
||||
public string $user_pass
|
||||
) {}
|
||||
}
|
||||
|
||||
final class WP_Error {
|
||||
public function __construct(
|
||||
private string $code = '',
|
||||
private string $message = '',
|
||||
private mixed $data = null
|
||||
) {}
|
||||
public function get_error_code(): string { return $this->code; }
|
||||
public function get_error_message(): string { return $this->message; }
|
||||
public function get_error_data(): mixed { return $this->data; }
|
||||
}
|
||||
|
||||
final class JsonResponseException extends \RuntimeException {
|
||||
public function __construct(public array $payload) { parent::__construct('json_response'); }
|
||||
}
|
||||
|
||||
function add_action(string $hook, callable $callback): void {
|
||||
$GLOBALS['aksrt_account_actions'][$hook] = $callback;
|
||||
}
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function is_wp_error(mixed $value): bool { return $value instanceof WP_Error; }
|
||||
function is_email(string $email): bool { return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); }
|
||||
function sanitize_email(string $email): string { return trim($email); }
|
||||
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
|
||||
function sanitize_text_field(string $value): string { return trim(strip_tags($value)); }
|
||||
function wp_unslash(mixed $value): mixed { return $value; }
|
||||
function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'aksrt_account_settings'; }
|
||||
function get_current_user_id(): int { return (int) $GLOBALS['aksrt_account_user_id']; }
|
||||
function wp_get_current_user(): mixed { return $GLOBALS['aksrt_account_user']; }
|
||||
function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['aksrt_account_user'] : null; }
|
||||
function get_bloginfo(string $show = ''): string { return 'AKSRT Test'; }
|
||||
function wp_hash_password(string $password): string { return 'hash:' . $password; }
|
||||
function wp_check_password(string $password, string $hash, int $user_id = 0): bool { return hash_equals('hash:' . $password, $hash); }
|
||||
function email_exists(string $email): int|false { return $GLOBALS['aksrt_account_existing_emails'][strtolower($email)] ?? false; }
|
||||
function set_transient(string $key, mixed $value, int $expiration = 0): bool {
|
||||
$GLOBALS['aksrt_account_transients'][$key] = [
|
||||
'value' => $value,
|
||||
'expires' => $expiration > 0 ? time() + $expiration : 0,
|
||||
];
|
||||
return true;
|
||||
}
|
||||
function get_transient(string $key): mixed {
|
||||
$row = $GLOBALS['aksrt_account_transients'][$key] ?? null;
|
||||
if (!is_array($row)) return false;
|
||||
if (!empty($row['expires']) && $row['expires'] <= time()) {
|
||||
unset($GLOBALS['aksrt_account_transients'][$key]);
|
||||
return false;
|
||||
}
|
||||
return $row['value'] ?? false;
|
||||
}
|
||||
function delete_transient(string $key): bool {
|
||||
unset($GLOBALS['aksrt_account_transients'][$key]);
|
||||
return true;
|
||||
}
|
||||
function get_user_meta(int $user_id, string $key, bool $single = false): mixed {
|
||||
return $GLOBALS['aksrt_account_meta'][$user_id][$key] ?? '';
|
||||
}
|
||||
function update_user_meta(int $user_id, string $key, mixed $value): bool {
|
||||
$GLOBALS['aksrt_account_meta'][$user_id][$key] = $value;
|
||||
return true;
|
||||
}
|
||||
function delete_user_meta(int $user_id, string $key): bool {
|
||||
unset($GLOBALS['aksrt_account_meta'][$user_id][$key]);
|
||||
return true;
|
||||
}
|
||||
function wp_update_user(array $data): int|WP_Error {
|
||||
if ($GLOBALS['aksrt_account_update_fail']) {
|
||||
return new WP_Error('update_failed', 'update failed');
|
||||
}
|
||||
$user = $GLOBALS['aksrt_account_user'];
|
||||
if (isset($data['user_email'])) $user->user_email = (string) $data['user_email'];
|
||||
if (isset($data['user_pass'])) $user->user_pass = wp_hash_password((string) $data['user_pass']);
|
||||
return (int) $user->ID;
|
||||
}
|
||||
function aksrt_require_captcha(string $scene, array $payload): true|WP_Error {
|
||||
$GLOBALS['aksrt_account_captcha_calls'][] = $scene;
|
||||
return $GLOBALS['aksrt_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true;
|
||||
}
|
||||
function aksrt_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; }
|
||||
function wp_send_json_error(array $data): never { throw new JsonResponseException(['success' => false, 'data' => $data]); }
|
||||
function wp_send_json_success(array $data): never { throw new JsonResponseException(['success' => true, 'data' => $data]); }
|
||||
|
||||
function account_settings_request(string $action, array $data = []): array {
|
||||
$callback = $GLOBALS['aksrt_account_actions'][$action] ?? null;
|
||||
if (!is_callable($callback)) throw new \RuntimeException('Missing AJAX action: ' . $action);
|
||||
$_POST = array_merge(['_settings_nonce' => 'valid'], $data);
|
||||
try {
|
||||
$callback();
|
||||
} catch (JsonResponseException $response) {
|
||||
return $response->payload;
|
||||
}
|
||||
throw new \RuntimeException('AJAX action returned without a JSON response: ' . $action);
|
||||
}
|
||||
|
||||
function account_settings_check(bool $condition, string $message): void {
|
||||
if (!$condition) throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
function account_settings_code_key(string $scene, string $target): string {
|
||||
return 'aksrt_account_code_7_' . $scene . '_' . md5(strtolower(trim($target)));
|
||||
}
|
||||
|
||||
function account_settings_rate_key(string $scene, string $target): string {
|
||||
return 'aksrt_account_email_rate_7_' . $scene . '_' . md5(strtolower(trim($target)));
|
||||
}
|
||||
|
||||
function account_settings_code(string $scene, string $target): string {
|
||||
$stored = get_transient(account_settings_code_key($scene, $target));
|
||||
account_settings_check(is_array($stored), 'expected verification code was not stored for ' . $scene);
|
||||
return substr((string) ($stored['hash'] ?? ''), strlen('hash:'));
|
||||
}
|
||||
|
||||
function account_settings_proof_key(): string {
|
||||
return 'aksrt_account_proof_email_old_7';
|
||||
}
|
||||
|
||||
require dirname(__DIR__, 2) . '/inc/modules/user-parts/account-ajax.php';
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class Settings {
|
||||
public static function get_all(): array { return $GLOBALS['aksrt_account_settings']; }
|
||||
public static function send_event_email(string $event, string $to, array $values): bool {
|
||||
$GLOBALS['aksrt_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Auth {
|
||||
final class Service {
|
||||
public static function validate_password(string $password, string $confirm = ''): true|string {
|
||||
if (strlen($password) < 6) return 'password too short';
|
||||
if ($confirm !== '' && $password !== $confirm) return 'password mismatch';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
$GLOBALS['aksrt_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123');
|
||||
$send_action = 'wp_ajax_aksrt_send_account_email_code';
|
||||
$verify_action = 'wp_ajax_aksrt_verify_account_email';
|
||||
$change_email_action = 'wp_ajax_aksrt_change_email';
|
||||
$change_password_action = 'wp_ajax_aksrt_change_password';
|
||||
$saved_user = $GLOBALS['aksrt_account_user'];
|
||||
$GLOBALS['aksrt_account_user'] = null;
|
||||
account_settings_check(empty(account_settings_request($send_action, ['scene' => 'verify_email'])['success']), 'email-code request did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($verify_action, ['code' => '123456'])['success']), 'old-email verification did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($change_email_action, ['new_email' => 'new@example.test'])['success']), 'email update did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123'])['success']), 'password update did not handle a missing user object');
|
||||
$GLOBALS['aksrt_account_user'] = $saved_user;
|
||||
|
||||
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
|
||||
account_settings_check(!empty($old_send['success']), 'old email verification code could not be sent');
|
||||
$old_code = account_settings_code('verify_email', 'old@example.test');
|
||||
$bad_old_code = account_settings_request($verify_action, ['code' => 'wrong']);
|
||||
account_settings_check(empty($bad_old_code['success']), 'email verification accepted an incorrect old-email code');
|
||||
$old_code_row = get_transient(account_settings_code_key('verify_email', 'old@example.test'));
|
||||
account_settings_check(($old_code_row['attempts'] ?? 0) === 1, 'incorrect old-email attempt was not recorded');
|
||||
$GLOBALS['aksrt_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1;
|
||||
$expired_old_code = account_settings_request($verify_action, ['code' => $old_code]);
|
||||
account_settings_check(empty($expired_old_code['success']), 'email verification accepted an expired old-email code');
|
||||
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'expired old-email code was not discarded');
|
||||
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof');
|
||||
delete_transient(account_settings_rate_key('verify_email', 'old@example.test'));
|
||||
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
|
||||
account_settings_check(!empty($old_send['success']), 'old email code could not be resent after expiry');
|
||||
$old_code = account_settings_code('verify_email', 'old@example.test');
|
||||
$verified_old = account_settings_request($verify_action, ['code' => $old_code]);
|
||||
account_settings_check(!empty($verified_old['success']), 'email verification rejected the correct old-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued');
|
||||
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'valid old-email code was not consumed');
|
||||
|
||||
$new_email = 'new@example.test';
|
||||
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
|
||||
account_settings_check(!empty($new_send['success']), 'new email verification code could not be sent');
|
||||
$new_code = account_settings_code('change_email', $new_email);
|
||||
$GLOBALS['aksrt_account_existing_emails']['taken@example.test'] = 19;
|
||||
$duplicate = account_settings_request($change_email_action, ['new_email' => 'taken@example.test', 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($duplicate['success']), 'email update accepted an address already owned by another account');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'duplicate-email validation consumed the new-email code');
|
||||
unset($GLOBALS['aksrt_account_existing_emails']['taken@example.test']);
|
||||
|
||||
$bad_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => 'wrong', 'agree_terms' => '1']);
|
||||
account_settings_check(empty($bad_new_code['success']), 'email update accepted an incorrect new-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'incorrect-code validation consumed the new-email code');
|
||||
$GLOBALS['aksrt_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1;
|
||||
$expired_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($expired_new_code['success']), 'email update accepted an expired new-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'expired new-email code was not discarded');
|
||||
delete_transient(account_settings_rate_key('change_email', $new_email));
|
||||
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
|
||||
account_settings_check(!empty($new_send['success']), 'new email code could not be resent after expiry');
|
||||
$new_code = account_settings_code('change_email', $new_email);
|
||||
|
||||
$GLOBALS['aksrt_account_update_fail'] = true;
|
||||
$write_failure = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($write_failure['success']), 'email update reported success after the user write failed');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'failed database write consumed the new-email code');
|
||||
$GLOBALS['aksrt_account_update_fail'] = false;
|
||||
$changed = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($changed['success']), 'email update rejected valid old and new proofs');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $new_email, 'email update did not persist the new address');
|
||||
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'successful update retained the new-email code');
|
||||
account_settings_check((int) get_user_meta(7, 'aksrt_email_verified', true) === 1, 'successful update did not mark the email verified');
|
||||
account_settings_check(end($GLOBALS['aksrt_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email');
|
||||
|
||||
$GLOBALS['aksrt_account_settings']['email_set_captch'] = false;
|
||||
$direct_email = account_settings_request($change_email_action, ['new_email' => 'direct@example.test', 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($direct_email['success']), 'email update without configured verification still required a code');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === 'direct@example.test', 'direct email update did not persist');
|
||||
|
||||
$GLOBALS['aksrt_account_settings']['email_set_captch'] = true;
|
||||
$GLOBALS['aksrt_account_user']->user_email = '';
|
||||
$first_email = 'first@example.test';
|
||||
$first_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $first_email]);
|
||||
account_settings_check(!empty($first_send['success']), 'first email binding did not send a new-address code');
|
||||
$first_code = account_settings_code('change_email', $first_email);
|
||||
$first_bind = account_settings_request($change_email_action, ['new_email' => $first_email, 'code' => $first_code, 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($first_bind['success']), 'first email binding incorrectly required old-email verification');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $first_email, 'first email binding did not persist');
|
||||
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = true;
|
||||
$captcha_rejected = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(empty($captcha_rejected['success']), 'password change bypassed human verification');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password');
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = false;
|
||||
|
||||
for ($attempt = 0; $attempt < 8; $attempt++) {
|
||||
$wrong_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'wrong']);
|
||||
account_settings_check(empty($wrong_password['success']), 'password change accepted an incorrect old password');
|
||||
}
|
||||
$limited_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(empty($limited_password['success']), 'password change did not apply its old-password attempt limit');
|
||||
delete_transient('aksrt_account_rate_change_password_7');
|
||||
$changed_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(!empty($changed_password['success']), 'password change rejected the correct old password');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password');
|
||||
account_settings_check(get_transient('aksrt_account_rate_change_password_7') === false, 'successful password change did not reset its error limit');
|
||||
|
||||
update_user_meta(7, 'oauth_new', 1);
|
||||
$oauth_password = account_settings_request($change_password_action, ['password' => 'OAuthPass123', 'password2' => 'OAuthPass123']);
|
||||
account_settings_check(!empty($oauth_password['success']), 'OAuth first-time password setup incorrectly required an old password');
|
||||
account_settings_check(get_user_meta(7, 'oauth_new', true) === '', 'OAuth first-time password setup retained the new-account marker');
|
||||
|
||||
echo "Account settings harness passed\n";
|
||||
}
|
||||
@@ -42,16 +42,16 @@ test('password and email account flows verify captcha once per distinct HTTP req
|
||||
return account.slice(start, end === -1 ? account.length : end);
|
||||
};
|
||||
|
||||
const send = methodBody('wp_ajax_aksrt_send_email_code', 'wp_ajax_aksrt_change_password');
|
||||
const send = methodBody('wp_ajax_aksrt_send_account_email_code', 'wp_ajax_aksrt_verify_account_email');
|
||||
const password = methodBody('wp_ajax_aksrt_change_password', 'wp_ajax_aksrt_change_email');
|
||||
const email = methodBody('wp_ajax_aksrt_change_email');
|
||||
|
||||
assert.equal((send.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(send, /aksrt_require_captcha\( \$scene,/);
|
||||
assert.match(send, /aksrt_require_captcha\(\s*'change_email'/);
|
||||
assert.equal((password.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(password, /aksrt_require_captcha\( 'change_password'/);
|
||||
assert.match(password, /aksrt_require_captcha\(\s*'change_password'/);
|
||||
assert.equal((email.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(email, /aksrt_require_captcha\( 'change_email'/);
|
||||
assert.match(email, /aksrt_require_captcha\(\s*'change_email'/);
|
||||
});
|
||||
|
||||
test('legacy public SMS-code AJAX surface is removed while SMS service remains', () => {
|
||||
@@ -59,6 +59,5 @@ test('legacy public SMS-code AJAX surface is removed while SMS service remains',
|
||||
|
||||
assert.doesNotMatch(sms, /aksrt_send_sms_code/);
|
||||
assert.doesNotMatch(sms, /function ajax_send_code\s*\(/);
|
||||
assert.match(sms, /public function send_code\(string \$phone\): array/);
|
||||
assert.match(sms, /public function verify_code\(string \$phone, string \$code\): array/);
|
||||
assert.match(sms, /public function send_code\(string \$phone, string \$code\): array/);
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ test('auth and account security flows consume fresh captcha tickets', () => {
|
||||
const center = read('assets/js/routes/user-center.js');
|
||||
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone-code/);
|
||||
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone/);
|
||||
assert.match(center, /runCaptcha\(scene, fm\)[\s\S]*resetCaptcha\(scene, fm\)/);
|
||||
assert.match(center, /function postAjaxForm\(form, action, scene, success\)[\s\S]*?runCaptcha\(scene, form\)[\s\S]*?finally\(function \(\) \{ resetCaptcha\(scene, form\); \}\)/);
|
||||
assert.match(center, /\/oauth\/bind-pass/);
|
||||
assert.match(center, /bind_verification_required:\s*'请先完成人机验证后再绑定'/);
|
||||
});
|
||||
|
||||
@@ -104,6 +104,7 @@ test('wallet reward content opens in the shared modal runtime once', () => {
|
||||
const authorBar = read('templates/parts/single-author-bar.php');
|
||||
const singleActions = read('inc/helpers/aksrt-single-render.php');
|
||||
const scripts = read('src/Core/ScriptsPart1b.php');
|
||||
const scriptsPart1c = read('src/Core/ScriptsPart1c.php');
|
||||
|
||||
assert.match(reward, /runtime\.open\(\{/);
|
||||
assert.match(reward, /className: 'aksrt-reward-modal modal-mini full-sm'/);
|
||||
@@ -122,7 +123,8 @@ test('wallet reward content opens in the shared modal runtime once', () => {
|
||||
assert.match(helper, /data-aksrt-reward-submit/);
|
||||
assert.match(authorBar, /aksrt_render_rewards_template_for_user/);
|
||||
assert.match(singleActions, /aksrt_render_rewards_template_for_user/);
|
||||
assert.match(scripts, /'needs_reward'\s*=>\s*'single-post\/rewards'/);
|
||||
assert.match(scriptsPart1c, /\$requirements\['needs_reward'\]\s*=\s*\$is_article && !empty\(\$settings\['rewards_enabled'\]\)/);
|
||||
assert.match(scriptsPart1c, /\$singular_assets\['needs_reward'\][\s\S]*?\$reward_js[\s\S]*?'aksrt-reward'/);
|
||||
assert.doesNotMatch(helper + authorBar + singleActions, /aksrt_render_rewards_modal_for_user|reward-modal__qr/);
|
||||
});
|
||||
|
||||
|
||||
@@ -490,8 +490,9 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/);
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/);
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/);
|
||||
const settingsSubmitButtons = settingsAccount.match(/<button type="submit" class="but jb-blue padding-lg btn-block"[^>]*><i class="fa fas fa-check" aria-hidden="true"><\/i> <span data-aksrt-settings-submit-label><\?php esc_html_e\('确认提交', 'aksrt'\); \?><\/span><\/button>/g) || [];
|
||||
assert.equal(settingsSubmitButtons.length, 3, 'password, email, and phone dialogs use the reference submit button with the FA5 icon');
|
||||
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-aksrt-settings-submit-label/);
|
||||
assert.match(settingsAccount, /id="email-new-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||||
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||||
assert.doesNotMatch(settingsAccount, /aksrt-settings-modal__footer|确认修改|确认更改|确认绑定/);
|
||||
assert.ok(settingsAccount.indexOf('绑定邮箱') < settingsAccount.indexOf('绑定手机'));
|
||||
assert.ok(settingsAccount.indexOf('绑定手机') < settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'"));
|
||||
@@ -504,13 +505,20 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
|
||||
assert.match(userCenterJs, /className: 'aksrt-user-settings-modal modal-mini'/);
|
||||
assert.match(userCenterJs, /mobileBottom: true/);
|
||||
assert.match(userCenterJs, /onContent: function \(_, session\)/);
|
||||
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(shell\)/);
|
||||
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
|
||||
assert.match(userCenterJs, /resetCaptcha\(scene, form\)/);
|
||||
assert.match(userCenterJs, /function setSettingsSubmitLabel\(button, text\)[\s\S]*?label\.textContent = text/);
|
||||
assert.match(userCenterJs, /setSettingsSubmitLabel\(submitBtn, submitIdleText\)/);
|
||||
assert.match(userCenterJs, /aksrt_change_password/);
|
||||
assert.match(userCenterJs, /aksrt_change_email/);
|
||||
assert.match(userCenterJs, /AksrtModalRuntime\.close\('settings-submit'\)/);
|
||||
assert.match(settingsAccount, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"/);
|
||||
assert.match(settingsAccount, /\$email_verify_required/);
|
||||
assert.match(settingsAccount, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"/);
|
||||
assert.match(settingsAccount, /data-account-recovery-open[\s\S]*?data-account-password-pane="recovery"/);
|
||||
assert.match(settingsAccount, /aksrt_render_captcha\('change_password'\)/);
|
||||
assert.match(userCenterJs, /\/auth\/code/);
|
||||
assert.match(userCenterJs, /\/auth\/reset-password/);
|
||||
assert.match(userCenterJs, /\/user\/phone-verify/);
|
||||
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|aksrt_send_email_code/);
|
||||
assert.match(settingsCss, /\.aksrt-modal-runtime\.aksrt-user-settings-modal:not\(\.aksrt-modal-runtime--bottom\)[\s\S]*?width: 400px/);
|
||||
assert.match(settingsCss, /\.aksrt-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
|
||||
assert.match(settingsAccount, /class="colorful-make"/);
|
||||
|
||||
@@ -340,30 +340,25 @@ test('settings actions use rehydrated fixed-id notyf flows', () => {
|
||||
const context = read('inc/helpers/aksrt-user-center.php');
|
||||
const page = read('templates/pages/page-profile.php');
|
||||
|
||||
[
|
||||
'aksrt-settings-password-code-toast',
|
||||
'aksrt-settings-email-code-toast',
|
||||
'aksrt-settings-password-toast',
|
||||
'aksrt-settings-email-toast',
|
||||
'aksrt-settings-oauth-unbind-toast',
|
||||
'aksrt-settings-phone-bind-toast',
|
||||
].forEach((id) => assert.match(userCenter, new RegExp("['\"]" + id + "['\"]")));
|
||||
assert.match(userCenter, /aksrt-settings-oauth-unbind-toast/);
|
||||
assert.doesNotMatch(userCenter, /aksrt-settings-(?:password|email|phone)-(?:code-)?toast/);
|
||||
|
||||
assert.match(userCenter, /notyf\([^;]+?'load'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'success'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'danger'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initBindPhone\(\);/);
|
||||
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initSettingsCallbackToast\(\);/);
|
||||
assert.doesNotMatch(userCenter, /initBindPhone\(/);
|
||||
assert.match(userCenter, /aksrtSettingsInitialized/);
|
||||
assert.match(userCenter, /oauth_action[\s\S]*?notyf\(message, type\)[\s\S]*?history\.replaceState/);
|
||||
assert.doesNotMatch(userCenter, /oauth_unbound/);
|
||||
assert.match(auth, /\[data-identity-root\], \[data-aksrt-settings-root\]/);
|
||||
assert.doesNotMatch(auth, /\.settings-section/);
|
||||
|
||||
assert.match(userCenter, /'aksrt-settings-phone-code-toast'/);
|
||||
assert.match(userCenter, /notyf\('正在发送验证码…', 'load', '', toastId\)/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'success', 5, toastId\)/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'danger', 5, toastId\)/);
|
||||
assert.match(userCenter, /\/user\/phone-code/);
|
||||
assert.match(userCenter, /\/user\/phone-verify/);
|
||||
const accountSettings = read('inc/helpers/user-center-renderers/settings-account.php');
|
||||
assert.match(accountSettings, /data-aksrt-sms-scene="verify_old_phone"/);
|
||||
assert.match(accountSettings, /data-aksrt-sms-scene="bind_new_phone"/);
|
||||
|
||||
assert.match(settings, /require \$dir \. '\/settings-account\.php';/);
|
||||
assert.doesNotMatch(context, /account_modals_html|'account-modals'/);
|
||||
|
||||
@@ -198,27 +198,31 @@ test('native user profile omits ledger tables and renders canonical images eager
|
||||
assert.doesNotMatch(level, /Level::get_badge_html/);
|
||||
});
|
||||
|
||||
test('identity status, type, and name are directly editable on the native profile', () => {
|
||||
test('identity marker and profile are editable on the native profile while applications use the review queue', () => {
|
||||
const auth = read('src/User/AuthApply.php');
|
||||
const verify = read('src/Admin/Native/UsersMenusVerify.php');
|
||||
const transitions = read('src/Identity/TransitionService.php');
|
||||
|
||||
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'handle_admin_profile_update'\]\)/);
|
||||
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'save_admin_profile'\]\)/);
|
||||
assert.match(auth, /current_user_can\('aksrt_review_identity'\)/);
|
||||
assert.match(auth, /wp_nonce_field\('aksrt_identity_profile_' \.[\s\S]*?'aksrt_identity_profile_nonce'\)/);
|
||||
assert.match(auth, /name="aksrt_identity_application_uuid"/);
|
||||
assert.match(auth, /name="aksrt_identity_type"/);
|
||||
assert.match(auth, /value="personal"[\s\S]*?个人认证/);
|
||||
assert.match(auth, /value="enterprise"[\s\S]*?企业认证/);
|
||||
assert.match(auth, /name="aksrt_identity_public_name"/);
|
||||
assert.match(auth, /姓名 \/ 主体全称/);
|
||||
assert.match(auth, /name="aksrt_identity_status"/);
|
||||
assert.match(auth, /'manual_pending' => __\('待审核'/);
|
||||
assert.match(auth, /'verified' => __\('已认证'/);
|
||||
assert.match(auth, /'rejected' => __\('已驳回'/);
|
||||
assert.match(auth, /create_admin_application\(\$repo, \$user_id, \$fields\)/);
|
||||
assert.match(auth, /'type' => \$fields\['type'\]/);
|
||||
assert.match(auth, /self::review\(\$user_id, \$application_uuid, \$target_status, \$note, get_current_user_id\(\), \$fields\)/);
|
||||
assert.match(transitions, /public function admin_transition/);
|
||||
assert.match(transitions, /\? in_array\(\$next, \['manual_pending', 'verified', 'rejected', 'cancelled'\], true\)/);
|
||||
assert.match(transitions, /'verified' => \['cancelled'\]/);
|
||||
assert.match(auth, /wp_nonce_field\('aksrt_auth_profile_' \.[\s\S]*?'aksrt_auth_profile_nonce'\)/);
|
||||
assert.match(auth, /name="aksrt_auth_enabled"/);
|
||||
assert.match(auth, /name="aksrt_auth_name"/);
|
||||
assert.match(auth, /name="aksrt_auth_desc"/);
|
||||
assert.match(auth, /name="aksrt_auth_time"/);
|
||||
assert.match(auth, /update_user_meta\(\$user_id, 'auth', 1\)/);
|
||||
assert.match(auth, /update_user_meta\(\$user_id, 'auth_info'/);
|
||||
assert.doesNotMatch(auth, /name="aksrt_identity_(?:application_uuid|type|status|public_name)"/);
|
||||
|
||||
assert.match(verify, /'general',[\s\S]*?'manual'/);
|
||||
assert.match(verify, /if \(\$status === 'manual_pending'\)/);
|
||||
assert.match(verify, /'verified' => __\('通过申请'/);
|
||||
assert.match(verify, /'rejected' => __\('拒绝申请'/);
|
||||
assert.match(verify, /AuthApply::review\(\$user_id, \$uuid, \$status, \$note, get_current_user_id\(\), \$fields\)/);
|
||||
assert.match(auth, /\['verified', 'rejected'\]/);
|
||||
assert.match(auth, /\(string\) \$application\['status'\] !== 'manual_pending'/);
|
||||
assert.match(transitions, /'manual_pending' => 'pending'/);
|
||||
assert.match(transitions, /'verified' => 'verified'/);
|
||||
assert.match(transitions, /'rejected' => 'rejected'/);
|
||||
assert.match(transitions, /default => ''/);
|
||||
});
|
||||
|
||||
@@ -5,10 +5,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('MINUTE_IN_SECONDS', 60);
|
||||
define('HOUR_IN_SECONDS', 3600);
|
||||
$GLOBALS['aksrt_profile_meta'] = [];
|
||||
$GLOBALS['aksrt_profile_users'] = [];
|
||||
$GLOBALS['aksrt_nonce_valid'] = true;
|
||||
$GLOBALS['aksrt_can_edit_user'] = true;
|
||||
$GLOBALS['aksrt_current_user_id'] = 7;
|
||||
$GLOBALS['aksrt_transients'] = [];
|
||||
$GLOBALS['aksrt_expected_sms_code'] = '246810';
|
||||
|
||||
@@ -34,7 +37,7 @@ namespace {
|
||||
function is_admin(): bool { return true; }
|
||||
function current_user_can(string $capability, ...$args): bool { return $GLOBALS['aksrt_can_edit_user']; }
|
||||
function is_user_logged_in(): bool { return true; }
|
||||
function get_current_user_id(): int { return 7; }
|
||||
function get_current_user_id(): int { return (int) ($GLOBALS['aksrt_current_user_id'] ?? 7); }
|
||||
function get_user_meta(int $user_id, string $key, bool $single = false) {
|
||||
$value = $GLOBALS['aksrt_profile_meta'][$user_id][$key] ?? '';
|
||||
return $single ? $value : ($value === '' ? [] : [$value]);
|
||||
@@ -48,7 +51,7 @@ namespace {
|
||||
foreach ($GLOBALS['aksrt_profile_meta'] as $user_id => $meta) {
|
||||
if (in_array((int) $user_id, array_map('intval', $args['exclude'] ?? []), true)) continue;
|
||||
if (($meta[$args['meta_key'] ?? ''] ?? null) !== ($args['meta_value'] ?? null)) continue;
|
||||
$results[] = ($args['fields'] ?? '') === 'ids' ? (int) $user_id : new WP_User((int) $user_id);
|
||||
$results[] = in_array(($args['fields'] ?? ''), ['ids', 'ID'], true) ? (int) $user_id : new WP_User((int) $user_id);
|
||||
if (count($results) >= (int) ($args['number'] ?? 1)) break;
|
||||
}
|
||||
return $results;
|
||||
@@ -99,6 +102,12 @@ namespace AKSRT\Sms {
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class Settings {
|
||||
public static function get_all(): array { return ['bind_phone' => true]; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Auth {
|
||||
require dirname(__DIR__, 2) . '/src/Auth/ServicePart1b.php';
|
||||
final class PhoneHarnessService { use ServicePart1b; }
|
||||
@@ -150,10 +159,25 @@ namespace {
|
||||
check(empty($same_phone['success']), 'binding accepted the current phone as a change');
|
||||
|
||||
$phone_to_bind = '13800000010';
|
||||
$otp_key = 'aksrt_auth_code_' . md5('bind_phone|' . strtolower(trim($phone_to_bind)));
|
||||
$otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($phone_to_bind)));
|
||||
$operations = new \AKSRT\Admin\PhoneOperationsHarness();
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$old_phone = '13800000007';
|
||||
$old_otp_key = 'aksrt_auth_code_' . md5('account_verify_old_phone_7|' . strtolower(trim($old_phone)));
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$GLOBALS['aksrt_expected_sms_code'] = '246810';
|
||||
$wrong_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '000000']));
|
||||
check(empty($wrong_old_code->data['success']), 'REST phone replacement accepted an incorrect old-number code');
|
||||
check(($GLOBALS['aksrt_transients'][$old_otp_key]['attempts'] ?? 0) === 1, 'incorrect old-number code attempt was not recorded');
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() - 1];
|
||||
$expired_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
|
||||
check(empty($expired_old_code->data['success']), 'REST phone replacement accepted an expired old-number code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'expired old-number code was not discarded');
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$verified_old_phone = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
|
||||
check(!empty($verified_old_phone->data['success']), 'REST phone replacement rejected the correct old-number code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'valid old-number code was not consumed');
|
||||
check(($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']['phone'] ?? '') === $old_phone, 'old-number proof was not issued for the current account');
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$wrong_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '000000']));
|
||||
check(empty($wrong_code->data['success']), 'REST phone binding accepted an incorrect SMS code');
|
||||
check(($GLOBALS['aksrt_transients'][$otp_key]['attempts'] ?? 0) === 1, 'incorrect SMS code attempt was not recorded');
|
||||
@@ -161,11 +185,29 @@ namespace {
|
||||
$expired_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
|
||||
check(empty($expired_code->data['success']), 'REST phone binding accepted an expired SMS code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'expired SMS code was not discarded');
|
||||
$duplicate_phone = '13800000008';
|
||||
$duplicate_otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($duplicate_phone)));
|
||||
$GLOBALS['aksrt_transients'][$duplicate_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$duplicate_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $duplicate_phone, 'code' => '246810']));
|
||||
check(empty($duplicate_code->data['success']), 'REST phone binding accepted a number owned by another user');
|
||||
check(isset($GLOBALS['aksrt_transients'][$duplicate_otp_key]), 'duplicate-number validation consumed the valid SMS code');
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$valid_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
|
||||
check(!empty($valid_code->data['success']), 'REST phone binding rejected the correct SMS code');
|
||||
check(get_user_meta(7, 'phone_number', true) === $phone_to_bind, 'REST phone binding did not store the canonical phone meta');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'valid SMS code was not consumed');
|
||||
check(!isset($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']), 'successful phone change did not consume the old-number proof');
|
||||
|
||||
$initial_bind_user_id = 9;
|
||||
$initial_bind_phone = '13800000011';
|
||||
$initial_bind_code_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_9|' . strtolower(trim($initial_bind_phone)));
|
||||
$GLOBALS['aksrt_current_user_id'] = $initial_bind_user_id;
|
||||
$GLOBALS['aksrt_transients'][$initial_bind_code_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$initial_bind = $operations->user_bind_phone(new WP_REST_Request(['phone' => $initial_bind_phone, 'code' => '246810']));
|
||||
check(!empty($initial_bind->data['success']), 'first-time phone binding incorrectly required an old-number proof');
|
||||
check(get_user_meta($initial_bind_user_id, 'phone_number', true) === $initial_bind_phone, 'first-time phone binding did not store the canonical phone meta');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$initial_bind_code_key]), 'successful first-time phone binding did not consume its SMS code');
|
||||
$GLOBALS['aksrt_current_user_id'] = 7;
|
||||
|
||||
$_POST = [
|
||||
'aksrt_profile_admin_nonce' => 'ok',
|
||||
|
||||
Reference in New Issue
Block a user