mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-18 21:47:17 +02:00
fix: explain password validation errors and support login preview
This commit is contained in:
1 parent
8550325dfe
commit
05d7d9ec65
10 files changed
+203
-14
No files matched your search
@@ -68,3 +68,9 @@
|
||||
- Excel 第 11 行:当前 HTML 的 `fact-modal-summary` 标注“摘要(索引一行)”,`saveFactModal` 读取 summary 并必填检查,已存在原描述称缺失的字段。第 16 行是跨项目去重建议,当前 `list_vulnerabilities` 按授权项目/会话列举以避免重复,不新增跨项目合并。第 17 行原记录已注明没有点击保存;当前 tool-guard 草稿变更有未保存标识,只有保存动作 PUT 才改变服务端配置。
|
||||
- Excel 第 57 行:执行当前 `resetAuditLogFilters` 与查询参数构建函数,验证关键词、类别、动作、结果、时间条件全部清空,页码回到 1,模板按钮调用此函数。原描述在当前实现未复现。
|
||||
- 验证:新增工作流保存/API 校验、纯文本与 HITL 实际运行、实际工具参数传递、列表标记和审计重置回归;后端完整测试及新增前端测试在提交前执行。
|
||||
|
||||
## 追加:密码表单反馈与传输核对
|
||||
|
||||
- Excel 第 3、6 行:登录空密码通过自定义校验展示原因并聚焦字段,编辑后清除旧错误;显示/隐藏密码按钮在关闭或重新打开登录时恢复隐藏。修改密码显示当前密码必填、新密码长度与确认不一致的具体原因,服务端失败也保留在表单中。
|
||||
- 四项真实脚本回归通过;隔离预览使用实际模板片段与样式,在 375px 登录布局和桌面密码表单中验证提示、焦点和按钮。预览未使用真实账号凭据。
|
||||
- Excel 第 4 行:HTTP 下请求体可被截获,属于部署传输配置风险。当前服务已有 TLS、HTTP 到 HTTPS 重定向和同端口协议分流;现有 TLS/重定向测试通过。应在部署中启用 HTTPS,未加入额外客户端加密协议或改动凭据配置。
|
||||
@@ -5,10 +5,10 @@
|
||||
| Excel 行 | 序号 | 问题摘要 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 2 | 1 | 对话页面显示错乱了,按钮和文字位置全乱了, | 待核对 |
|
||||
| 3 | 2 | 登录时没填密码也能提交,但提示信息和样式可以在优化下 | 待核对 |
|
||||
| 4 | 3 | 登录密码是明文传输的,容易被截获有安全风险,建议参考TDP的加密方案处理 | 待核对 |
|
||||
| 3 | 2 | 登录时没填密码也能提交,但提示信息和样式可以在优化下 | 已修复;空值反馈、字段原因及浏览器布局验证通过 |
|
||||
| 4 | 3 | 登录密码是明文传输的,容易被截获有安全风险,建议参考TDP的加密方案处理 | HTTP 部署存在传输风险;现有 HTTPS 与 HTTP 重定向已实现并通过测试,需启用 TLS |
|
||||
| 5 | 4 | 登录页用户名字段未做必填校验,提交时 username 字段值为空字符串,应增加非空验证 | 已修复并有针对性验证 |
|
||||
| 6 | 5 | 修改密码,只有红框提示,没有具体错误原因 | 待核对 |
|
||||
| 6 | 5 | 修改密码,只有红框提示,没有具体错误原因 | 已修复;空值反馈、字段原因及浏览器布局验证通过 |
|
||||
| 7 | 6 | 通知,点击查看后跳转到页面,通知的计数还在,只有点了已读在不在。 | 已修复并有针对性验证 |
|
||||
| 8 | 7 | 新增资产,自动识别出来的不是中文呢,好像是时区的问题。 | 待核对 |
|
||||
| 9 | 8 | 接上一个,改成中国后,省份还是拼音 | 待核对 |
|
||||
|
||||
@@ -6550,6 +6550,33 @@ html[data-theme="dark"] .chat-input-container .send-btn:disabled::before {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-password-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-password-field input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.login-password-field button {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.password-field-error {
|
||||
color: var(--error-color, #dc3545);
|
||||
margin-top: 6px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.login-form input[aria-invalid="true"],
|
||||
.login-form input[aria-invalid="true"]:focus {
|
||||
border-color: var(--error-color, #dc3545);
|
||||
}
|
||||
|
||||
.login-form .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"itemDefaultTitle": "Notification"
|
||||
},
|
||||
"login": {
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"title": "Sign in to CyberStrikeAI",
|
||||
"titlePrefix": "Sign in to",
|
||||
"subtitle": "Sign in with a platform account. The default administrator is admin.",
|
||||
@@ -3342,6 +3344,9 @@
|
||||
"detailSession": "Session"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"currentRequired": "Enter your current password",
|
||||
"newTooShort": "The new password must contain at least 8 characters",
|
||||
"confirmMismatch": "The new passwords do not match",
|
||||
"changePasswordTitle": "Change password",
|
||||
"changePasswordDesc": "After changing password, sign in again with the new password.",
|
||||
"currentPassword": "Current password",
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"itemDefaultTitle": "Уведомление"
|
||||
},
|
||||
"login": {
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"title": "Вход в CyberStrikeAI",
|
||||
"titlePrefix": "Вход в",
|
||||
"subtitle": "Войдите с учётной записью платформы. Администратор по умолчанию — admin.",
|
||||
@@ -3355,6 +3357,9 @@
|
||||
"detailSession": "Сессия"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"currentRequired": "Введите текущий пароль",
|
||||
"newTooShort": "Новый пароль должен содержать не менее 8 символов",
|
||||
"confirmMismatch": "Новые пароли не совпадают",
|
||||
"changePasswordTitle": "Сменить пароль",
|
||||
"changePasswordDesc": "После смены пароля войдите снова с новым паролем.",
|
||||
"currentPassword": "Текущий пароль",
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"itemDefaultTitle": "通知"
|
||||
},
|
||||
"login": {
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"title": "登录 CyberStrikeAI",
|
||||
"titlePrefix": "登录",
|
||||
"subtitle": "使用平台账号登录,默认管理员为 admin。",
|
||||
@@ -3330,6 +3332,9 @@
|
||||
"detailSession": "会话"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"currentRequired": "请输入当前密码",
|
||||
"newTooShort": "新密码至少需要 8 位",
|
||||
"confirmMismatch": "两次输入的新密码不一致",
|
||||
"changePasswordTitle": "修改密码",
|
||||
"changePasswordDesc": "修改登录密码后,需要使用新密码重新登录。",
|
||||
"currentPassword": "当前密码",
|
||||
|
||||
@@ -92,7 +92,25 @@ function resolveAuthPromises(success) {
|
||||
authPromise = null;
|
||||
}
|
||||
|
||||
function setLoginPasswordVisible(visible) {
|
||||
const input = document.getElementById('login-password');
|
||||
const button = document.getElementById('login-password-toggle');
|
||||
if (input) input.type = visible ? 'text' : 'password';
|
||||
if (button) {
|
||||
button.setAttribute('aria-pressed', String(visible));
|
||||
const key = visible ? 'login.hidePassword' : 'login.showPassword';
|
||||
button.setAttribute('data-i18n', key);
|
||||
button.textContent = typeof window.t === 'function' ? window.t(key) : (visible ? '隐藏密码' : '显示密码');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLoginPasswordVisibility() {
|
||||
const input = document.getElementById('login-password');
|
||||
setLoginPasswordVisible(input && input.type === 'password');
|
||||
}
|
||||
|
||||
function showLoginOverlay(message = '') {
|
||||
setLoginPasswordVisible(false);
|
||||
const overlay = document.getElementById('login-overlay');
|
||||
const errorBox = document.getElementById('login-error');
|
||||
const usernameInput = document.getElementById('login-username');
|
||||
@@ -120,6 +138,7 @@ function showLoginOverlay(message = '') {
|
||||
}
|
||||
|
||||
function hideLoginOverlay() {
|
||||
setLoginPasswordVisible(false);
|
||||
const overlay = document.getElementById('login-overlay');
|
||||
const errorBox = document.getElementById('login-error');
|
||||
const usernameInput = document.getElementById('login-username');
|
||||
@@ -131,7 +150,9 @@ function hideLoginOverlay() {
|
||||
}
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
passwordInput.setAttribute('aria-invalid', 'false');
|
||||
}
|
||||
if (usernameInput) usernameInput.setAttribute('aria-invalid', 'false');
|
||||
if (usernameInput && !authUser) {
|
||||
usernameInput.value = '';
|
||||
}
|
||||
@@ -244,6 +265,14 @@ async function apiUploadWithProgress(url, formData, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearLoginFormError(event) {
|
||||
const input = event.target;
|
||||
if (!input || (input.id !== 'login-username' && input.id !== 'login-password')) return;
|
||||
input.setAttribute('aria-invalid', 'false');
|
||||
const error = document.getElementById('login-error');
|
||||
if (error) { error.textContent = ''; error.style.display = 'none'; }
|
||||
}
|
||||
|
||||
async function submitLogin(event) {
|
||||
event.preventDefault();
|
||||
const usernameInput = document.getElementById('login-username');
|
||||
@@ -251,6 +280,8 @@ async function submitLogin(event) {
|
||||
const errorBox = document.getElementById('login-error');
|
||||
const submitBtn = document.querySelector('.login-submit');
|
||||
|
||||
[usernameInput, passwordInput].forEach(input => input && input.setAttribute('aria-invalid', 'false'));
|
||||
|
||||
if (!passwordInput) {
|
||||
return;
|
||||
}
|
||||
@@ -258,6 +289,7 @@ async function submitLogin(event) {
|
||||
const username = usernameInput ? usernameInput.value.trim() : '';
|
||||
const password = passwordInput.value.trim();
|
||||
if (!username || Array.from(username).length > 64) {
|
||||
if (usernameInput) usernameInput.setAttribute('aria-invalid', 'true');
|
||||
if (errorBox) {
|
||||
errorBox.textContent = typeof window.t === 'function' ? window.t('auth.usernameRequired') : '用户名不能为空,且不能超过 64 个字符';
|
||||
errorBox.style.display = 'block';
|
||||
@@ -266,6 +298,7 @@ async function submitLogin(event) {
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
passwordInput.setAttribute('aria-invalid', 'true');
|
||||
if (errorBox) {
|
||||
const msgEmpty = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('auth.enterPassword')
|
||||
@@ -273,6 +306,7 @@ async function submitLogin(event) {
|
||||
errorBox.textContent = msgEmpty;
|
||||
errorBox.style.display = 'block';
|
||||
}
|
||||
passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -683,6 +717,7 @@ function setupLoginUI() {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', submitLogin);
|
||||
loginForm.addEventListener('input', clearLoginFormError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,6 +994,7 @@ window.generateRobotBindingCode = generateRobotBindingCode;
|
||||
window.copyRobotBindingCode = copyRobotBindingCode;
|
||||
window.deleteRobotAccountBinding = deleteRobotAccountBinding;
|
||||
window.logout = logout;
|
||||
window.toggleLoginPasswordVisibility = toggleLoginPasswordVisibility;
|
||||
window.hasPermission = hasPermission;
|
||||
window.hasAnyPermission = hasAnyPermission;
|
||||
window.readApiError = readApiError;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
const fs=require('node:fs');
|
||||
const vm=require('node:vm');
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const auth=fs.readFileSync('web/static/js/auth.js','utf8');
|
||||
const settings=fs.readFileSync('web/static/js/settings.js','utf8');
|
||||
function input(id,value='') {
|
||||
const attrs={};
|
||||
return {id,value,type:'password',style:{},classList:{toggle(){},remove(){}},focus(){this.focused=true;},setAttribute(k,v){attrs[k]=v;},getAttribute:k => attrs[k]};
|
||||
}
|
||||
function harness(values={}) {
|
||||
const fields={};
|
||||
for (const id of ['auth-current-password','auth-new-password','auth-confirm-password','login-username','login-password']) fields[id]=input(id,values[id]||'');
|
||||
for (const id of ['auth-current-password-error','auth-new-password-error','auth-confirm-password-error','password-form-error','login-error','login-password-toggle']) fields[id]=input(id);
|
||||
const button={disabled:false};let calls=0;
|
||||
const ctx={document:{getElementById:id => fields[id]||null,querySelector:() => button},window:{t:x => x},alert(){},console:{error(){}},
|
||||
fetch:async () => {calls++;throw Error('unexpected login request');},
|
||||
apiFetch:async () => {calls++;return {ok:false,json:async () => ({error:'test-only incorrect current password'})};}};
|
||||
vm.createContext(ctx);
|
||||
let start=auth.indexOf('function setLoginPasswordVisible(');
|
||||
vm.runInContext(auth.slice(start,auth.indexOf('function showLoginOverlay(',start)),ctx);
|
||||
start=auth.indexOf('function clearLoginFormError(');
|
||||
vm.runInContext(auth.slice(start,auth.indexOf('\nfunction ',start)),ctx);
|
||||
start=settings.indexOf('function setPasswordFieldError(');
|
||||
vm.runInContext(settings.slice(start,settings.indexOf('// ==================== 外部MCP管理',start)),ctx);
|
||||
return {ctx,fields,button,calls:() => calls};
|
||||
}
|
||||
test('empty login password explains the error and never calls the server',async () => {
|
||||
const h=harness({'login-username':'admin'});
|
||||
await h.ctx.submitLogin({preventDefault(){}});
|
||||
assert.equal(h.calls(),0);
|
||||
assert.equal(h.fields['login-error'].textContent,'auth.enterPassword');
|
||||
assert.equal(h.fields['login-password'].getAttribute('aria-invalid'),'true');
|
||||
assert.equal(h.fields['login-password'].focused,true);
|
||||
h.ctx.clearLoginFormError({target:h.fields['login-password']});
|
||||
assert.equal(h.fields['login-error'].style.display,'none');
|
||||
assert.equal(h.fields['login-password'].getAttribute('aria-invalid'),'false');
|
||||
assert.match(fs.readFileSync('web/templates/index.html','utf8'),/<form id="login-form"[^>]*novalidate/);
|
||||
});
|
||||
test('password preview toggles accessibly and can be reset without changing the value',() => {
|
||||
const h=harness({'login-password':'test-only-password'});
|
||||
h.ctx.toggleLoginPasswordVisibility();
|
||||
assert.equal(h.fields['login-password'].type,'text');
|
||||
assert.equal(h.fields['login-password-toggle'].getAttribute('aria-pressed'),'true');
|
||||
h.ctx.setLoginPasswordVisible(false);
|
||||
assert.equal(h.fields['login-password'].type,'password');
|
||||
assert.equal(h.fields['login-password'].value,'test-only-password');
|
||||
assert.equal(h.fields['login-password-toggle'].getAttribute('data-i18n'),'login.showPassword');
|
||||
});
|
||||
test('password form has field-specific errors without requesting a save',async () => {
|
||||
const h=harness({'auth-new-password':'short','auth-confirm-password':'different'});
|
||||
await h.ctx.changePassword();
|
||||
assert.equal(h.calls(),0);
|
||||
assert.equal(h.fields['auth-current-password-error'].textContent,'settingsSecurity.currentRequired');
|
||||
assert.equal(h.fields['auth-new-password-error'].textContent,'settingsSecurity.newTooShort');
|
||||
assert.equal(h.fields['auth-confirm-password-error'].textContent,'settingsSecurity.confirmMismatch');
|
||||
assert.equal(h.fields['auth-current-password'].focused,true);
|
||||
h.ctx.resetPasswordForm();
|
||||
for (const id of ['auth-current-password','auth-new-password','auth-confirm-password']) {
|
||||
assert.equal(h.fields[id].getAttribute('aria-invalid'),'false');
|
||||
assert.equal(h.fields[id+'-error'].hidden,true);
|
||||
}
|
||||
});
|
||||
test('server password failure is visible in the form and restores the save button',async () => {
|
||||
const h=harness({'auth-current-password':'test-only-old','auth-new-password':'test-only-new','auth-confirm-password':'test-only-new'});
|
||||
await h.ctx.changePassword();
|
||||
assert.equal(h.calls(),1);
|
||||
assert.equal(h.fields['password-form-error'].hidden,false);
|
||||
assert.equal(h.fields['password-form-error'].textContent,'test-only incorrect current password');
|
||||
assert.equal(h.button.disabled,false);
|
||||
});
|
||||
@@ -4062,6 +4062,28 @@ async function saveToolsConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function setPasswordFieldError(input, message) {
|
||||
if (!input) return;
|
||||
input.classList.toggle('error', !!message);
|
||||
input.setAttribute('aria-invalid', message ? 'true' : 'false');
|
||||
const error = document.getElementById(input.id + '-error');
|
||||
if (error) {
|
||||
error.textContent = message || '';
|
||||
error.hidden = !message;
|
||||
input.setAttribute('aria-describedby', error.id);
|
||||
}
|
||||
}
|
||||
|
||||
function passwordValidationText(key, fallback) {
|
||||
return typeof window.t === 'function' ? window.t('settingsSecurity.' + key) : fallback;
|
||||
}
|
||||
|
||||
function clearPasswordFieldError(input) {
|
||||
setPasswordFieldError(input, '');
|
||||
const error = document.getElementById('password-form-error');
|
||||
if (error) { error.textContent = ''; error.hidden = true; }
|
||||
}
|
||||
|
||||
function resetPasswordForm() {
|
||||
const currentInput = document.getElementById('auth-current-password');
|
||||
const newInput = document.getElementById('auth-new-password');
|
||||
@@ -4070,9 +4092,11 @@ function resetPasswordForm() {
|
||||
[currentInput, newInput, confirmInput].forEach(input => {
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.classList.remove('error');
|
||||
setPasswordFieldError(input, '');
|
||||
}
|
||||
});
|
||||
const error = document.getElementById('password-form-error');
|
||||
if (error) { error.textContent = ''; error.hidden = true; }
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
@@ -4081,7 +4105,9 @@ async function changePassword() {
|
||||
const confirmInput = document.getElementById('auth-confirm-password');
|
||||
const submitBtn = document.querySelector('.change-password-submit');
|
||||
|
||||
[currentInput, newInput, confirmInput].forEach(input => input && input.classList.remove('error'));
|
||||
[currentInput, newInput, confirmInput].forEach(input => setPasswordFieldError(input, ''));
|
||||
const formError = document.getElementById('password-form-error');
|
||||
if (formError) { formError.textContent = ''; formError.hidden = true; }
|
||||
|
||||
const currentPassword = currentInput?.value.trim() || '';
|
||||
const newPassword = newInput?.value.trim() || '';
|
||||
@@ -4090,22 +4116,22 @@ async function changePassword() {
|
||||
let hasError = false;
|
||||
|
||||
if (!currentPassword) {
|
||||
currentInput?.classList.add('error');
|
||||
setPasswordFieldError(currentInput, passwordValidationText('currentRequired', '请输入当前密码'));
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length < 8) {
|
||||
newInput?.classList.add('error');
|
||||
setPasswordFieldError(newInput, passwordValidationText('newTooShort', '新密码至少需要 8 位'));
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
confirmInput?.classList.add('error');
|
||||
setPasswordFieldError(confirmInput, passwordValidationText('confirmMismatch', '两次输入的新密码不一致'));
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
alert(typeof window.t === 'function' ? window.t('settings.security.fillPasswordHint') : '请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
|
||||
[currentInput, newInput, confirmInput].find(input => input && input.getAttribute('aria-invalid') === 'true')?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4137,6 +4163,7 @@ async function changePassword() {
|
||||
closeSettings();
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
if (formError) { formError.textContent = error.message; formError.hidden = false; }
|
||||
alert((typeof window.t === 'function' ? window.t('settings.security.changePasswordFailed') : '修改密码失败') + ': ' + error.message);
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
|
||||
@@ -50,14 +50,17 @@
|
||||
</h2>
|
||||
<p class="login-subtitle" data-i18n="login.subtitle">使用平台账号登录,默认管理员为 admin。</p>
|
||||
</div>
|
||||
<form id="login-form" class="login-form">
|
||||
<form id="login-form" class="login-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="login-username" data-i18n="login.usernameLabel">用户名</label>
|
||||
<input type="text" id="login-username" placeholder="admin" required maxlength="64" autocomplete="username" data-i18n="login.usernamePlaceholder" data-i18n-attr="placeholder" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password" data-i18n="login.passwordLabel">密码</label>
|
||||
<input type="password" id="login-password" placeholder="输入登录密码" required autocomplete="current-password" data-i18n="login.passwordPlaceholder" data-i18n-attr="placeholder" />
|
||||
<div class="login-password-field">
|
||||
<input type="password" id="login-password" placeholder="输入登录密码" required autocomplete="current-password" aria-describedby="login-error" data-i18n="login.passwordPlaceholder" data-i18n-attr="placeholder" />
|
||||
<button type="button" id="login-password-toggle" class="btn-secondary" aria-controls="login-password" aria-pressed="false" onclick="toggleLoginPasswordVisibility()" data-i18n="login.showPassword">显示密码</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="login-error" class="login-error" role="alert" style="display: none;"></div>
|
||||
<button type="submit" class="btn-primary login-submit">
|
||||
@@ -5069,16 +5072,20 @@
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="auth-current-password" data-i18n="settingsSecurity.currentPassword">当前密码</label>
|
||||
<input type="password" id="auth-current-password" data-i18n="settingsSecurity.currentPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="输入当前登录密码" autocomplete="current-password" />
|
||||
<input type="password" id="auth-current-password" oninput="clearPasswordFieldError(this)" data-i18n="settingsSecurity.currentPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="输入当前登录密码" autocomplete="current-password" />
|
||||
<small id="auth-current-password-error" class="password-field-error" role="alert" hidden></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="auth-new-password" data-i18n="settingsSecurity.newPassword">新密码</label>
|
||||
<input type="password" id="auth-new-password" data-i18n="settingsSecurity.newPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="设置新密码(至少 8 位)" autocomplete="new-password" />
|
||||
<input type="password" id="auth-new-password" oninput="clearPasswordFieldError(this)" data-i18n="settingsSecurity.newPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="设置新密码(至少 8 位)" autocomplete="new-password" />
|
||||
<small id="auth-new-password-error" class="password-field-error" role="alert" hidden></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="auth-confirm-password" data-i18n="settingsSecurity.confirmPassword">确认新密码</label>
|
||||
<input type="password" id="auth-confirm-password" data-i18n="settingsSecurity.confirmPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="再次输入新密码" autocomplete="new-password" />
|
||||
<input type="password" id="auth-confirm-password" oninput="clearPasswordFieldError(this)" data-i18n="settingsSecurity.confirmPasswordPlaceholder" data-i18n-attr="placeholder" placeholder="再次输入新密码" autocomplete="new-password" />
|
||||
<small id="auth-confirm-password-error" class="password-field-error" role="alert" hidden></small>
|
||||
</div>
|
||||
<div id="password-form-error" class="password-field-error" role="alert" hidden></div>
|
||||
<div class="form-actions">
|
||||
<button class="btn-secondary" type="button" onclick="resetPasswordForm()" data-i18n="settingsSecurity.clear">清空</button>
|
||||
<button class="btn-primary change-password-submit" type="button" data-require-permission="auth:self" onclick="changePassword()" data-i18n="settingsSecurity.changePasswordBtn">修改密码</button>
|
||||
|
||||
Reference in new issue
Block a user