Files
jiapu/public/js/security-pages.js
T

510 lines
16 KiB
JavaScript

(function (root, factory) {
// 账号安全模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.SecurityPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.SecurityPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function confirmAction(message) {
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
return root.ProfileUI.confirmAction(message);
}
return Promise.resolve(!root['confirm'] || root['confirm'](message));
}
var documentRef = root.document;
var CAPTCHA_OPERATIONS = {
phone: 'phone-change',
deactivate: 'account-deactivate'
};
var SMS_CODE_COOLDOWN_SECONDS = 60;
var boundPhone = '';
function getCaptchaOperation(formType) {
return CAPTCHA_OPERATIONS[formType] || '';
}
function hashPassword(value) {
// 接口文档要求密码提交 32 位 MD5,测试环境可传入 hashFn 替代。
if (root.hex_md5) return root.hex_md5(value);
if (root.md5) return root.md5(value);
return value;
}
function buildPasswordChangeBody(values, hashFn) {
var source = values || {};
var hash = hashFn || hashPassword;
return {
oldPassword: hash(String(source.oldPassword || '').trim()),
newPassword: hash(String(source.newPassword || '').trim())
};
}
function buildPhoneChangeBody(values) {
var source = values || {};
return {
phone: String(source.phone || '').trim(),
smsCode: String(source.smsCode || '').trim()
};
}
function buildDeactivateBody(values) {
var source = values || {};
return {
smsCode: String(source.smsCode || '').trim()
};
}
function isDangerConfirmed(value) {
return String(value || '').trim() === '确认注销';
}
function isPhone(value) {
return /^1\d{10}$/.test(String(value || '').trim());
}
function isSmsCode(value) {
return /^\d{4}$/.test(String(value || '').trim());
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectToLogin(api) {
if (api && api.clearToken) api.clearToken();
root.NavigationUtil.open('login.html');
}
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
redirectToLogin(api);
return true;
}
async function ensureCaptcha(form, formType, subject) {
// 发送敏感业务短信前,先取得与当前认证动作绑定的验证码票据。
var captcha = root.CaptchaPages;
var operationCode = getCaptchaOperation(formType);
if (!operationCode) return true;
if (!captcha || !captcha.ensureToken) {
showMessage('验证码组件未加载,请刷新后重试');
return false;
}
return Boolean(await captcha.ensureToken(form, {
operationCode: operationCode,
subject: subject || ''
}));
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function setStatus(form, type, message) {
var status = query('[data-security-status]', form);
if (!status) return;
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
root.ProfileUI.setApiState(status, type, message);
return;
}
status.textContent = message || '';
}
function setSubmitting(form, isSubmitting) {
queryAll('button[type="submit"]', form).forEach(function (button) {
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
button.disabled = isSubmitting;
button.textContent = isSubmitting ? '提交中…' : button.dataset.defaultLabel;
});
}
function setBoundPhoneStatus(type, message) {
queryAll('[data-security-bound-phone]').forEach(function (node) {
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
root.ProfileUI.setApiState(node, type, message);
return;
}
node.textContent = message || '';
});
}
function clearCaptchaToken(form) {
var field = query('input[name="validToken"]', form);
if (field) field.value = '';
}
function takeCaptchaToken(form) {
var field = query('input[name="validToken"]', form);
var token = field && field.value || '';
if (field) field.value = '';
return token;
}
function startSmsCooldown(button) {
var originalText;
var remaining = SMS_CODE_COOLDOWN_SECONDS;
var interval;
if (!button || typeof root.setInterval !== 'function') return;
if (button.__smsCooldownTimer && typeof root.clearInterval === 'function') {
root.clearInterval(button.__smsCooldownTimer);
}
originalText = button.getAttribute('data-sms-code-label') || button.textContent.trim();
button.setAttribute('data-sms-code-label', originalText);
button.disabled = true;
button.textContent = remaining + ' 秒后重试';
interval = root.setInterval(function () {
remaining -= 1;
if (remaining > 0) {
button.textContent = remaining + ' 秒后重试';
return;
}
if (typeof root.clearInterval === 'function') root.clearInterval(interval);
button.__smsCooldownTimer = null;
button.disabled = false;
button.textContent = originalText;
}, 1000);
button.__smsCooldownTimer = interval;
}
function maskPhone(phone) {
var value = String(phone || '').trim();
return isPhone(value) ? value.slice(0, 3) + '****' + value.slice(-4) : '';
}
async function loadBoundPhone() {
var api = getApi();
var profile;
var message;
if (!api || !documentRef) return;
if (redirectUnauthorized(api)) return;
setBoundPhoneStatus('loading', '正在读取已绑定手机号…');
try {
profile = await api.currentProfile();
boundPhone = String(profile && profile.phone || '').trim();
message = isPhone(boundPhone)
? '验证码将发送至 ' + maskPhone(boundPhone)
: '未获取到已绑定手机号';
setBoundPhoneStatus('', message);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setBoundPhoneStatus('error', '读取已绑定手机号失败,请刷新后重试。');
showMessage(error.message || '账号资料加载失败');
}
}
async function submitPassword(form) {
var api = getApi();
var values = getFormValues(form);
if (redirectUnauthorized(api)) return;
if (!values.oldPassword || !values.newPassword) {
setStatus(form, 'error', '请填写当前密码和新密码');
showMessage('请填写原密码和新密码');
return;
}
if (String(values.newPassword).length < 6) {
setStatus(form, 'error', '新密码至少需要 6 位');
showMessage('新密码至少需要 6 位');
return;
}
if (values.newPassword !== values.confirmPassword) {
setStatus(form, 'error', '两次新密码不一致');
showMessage('两次新密码不一致');
return;
}
try {
setSubmitting(form, true);
setStatus(form, 'loading', '正在修改密码…');
await api.changePassword(buildPasswordChangeBody(values));
setStatus(form, '', '密码已修改');
showMessage('密码已修改');
form.reset();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(form, 'error', '修改失败,请稍后重试。');
showMessage(error.message || '密码修改失败');
} finally {
setSubmitting(form, false);
}
}
async function submitPhone(form) {
var api = getApi();
var body = buildPhoneChangeBody(getFormValues(form));
if (redirectUnauthorized(api)) return;
if (!body.phone || !body.smsCode) {
setStatus(form, 'error', '请填写新手机号和验证码');
showMessage('请填写新手机号和验证码');
return;
}
if (!isPhone(body.phone)) {
setStatus(form, 'error', '请输入正确的新手机号');
showMessage('请输入正确的新手机号');
return;
}
if (!isSmsCode(body.smsCode)) {
setStatus(form, 'error', '请输入正确的短信验证码');
showMessage('请输入正确的短信验证码');
return;
}
try {
setSubmitting(form, true);
setStatus(form, 'loading', '正在换绑手机号…');
await api.changePhone(body);
boundPhone = body.phone;
setBoundPhoneStatus('', '验证码将发送至 ' + maskPhone(boundPhone));
setStatus(form, '', '手机号已换绑');
showMessage('手机号已换绑');
form.reset();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(form, 'error', '换绑失败,请稍后重试。');
showMessage(error.message || '手机号换绑失败');
} finally {
setSubmitting(form, false);
}
}
async function sendPhoneCode(button) {
// 换绑手机号使用独立认证动作,避免复用登录或找回密码票据。
var api = getApi();
var form = query('[data-security-form="phone"]');
var values;
if (!form || redirectUnauthorized(api)) return;
values = getFormValues(form);
if (!isPhone(values.phone)) {
setStatus(form, 'error', '请输入正确的新手机号');
showMessage('请输入正确的新手机号');
return;
}
if (!await ensureCaptcha(form, 'phone', values.phone)) return;
values = getFormValues(form);
values.validToken = takeCaptchaToken(form);
button.disabled = true;
setStatus(form, 'loading', '正在发送验证码…');
try {
await api.sendSmsCode(getCaptchaOperation('phone'), {
phone: values.phone,
validToken: values.validToken || ''
});
setStatus(form, '', '验证码已发送,请查收短信。');
showMessage('验证码已发送');
startSmsCooldown(button);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(form, 'error', '验证码发送失败,请稍后重试。');
showMessage(error.message || '验证码发送失败');
button.disabled = false;
}
}
async function sendDeactivateCode(button) {
var api = getApi();
var form = query('[data-security-form="deactivate"]');
var values;
if (!form || redirectUnauthorized(api)) return;
if (!isPhone(boundPhone)) {
setStatus(form, 'error', '未获取到已绑定手机号,请刷新后重试。');
showMessage('未获取到已绑定手机号,请刷新后重试');
return;
}
if (!await ensureCaptcha(form, 'deactivate', boundPhone)) return;
values = getFormValues(form);
values.validToken = takeCaptchaToken(form);
button.disabled = true;
setStatus(form, 'loading', '正在发送验证码…');
try {
await api.sendSmsCode(getCaptchaOperation('deactivate'), {
phone: boundPhone,
validToken: values.validToken || ''
});
setStatus(form, '', '验证码已发送,请查收短信。');
showMessage('验证码已发送');
startSmsCooldown(button);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(form, 'error', '验证码发送失败,请稍后重试。');
showMessage(error.message || '验证码发送失败');
button.disabled = false;
}
}
async function submitDeactivate(form) {
var api = getApi();
var values = getFormValues(form);
if (redirectUnauthorized(api)) return;
if (!isSmsCode(values.smsCode)) {
setStatus(form, 'error', '请输入正确的短信验证码');
showMessage('请输入正确的短信验证码');
return;
}
if (!isDangerConfirmed(values.confirmText)) {
setStatus(form, 'error', '请完整输入“确认注销”');
showMessage('请完整输入“确认注销”');
return;
}
if (!await confirmAction('账号注销后将退出登录,确认继续?')) return;
try {
setSubmitting(form, true);
setStatus(form, 'loading', '正在提交注销申请…');
await api.deactivateAccount(buildDeactivateBody(values));
showMessage('账号已提交注销');
root.NavigationUtil.open('index.html');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(form, 'error', '注销失败,请稍后重试。');
showMessage(error.message || '账号注销失败');
} finally {
setSubmitting(form, false);
}
}
async function logout() {
var api = getApi();
if (!await confirmAction('确认退出当前账号?')) return;
try {
if (api) await api.logout();
} catch (error) {
// 退出请求失败时客户端仍会清理本地令牌,页面必须离开受保护区域。
} finally {
root.NavigationUtil.open('index.html');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-security-form]');
var formType;
if (!form) return;
event.preventDefault();
if (!form.checkValidity()) {
setStatus(form, 'error', '请检查表单填写是否正确');
form.reportValidity();
return;
}
formType = form.getAttribute('data-security-form');
if (formType === 'password') submitPassword(form);
if (formType === 'phone') submitPhone(form);
if (formType === 'deactivate') submitDeactivate(form);
});
documentRef.addEventListener('click', function (event) {
var codeButton = event.target.closest('[data-security-send-code]');
var deactivateCodeButton = event.target.closest('[data-security-send-deactivate-code]');
var button = event.target.closest('[data-security-logout]');
if (codeButton) {
event.preventDefault();
sendPhoneCode(codeButton);
return;
}
if (deactivateCodeButton) {
event.preventDefault();
sendDeactivateCode(deactivateCodeButton);
return;
}
if (!button) return;
event.preventDefault();
logout();
});
}
function bindCaptchaTokenInvalidation() {
queryAll('[data-security-form] input[name="phone"]').forEach(function (field) {
field.addEventListener('input', function () {
clearCaptchaToken(field.closest('[data-security-form]'));
});
});
}
function init() {
if (!documentRef || !query('[data-security-page]')) return;
if (redirectUnauthorized(getApi())) return;
bindActions();
bindCaptchaTokenInvalidation();
loadBoundPhone();
}
return {
buildPasswordChangeBody: buildPasswordChangeBody,
buildPhoneChangeBody: buildPhoneChangeBody,
buildDeactivateBody: buildDeactivateBody,
getCaptchaOperation: getCaptchaOperation,
isDangerConfirmed: isDangerConfirmed,
isPhone: isPhone,
isSmsCode: isSmsCode,
takeCaptchaToken: takeCaptchaToken,
shouldRedirectToLogin: shouldRedirectToLogin,
init: init
};
});