611 lines
21 KiB
JavaScript
611 lines
21 KiB
JavaScript
/**
|
|
* AKSRT modal auth interactions.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
/**
|
|
* Resolve REST root for aksrt/v1.
|
|
* Prefer aksrtData.restUrl (works with plain permalinks: index.php?rest_route=/aksrt/v1).
|
|
* Do not hardcode /wp-json when rewrite rules are off — that 404s on Apache.
|
|
*/
|
|
function resolveApiRoot() {
|
|
var d = window.aksrtData || {};
|
|
var raw = d.apiUrl || d.restUrl || d.rest_url || '';
|
|
if (!raw) {
|
|
raw = '/index.php?rest_route=/aksrt/v1';
|
|
}
|
|
raw = String(raw).trim();
|
|
// restUrl may be site root rest_url() without namespace in some localizes — ensure aksrt/v1.
|
|
if (/rest_route=\/?$/.test(raw) || /\/wp-json\/?$/.test(raw)) {
|
|
if (raw.indexOf('rest_route=') !== -1) {
|
|
raw = raw.replace(/rest_route=\/?$/, 'rest_route=/aksrt/v1');
|
|
} else {
|
|
raw = raw.replace(/\/?$/, '/') + 'aksrt/v1';
|
|
}
|
|
}
|
|
return raw.replace(/\/$/, '');
|
|
}
|
|
|
|
var API_ROOT = resolveApiRoot();
|
|
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
|
|
|
|
var modal = null;
|
|
var currentTab = 'login';
|
|
|
|
function init() {
|
|
modal = document.getElementById('aksrt-modal-auth');
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
var closeBtn = modal.querySelector('.aksrt-modal-auth__close');
|
|
var overlay = modal.querySelector('.aksrt-modal-auth__overlay');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', close);
|
|
}
|
|
if (overlay) {
|
|
overlay.addEventListener('click', close);
|
|
}
|
|
|
|
document.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
|
|
close();
|
|
}
|
|
});
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__tab').forEach(function (tab) {
|
|
tab.addEventListener('click', function () {
|
|
switchTab(this.dataset.tab || 'login');
|
|
});
|
|
});
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__switch').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
switchTab(this.dataset.switch || 'login');
|
|
});
|
|
});
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__pass-toggle').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
var input = this.parentElement.querySelector('input');
|
|
var icon = this.querySelector('i');
|
|
if (!input || !icon) {
|
|
return;
|
|
}
|
|
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
icon.className = 'far fa-eye-slash';
|
|
} else {
|
|
input.type = 'password';
|
|
icon.className = 'far fa-eye';
|
|
}
|
|
});
|
|
});
|
|
|
|
var loginForm = document.getElementById('aksrt-modal-login-form');
|
|
if (loginForm) {
|
|
loginForm.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
handleLogin(loginForm);
|
|
});
|
|
}
|
|
|
|
var registerForm = document.getElementById('aksrt-modal-register-form');
|
|
if (registerForm) {
|
|
registerForm.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
handleRegister(registerForm);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var trigger = event.target.closest('[data-aksrt-auth-required], .aksrt-login-trigger, #nav-login-btn, .signin-loader, .signup-loader, .qrcode-signin');
|
|
if (!trigger) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
if (trigger.classList.contains('signup-loader')) {
|
|
open(trigger.dataset.authRedirect || '');
|
|
switchTab('register');
|
|
return;
|
|
}
|
|
if (trigger.classList.contains('qrcode-signin')) {
|
|
open(trigger.dataset.authRedirect || '');
|
|
switchTab('qrcode');
|
|
return;
|
|
}
|
|
|
|
open(trigger.dataset.authRedirect || '');
|
|
});
|
|
|
|
initQrLogin();
|
|
initAgreementShake();
|
|
initSocialPriority();
|
|
}
|
|
|
|
function open(redirectUrl) {
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
modal.classList.add('is-open');
|
|
modal.setAttribute('aria-hidden', 'false');
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
if (redirectUrl) {
|
|
modal.dataset.redirect = redirectUrl;
|
|
}
|
|
|
|
setTimeout(function () {
|
|
var firstInput = modal.querySelector('.aksrt-modal-auth__panel.is-active input:not([type="hidden"])');
|
|
if (firstInput) {
|
|
firstInput.focus();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
var qrLoginTimer = null;
|
|
|
|
function close() {
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
modal.classList.remove('is-open');
|
|
modal.setAttribute('aria-hidden', 'true');
|
|
document.body.style.overflow = '';
|
|
clearMessages();
|
|
stopQrPolling();
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__tab').forEach(function (button) {
|
|
var isActive = button.dataset.tab === tab;
|
|
button.classList.toggle('is-active', isActive);
|
|
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
});
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__panel').forEach(function (panel) {
|
|
panel.classList.toggle('is-active', panel.dataset.panel === tab);
|
|
});
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__footer-text').forEach(function (row) {
|
|
row.hidden = row.dataset.for !== tab;
|
|
});
|
|
|
|
modal.setAttribute('aria-label', tab === 'register' ? '注册' : '登录');
|
|
clearMessages();
|
|
}
|
|
|
|
function withCaptcha(scene, form, next) {
|
|
var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes
|
|
? window.AKSRTCaptchaState.scenes
|
|
: null;
|
|
var instance = store && store[scene] ? store[scene] : null;
|
|
if (instance && typeof instance.verify === 'function') {
|
|
instance.verify(function () {
|
|
next(serializeForm(form));
|
|
});
|
|
return;
|
|
}
|
|
next(serializeForm(form));
|
|
}
|
|
|
|
function resetCaptcha(scene) {
|
|
try {
|
|
var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes
|
|
? window.AKSRTCaptchaState.scenes
|
|
: null;
|
|
if (store && store[scene] && typeof store[scene].reset === 'function') {
|
|
store[scene].reset();
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
function handleLogin(form) {
|
|
if (!form.querySelector('[name="log"]') || !form.querySelector('[name="pwd"]')) {
|
|
showMessage('login', '请输入用户名和密码', 'error');
|
|
return;
|
|
}
|
|
|
|
setLoading(form, true);
|
|
clearMessages();
|
|
|
|
withCaptcha('login', form, function (payload) {
|
|
if (!payload.log || !payload.pwd) {
|
|
setLoading(form, false);
|
|
showMessage('login', '请输入用户名和密码', 'error');
|
|
return;
|
|
}
|
|
|
|
request('/auth/modal-login', payload)
|
|
.then(function (result) {
|
|
if (result.ok && result.data.success) {
|
|
onAuthSuccess(result.data);
|
|
return;
|
|
}
|
|
|
|
resetCaptcha('login');
|
|
var message = result.data.message || '登录失败';
|
|
if (result.data.verify_url) {
|
|
message += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
|
|
}
|
|
showMessage('login', message, 'error');
|
|
})
|
|
.catch(function () {
|
|
resetCaptcha('login');
|
|
showMessage('login', '网络错误,请稍后重试', 'error');
|
|
})
|
|
.finally(function () {
|
|
setLoading(form, false);
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleRegister(form) {
|
|
setLoading(form, true);
|
|
clearMessages();
|
|
|
|
withCaptcha('register', form, function (payload) {
|
|
if (!payload.user_login || !payload.user_email || !payload.user_pass || !payload.user_pass_confirm) {
|
|
setLoading(form, false);
|
|
showMessage('register', '请完整填写注册信息', 'error');
|
|
return;
|
|
}
|
|
|
|
if (payload.user_pass !== payload.user_pass_confirm) {
|
|
setLoading(form, false);
|
|
showMessage('register', '两次输入的密码不一致', 'error');
|
|
return;
|
|
}
|
|
|
|
request('/auth/modal-register', payload)
|
|
.then(function (result) {
|
|
if (!result.ok || !result.data.success) {
|
|
resetCaptcha('register');
|
|
showMessage('register', result.data.message || '注册失败', 'error');
|
|
return;
|
|
}
|
|
|
|
if (result.data.user) {
|
|
onAuthSuccess(result.data);
|
|
return;
|
|
}
|
|
|
|
if (result.data.verification_required) {
|
|
var verifyMessage = result.data.message || '注册成功,请完成邮箱验证';
|
|
if (result.data.notice) verifyMessage += '<br>' + escapeAttribute(result.data.notice);
|
|
if (result.data.verify_url) {
|
|
verifyMessage += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
|
|
}
|
|
showMessage('register', verifyMessage, 'success');
|
|
form.reset();
|
|
resetCaptcha('register');
|
|
return;
|
|
}
|
|
|
|
var loginMessage = result.data.message || '注册成功,请登录';
|
|
if (result.data.notice) loginMessage += ' ' + result.data.notice;
|
|
switchTab('login');
|
|
prefillLogin(result.data.user_login || result.data.user_email || '');
|
|
showMessage('login', loginMessage, 'success');
|
|
form.reset();
|
|
resetCaptcha('register');
|
|
})
|
|
.catch(function () {
|
|
resetCaptcha('register');
|
|
showMessage('register', '网络错误,请稍后重试', 'error');
|
|
})
|
|
.finally(function () {
|
|
setLoading(form, false);
|
|
});
|
|
});
|
|
}
|
|
|
|
function serializeForm(form) {
|
|
var payload = {};
|
|
var formData = new FormData(form);
|
|
|
|
formData.forEach(function (value, key) {
|
|
if (payload[key] !== undefined) {
|
|
if (!Array.isArray(payload[key])) {
|
|
payload[key] = [payload[key]];
|
|
}
|
|
payload[key].push(value);
|
|
} else {
|
|
payload[key] = value;
|
|
}
|
|
});
|
|
|
|
return payload;
|
|
}
|
|
|
|
function request(path, payload) {
|
|
var headers = { 'Content-Type': 'application/json' };
|
|
if (NONCE) {
|
|
headers['X-WP-Nonce'] = NONCE;
|
|
}
|
|
|
|
var url = joinRestPath(API_ROOT, path);
|
|
|
|
return fetch(url, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(payload),
|
|
}).then(function (response) {
|
|
return response.json().then(function (data) {
|
|
return { ok: response.ok, data: data };
|
|
}).catch(function () {
|
|
return { ok: false, data: { message: '响应解析失败' } };
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Join REST root + path for both pretty (/wp-json/aksrt/v1/x)
|
|
* and plain (?rest_route=/aksrt/v1/x) permalinks.
|
|
*/
|
|
function joinRestPath(root, path) {
|
|
path = String(path || '');
|
|
if (path.charAt(0) !== '/') {
|
|
path = '/' + path;
|
|
}
|
|
root = String(root || '').replace(/\/$/, '');
|
|
if (root.indexOf('rest_route=') !== -1) {
|
|
// e.g. https://host/index.php?rest_route=/aksrt/v1 + /auth/modal-login
|
|
return root + path;
|
|
}
|
|
return root + path;
|
|
}
|
|
|
|
function prefillLogin(value) {
|
|
var loginInput = document.getElementById('modal-login-user');
|
|
if (loginInput && value) {
|
|
loginInput.value = value;
|
|
}
|
|
}
|
|
|
|
function onAuthSuccess(data) {
|
|
close();
|
|
|
|
if (data.user) {
|
|
updateNavUser(data.user);
|
|
}
|
|
|
|
notyf(data.message || '登录成功', 'success');
|
|
|
|
var redirect = modal.dataset.redirect || data.redirect;
|
|
if (redirect && redirect !== window.location.href) {
|
|
setTimeout(function () {
|
|
window.location.href = redirect;
|
|
}, 500);
|
|
return;
|
|
}
|
|
|
|
setTimeout(function () {
|
|
window.location.reload();
|
|
}, 500);
|
|
}
|
|
|
|
function updateNavUser(user) {
|
|
document.querySelectorAll('.nav-user__avatar img, .nav-user__toggle img').forEach(function (image) {
|
|
if (user.avatar_url) {
|
|
image.src = user.avatar_url;
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.nav-user__name').forEach(function (el) {
|
|
if (user.display_name) {
|
|
el.textContent = user.display_name;
|
|
}
|
|
});
|
|
}
|
|
|
|
function showMessage(panel, message, type) {
|
|
var box = document.getElementById('aksrt-modal-' + panel + '-error');
|
|
if (!box) {
|
|
return;
|
|
}
|
|
|
|
box.innerHTML = message;
|
|
box.hidden = false;
|
|
box.classList.toggle('is-success', type === 'success');
|
|
}
|
|
|
|
function clearMessages() {
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
modal.querySelectorAll('.aksrt-modal-auth__error').forEach(function (box) {
|
|
box.hidden = true;
|
|
box.innerHTML = '';
|
|
box.classList.remove('is-success');
|
|
});
|
|
}
|
|
|
|
function setLoading(form, loading) {
|
|
var submit = form.querySelector('.aksrt-modal-auth__submit');
|
|
if (!submit) {
|
|
return;
|
|
}
|
|
|
|
var label = submit.querySelector('span');
|
|
var spinner = submit.querySelector('i');
|
|
submit.disabled = loading;
|
|
|
|
if (label) {
|
|
label.hidden = loading;
|
|
}
|
|
if (spinner) {
|
|
spinner.hidden = !loading;
|
|
}
|
|
}
|
|
|
|
function escapeAttribute(value) {
|
|
return String(value || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
// ── QR Code Login (§10.7) ──
|
|
|
|
|
|
function bindWeixingzhCodeForm(container) {
|
|
if (!container) return;
|
|
var form = container.querySelector('[data-aksrt-weixingzh-code]');
|
|
if (!form || form._bound) return;
|
|
form._bound = true;
|
|
form.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
var input = form.querySelector('[name="code"]');
|
|
var code = input ? String(input.value || '').trim() : '';
|
|
if (!code) return;
|
|
var btn = form.querySelector('button[type="submit"]');
|
|
if (btn) btn.disabled = true;
|
|
request('/auth/weixingzh-code', { code: code })
|
|
.then(function (result) {
|
|
var data = (result && result.data) || result || {};
|
|
if ((result && result.ok) || (result && result.success) || data.logged_in) {
|
|
onAuthSuccess(data.data || data);
|
|
return;
|
|
}
|
|
throw new Error((result && (result.message || result.msg)) || '验证码错误');
|
|
})
|
|
.catch(function (err) {
|
|
alert(err.message || '登录失败');
|
|
})
|
|
.finally(function () {
|
|
if (btn) btn.disabled = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
function initQrLogin() {
|
|
var qrPanel = modal.querySelector('[data-panel="qrcode"]');
|
|
if (!qrPanel) return;
|
|
|
|
var qrContainer = qrPanel.querySelector('.aksrt-modal-auth__qr-code');
|
|
if (!qrContainer) return;
|
|
|
|
// When qrcode tab is activated, fetch QR and start polling
|
|
var observer = new MutationObserver(function () {
|
|
if (qrPanel.classList.contains('is-active')) {
|
|
fetchQrCode(qrContainer);
|
|
} else {
|
|
stopQrPolling();
|
|
}
|
|
});
|
|
observer.observe(qrPanel, { attributes: true, attributeFilter: ['class'] });
|
|
}
|
|
|
|
function fetchQrCode(container) {
|
|
container.innerHTML = '<div class="aksrt-modal-auth__qr-loading"><i class="fas fa-spinner fa-spin"></i></div>';
|
|
|
|
request('/auth/qrcode-login', {})
|
|
.then(function (result) {
|
|
var payload = (result && result.data) || result || {};
|
|
var qrHtml = payload.qr_html || (payload.data && payload.data.qr_html) || '';
|
|
var checkUrl = payload.check_url || (payload.data && payload.data.check_url) || '/auth/check-qrcode-login';
|
|
var token = payload.token || (payload.data && payload.data.token) || '';
|
|
if (qrHtml) {
|
|
container.innerHTML = qrHtml;
|
|
bindWeixingzhCodeForm(container);
|
|
if (checkUrl && token) {
|
|
startQrPolling(checkUrl, token);
|
|
}
|
|
} else {
|
|
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">扫码登录暂不可用</p>';
|
|
}
|
|
})
|
|
.catch(function () {
|
|
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">网络错误</p>';
|
|
});
|
|
}
|
|
|
|
function startQrPolling(checkUrl, token) {
|
|
stopQrPolling();
|
|
qrLoginTimer = setInterval(function () {
|
|
request(checkUrl, { token: token })
|
|
.then(function (result) {
|
|
var data = (result && result.data) || result || {};
|
|
var body = data.data || data;
|
|
if ((result && result.ok) || data.success || body.logged_in) {
|
|
if (body.logged_in || (data.data && data.data.logged_in)) {
|
|
stopQrPolling();
|
|
onAuthSuccess(body.data || body);
|
|
}
|
|
}
|
|
})
|
|
.catch(function () { /* keep polling */ });
|
|
}, 2000);
|
|
}
|
|
|
|
function stopQrPolling() {
|
|
if (qrLoginTimer) {
|
|
clearInterval(qrLoginTimer);
|
|
qrLoginTimer = null;
|
|
}
|
|
}
|
|
|
|
// ── Social Login Priority (§10.7) ──
|
|
|
|
function initSocialPriority() {
|
|
var wxPriority = window.aksrtData && aksrtData.signinWxPriority;
|
|
if (wxPriority && modal) {
|
|
var wxBtn = modal.querySelector('.social-login-item.weixingzh, .social-login-item[data-provider="wechat"]');
|
|
if (wxBtn) {
|
|
setTimeout(function () { wxBtn.click(); }, 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Agreement Check: shake instead of disable (§10.7) ──
|
|
|
|
function initAgreementShake() {
|
|
if (!modal) return;
|
|
modal.querySelectorAll('.aksrt-modal-auth__submit').forEach(function (btn) {
|
|
btn.addEventListener('click', function (e) {
|
|
var form = btn.closest('form');
|
|
if (!form) return;
|
|
var agree = form.querySelector('[name="agree_terms"], [name="user_agreement"]');
|
|
if (agree && !agree.checked) {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
agree.closest('label, .aksrt-modal-auth__agreement').classList.add('ani', 'shake');
|
|
notyf('请先同意用户协议', 'warning');
|
|
setTimeout(function () {
|
|
agree.closest('label, .aksrt-modal-auth__agreement').classList.remove('ani', 'shake');
|
|
}, 400);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Password Visibility: unified (§10.7) ──
|
|
// Already handled above; added unification note.
|
|
|
|
window.AksrtAuth = {
|
|
open: open,
|
|
close: close,
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|