fix: simplify account password flow
This commit is contained in:
1 parent
203bfb3246
commit
df65e9f517
9 files changed
+118
-131
No files matched your search
@@ -32,17 +32,18 @@ function loadNamedFunction(name) {
|
||||
return context.loaded;
|
||||
}
|
||||
|
||||
test('account creation rejects a short password before sending the request', () => {
|
||||
test('account creation accepts any non-empty password and blocks an empty password before the request', () => {
|
||||
const validate = loadNamedFunction('validateAccountCreationValues');
|
||||
const result = validate({
|
||||
const empty = validate({
|
||||
username: 'operator',
|
||||
password: '12345678901',
|
||||
role: 'user',
|
||||
mustChangePassword: true
|
||||
password: '',
|
||||
role: 'user'
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.field, 'accountPassword');
|
||||
assert.equal(result.message, '初始密码必须为 12—512 个字符。');
|
||||
assert.equal(empty.ok, false);
|
||||
assert.equal(empty.field, 'accountPassword');
|
||||
assert.equal(empty.message, '请输入初始密码。');
|
||||
const oneCharacter = validate({ username: 'operator', password: '1', role: 'user' });
|
||||
assert.equal(oneCharacter.ok, true);
|
||||
const createStart = appSource.indexOf('async function createAccountFromForm()');
|
||||
const createEnd = appSource.indexOf('\n}\n\nasync function updateManagedAccount', createStart);
|
||||
assert.notEqual(createStart, -1);
|
||||
@@ -56,15 +57,14 @@ test('account creation normalizes valid form values into the server contract', (
|
||||
const validate = loadNamedFunction('validateAccountCreationValues');
|
||||
const result = validate({
|
||||
username: ' TeamLead ',
|
||||
password: 'correct-horse-battery-staple',
|
||||
role: 'team_lead',
|
||||
mustChangePassword: true
|
||||
password: '123456',
|
||||
role: 'team_lead'
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.body.username, 'TeamLead');
|
||||
assert.equal(result.body.password, 'correct-horse-battery-staple');
|
||||
assert.equal(result.body.password, '123456');
|
||||
assert.equal(result.body.role, 'team_lead');
|
||||
assert.equal(result.body.must_change_password, true);
|
||||
assert.equal(Object.hasOwn(result.body, 'must_change_password'), false);
|
||||
assert.equal(Array.isArray(result.body.business_route_ids), true);
|
||||
assert.equal(result.body.business_route_ids.length, 0);
|
||||
});
|
||||
@@ -76,10 +76,11 @@ test('account creation translates backend password validation into an actionable
|
||||
details: ['password'],
|
||||
message: '请求参数不符合要求。'
|
||||
});
|
||||
assert.equal(message, '初始密码必须为 12—512 个字符。');
|
||||
assert.equal(message, '请输入初始密码。');
|
||||
});
|
||||
|
||||
test('account form states the password requirement beside the field', () => {
|
||||
assert.match(indexSource, /初始密码(12—512 个字符)<input id="accountPassword"/u);
|
||||
assert.match(indexSource, /id="accountPassword"[^>]+minlength="12"[^>]+maxlength="512"/u);
|
||||
test('account forms expose no length rule or first-login forced-password flow', () => {
|
||||
assert.match(indexSource, /初始密码<input id="accountPassword"[^>]+required>/u);
|
||||
assert.doesNotMatch(indexSource, /accountMustChangePassword|首次登录必须修改密码|minlength="12"|12—512/u);
|
||||
assert.doesNotMatch(appSource, /passwordChangeForced|must_change_password/u);
|
||||
});
|
||||
+23
-26
@@ -36,7 +36,6 @@ let remoteSyncInProgress = false;
|
||||
let syncRequested = false;
|
||||
let csrfToken = '';
|
||||
let authUser = null;
|
||||
let passwordChangeForced = false;
|
||||
let browserConnectionId = '';
|
||||
let organizationAutomationEnabled = false;
|
||||
let automationSettingsLoaded = false;
|
||||
@@ -360,7 +359,6 @@ function showAuthChecking() {
|
||||
|
||||
function showAuthenticatedApp(user) {
|
||||
authUser = user;
|
||||
passwordChangeForced = Boolean(user?.must_change_password);
|
||||
browserConnectionId = connectionIdForUser(user);
|
||||
if (!isAdministrator() && IS_ADMIN_PAGE) {
|
||||
window.location.replace('/');
|
||||
@@ -410,23 +408,19 @@ function showAuthenticatedApp(user) {
|
||||
}
|
||||
const submitButton = $('#loginForm button[type="submit"]');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
if (user.must_change_password) showPasswordChangePanel(true);
|
||||
}
|
||||
|
||||
function showPasswordChangePanel(forced = false) {
|
||||
function showPasswordChangePanel() {
|
||||
if (!authUser) return;
|
||||
passwordChangeForced = forced;
|
||||
for (const selector of ['#loginPanel', '#workbench', '#channelsPage', '#parserRoutingPage', '#accountsPage', '#auditPage', '#operationsDashboardPage']) {
|
||||
const page = $(selector);
|
||||
if (page) page.hidden = true;
|
||||
}
|
||||
const panel = $('#passwordChangePanel');
|
||||
if (panel) panel.hidden = false;
|
||||
$('#passwordChangeTitle').textContent = forced ? '请先设置新密码' : '修改密码';
|
||||
$('#passwordChangeHint').textContent = forced
|
||||
? '管理员已重置你的密码。继续使用平台前,请设置至少 12 个字符的新密码。'
|
||||
: '新密码至少 12 个字符。修改后,其他已登录会话将失效。';
|
||||
$('#passwordChangeCancel').hidden = forced;
|
||||
$('#passwordChangeTitle').textContent = '修改密码';
|
||||
$('#passwordChangeHint').textContent = '修改后,其他已登录会话将失效。';
|
||||
$('#passwordChangeCancel').hidden = false;
|
||||
$('#passwordChangeError').textContent = '';
|
||||
$('#changePasswordButton').hidden = true;
|
||||
$('#automationToggleButton').hidden = true;
|
||||
@@ -435,7 +429,7 @@ function showPasswordChangePanel(forced = false) {
|
||||
function renderAutomationToggle() {
|
||||
const button = $('#automationToggleButton');
|
||||
if (!button) return;
|
||||
button.hidden = !isAdministrator() || IS_MANAGEMENT_PAGE || passwordChangeForced;
|
||||
button.hidden = !isAdministrator() || IS_MANAGEMENT_PAGE;
|
||||
button.disabled = !authUser || automationSettingsBusy || !automationSettingsLoaded || Boolean(automationSettingsError);
|
||||
button.setAttribute('aria-pressed', organizationAutomationEnabled ? 'true' : 'false');
|
||||
button.classList.toggle('is-enabled', organizationAutomationEnabled);
|
||||
@@ -902,8 +896,8 @@ function validateAccountCreationValues(values = {}) {
|
||||
if (!username || username.length > 160) {
|
||||
return { ok: false, field: 'accountUsername', message: '账号必须为 1—160 个字符。' };
|
||||
}
|
||||
if (password.length < 12 || password.length > 512) {
|
||||
return { ok: false, field: 'accountPassword', message: '初始密码必须为 12—512 个字符。' };
|
||||
if (!password) {
|
||||
return { ok: false, field: 'accountPassword', message: '请输入初始密码。' };
|
||||
}
|
||||
if (!['admin', 'team_lead', 'user'].includes(role)) {
|
||||
return { ok: false, field: 'accountRole', message: '请选择有效的账号角色。' };
|
||||
@@ -914,7 +908,6 @@ function validateAccountCreationValues(values = {}) {
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
must_change_password: values.mustChangePassword === true,
|
||||
business_route_ids: []
|
||||
}
|
||||
};
|
||||
@@ -924,7 +917,7 @@ function accountCreationErrorMessage(error) {
|
||||
const details = Array.isArray(error?.details) ? error.details.map((item) => String(item || '')) : [];
|
||||
if (error?.errorCode === 'invalid_request') {
|
||||
if (details.some((path) => path === 'password' || path.startsWith('password.'))) {
|
||||
return '初始密码必须为 12—512 个字符。';
|
||||
return '请输入初始密码。';
|
||||
}
|
||||
if (details.some((path) => path === 'username' || path.startsWith('username.'))) {
|
||||
return '账号必须为 1—160 个字符。';
|
||||
@@ -970,7 +963,7 @@ function renderAccounts() {
|
||||
heading.append(el('strong', '', account.username));
|
||||
heading.append(el('span', `state ${account.is_active ? 'state-ok' : 'state-bad'}`, account.is_active ? '有效' : '已停用'));
|
||||
main.append(heading);
|
||||
main.append(el('p', 'muted', `${accountRoleLabel(account.role)}${account.must_change_password ? ' · 待修改密码' : ''}`));
|
||||
main.append(el('p', 'muted', accountRoleLabel(account.role)));
|
||||
const authorizedCount = Array.isArray(account.authorized_business_route_ids)
|
||||
? account.authorized_business_route_ids.length
|
||||
: 0;
|
||||
@@ -1112,8 +1105,7 @@ async function createAccountFromForm() {
|
||||
const validation = validateAccountCreationValues({
|
||||
username: $('#accountUsername').value,
|
||||
password: $('#accountPassword').value,
|
||||
role: $('#accountRole').value,
|
||||
mustChangePassword: $('#accountMustChangePassword').checked
|
||||
role: $('#accountRole').value
|
||||
});
|
||||
if (!validation.ok) {
|
||||
showAccountCreationValidationError(validation);
|
||||
@@ -1158,14 +1150,19 @@ async function updateManagedAccount(accountId, patch) {
|
||||
|
||||
async function resetManagedAccountPassword(accountId) {
|
||||
if (!isAdministrator() || accountSettingsBusy) return;
|
||||
const password = window.prompt('请输入至少 12 个字符的新密码。账号下次登录时必须修改此密码:');
|
||||
const password = window.prompt('请输入新密码:');
|
||||
if (password == null) return;
|
||||
if (!password) {
|
||||
const message = $('#accountMessage');
|
||||
if (message) message.textContent = '密码不能为空。';
|
||||
return;
|
||||
}
|
||||
accountSettingsBusy = true;
|
||||
renderAccounts();
|
||||
try {
|
||||
await apiRequest(`/api/accounts/${encodeURIComponent(accountId)}/reset-password`, {
|
||||
method: 'POST',
|
||||
body: { password, must_change_password: true }
|
||||
body: { password }
|
||||
});
|
||||
const message = $('#accountMessage');
|
||||
if (message) message.textContent = '密码已重置,该账号的现有会话已全部撤销。';
|
||||
@@ -5219,7 +5216,6 @@ async function initializeSession() {
|
||||
}
|
||||
|
||||
showAuthenticatedApp(me.user);
|
||||
if (me.user?.must_change_password) return true;
|
||||
if (isAdministrator()) {
|
||||
try {
|
||||
await syncAutomationSettings();
|
||||
@@ -5320,7 +5316,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
csrfToken = result.csrf_token || '';
|
||||
showAuthenticatedApp(result.user);
|
||||
$('#loginPassword').value = '';
|
||||
if (result.user?.must_change_password) return;
|
||||
if (isAdministrator()) {
|
||||
await syncAutomationSettings().catch(() => setTaskState('全自动化设置读取失败'));
|
||||
}
|
||||
@@ -5343,9 +5338,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
$('#changePasswordButton').addEventListener('click', () => showPasswordChangePanel(false));
|
||||
$('#changePasswordButton').addEventListener('click', showPasswordChangePanel);
|
||||
$('#passwordChangeCancel').addEventListener('click', () => {
|
||||
if (passwordChangeForced || !authUser) return;
|
||||
if (!authUser) return;
|
||||
showAuthenticatedApp(authUser);
|
||||
});
|
||||
$('#passwordChangeForm').addEventListener('submit', async (event) => {
|
||||
@@ -5354,6 +5349,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const currentPassword = String($('#currentPassword').value || '');
|
||||
const newPassword = String($('#newPassword').value || '');
|
||||
const confirmation = String($('#confirmNewPassword').value || '');
|
||||
if (!currentPassword || !newPassword) {
|
||||
errorNode.textContent = '当前密码和新密码不能为空。';
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmation) {
|
||||
errorNode.textContent = '两次输入的新密码不一致。';
|
||||
return;
|
||||
@@ -5369,7 +5368,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
$('#currentPassword').value = '';
|
||||
$('#newPassword').value = '';
|
||||
$('#confirmNewPassword').value = '';
|
||||
passwordChangeForced = false;
|
||||
await initializeSession();
|
||||
} catch (error) {
|
||||
errorNode.textContent = error.message || String(error);
|
||||
@@ -5773,7 +5771,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
void copyTaskLifecycle(button);
|
||||
});
|
||||
if (await initializeSession()) {
|
||||
if (authUser?.must_change_password) return;
|
||||
if (IS_TASK_PAGE) renderTaskCards();
|
||||
pingAi().catch(() => {});
|
||||
pingBridge().catch(() => {});
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
<div class="login-card">
|
||||
<p class="eyebrow">ACCOUNT SECURITY</p>
|
||||
<h1 id="passwordChangeTitle">修改密码</h1>
|
||||
<p id="passwordChangeHint" class="muted">新密码至少 12 个字符。修改后,其他已登录会话将失效。</p>
|
||||
<p id="passwordChangeHint" class="muted">修改后,其他已登录会话将失效。</p>
|
||||
<form id="passwordChangeForm" novalidate>
|
||||
<label>当前密码<input id="currentPassword" type="password" autocomplete="current-password" required></label>
|
||||
<label>新密码<input id="newPassword" type="password" autocomplete="new-password" minlength="12" required></label>
|
||||
<label>确认新密码<input id="confirmNewPassword" type="password" autocomplete="new-password" minlength="12" required></label>
|
||||
<label>新密码<input id="newPassword" type="password" autocomplete="new-password" required></label>
|
||||
<label>确认新密码<input id="confirmNewPassword" type="password" autocomplete="new-password" required></label>
|
||||
<div class="actions">
|
||||
<button type="submit">保存新密码</button>
|
||||
<button id="passwordChangeCancel" type="button" class="secondary-button">取消</button>
|
||||
@@ -96,9 +96,8 @@
|
||||
</div>
|
||||
<form id="accountForm" class="channel-form" novalidate>
|
||||
<label>账号<input id="accountUsername" maxlength="160" autocomplete="off" required></label>
|
||||
<label>初始密码(12—512 个字符)<input id="accountPassword" type="password" minlength="12" maxlength="512" autocomplete="new-password" required></label>
|
||||
<label>初始密码<input id="accountPassword" type="password" autocomplete="new-password" required></label>
|
||||
<label>角色<select id="accountRole"><option value="user">普通用户</option><option value="team_lead">组长</option><option value="admin">管理员</option></select></label>
|
||||
<label class="history-select-all"><input id="accountMustChangePassword" type="checkbox" checked>首次登录必须修改密码</label>
|
||||
<button type="submit">创建账号</button>
|
||||
</form>
|
||||
<section id="accountAuthorizationPanel" class="account-authorization-panel" hidden aria-labelledby="accountAuthorizationTitle">
|
||||
|
||||
Reference in new issue
Block a user