diff --git a/.project-docs/30-worklog/tasks/20260902-integrate-password-flow-b73c91.md b/.project-docs/30-worklog/tasks/20260902-integrate-password-flow-b73c91.md new file mode 100644 index 0000000..7518c55 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-integrate-password-flow-b73c91.md @@ -0,0 +1,57 @@ +# Task: Integrate simplified account password flow + +## Identity + +- Task ID: 20260902-integrate-password-flow-b73c91 +- Mode: Integration +- Branch: main +- Worktree: /Users/inmanx/Documents/lwltAPI +- Base commit: e530b7a3489c8f2be2f15f69de2149658aaa8ca3 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Integrate feature task `20260902-registration-invalid-params-59f94692` commits `203bfb3` and `df65e9f` into the current `main` branch after the dashboard tasks released the main worktree. +- Reconcile overlapping account UI and authorization tests while preserving the subsequently integrated dashboard layout and ranking-bar behavior. +- Run the complete repository verification gates on the merged result. +- Hand canonical promotion to a follow-on Integration Gate whose base commit already contains the source task record and supporting evidence, as required by the task-aware drift checker. + +## Intent And Constraints + +- Password entry points require a non-empty value but impose no application-level length restriction. +- Remove first-login forced password changes from the UI, API/session contract, and authorization gates; retain voluntary password change, administrator reset, and session revocation. +- Keep the historical `must_change_password` column compatibility-only and clear it on password writes rather than adding a destructive migration. +- Preserve all current dashboard changes already on `main`, account roles, owner isolation, route authorization, audit, and ERP safety boundaries. +- The user authorized merging to `main`; service restart, deployment, live account/database mutation, and ERP access remain out of scope. + +## Outcome + +- Merged feature commits `203bfb3246f70beb82f7759752d828cfc8259060` and `df65e9f5174b843de9722dfddcba172888f2b557` into the current `main` history without conflicts. +- Confirmed that the three subsequent dashboard commits and their ranking-bar UI remain present after the merge. +- Account creation, login, administrator reset, and self-service change now accept any non-empty password and impose no application-level length limit. +- Removed the first-login forced-change UI option, response/session flag, account badge, and read/mutation gates. Voluntary password change, administrator reset, session revocation, roles, owner isolation, route grants, and audit behavior remain intact. +- Kept canonical project-memory edits out of this source merge. The first drift run correctly identified that source task-owned records landed after this task's base commit; canonical promotion continues under task `20260902-promote-password-flow-c81d42` from the completed merge commit. +- No service restart, deployment, live account/database mutation, ERP access, or external delivery was performed. + +## Verification + +- Feature-worktree verification: account-form 4/4, focused authorization 8/8, repository check 10/10, control-plane 153/153, legacy 260/260, TypeScript check/build, JavaScript syntax, and diff check all passed. +- Integrated `main` `node --run check:repo`: 10/10 passed. +- Integrated `main` `node --run check`: passed. +- Integrated `main` `node --run test:control-plane`: 153/153 passed. +- Integrated `main` `node --run test:legacy`: 260/260 passed. +- Integrated `main` `node --run build`: passed. +- Integrated `main` `node --check LianSyn-platform/app.js`: passed. +- `git diff --check`: passed. +- `check_project_docs.py`: passed. +- The initial task-aware drift check correctly blocked canonical integration because the feature task record and evidence entered after this task's base commit. The check was not bypassed; source merge and canonical promotion were split so the follow-on integration task starts from the merged source commit. + +## Follow-ups + +- Continue canonical promotion under integration task `20260902-promote-password-flow-c81d42`. +- Restart or redeploy the standard service only under separate explicit authorization before relying on the new backend behavior in the running process. + +## Promotion Candidates + +- Carry the source feature task's accepted password-lifecycle candidates into integration task `20260902-promote-password-flow-c81d42`. diff --git a/.project-docs/30-worklog/tasks/20260902-registration-invalid-params-59f94692.md b/.project-docs/30-worklog/tasks/20260902-registration-invalid-params-59f94692.md new file mode 100644 index 0000000..9ded14d --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-registration-invalid-params-59f94692.md @@ -0,0 +1,70 @@ +# Task: Fix account registration invalid request parameters + +## Identity + +- Task ID: 20260902-registration-invalid-params-59f94692 +- Mode: Feature +- Branch: codex/20260902-registration-invalid-params-59f94692-registration-invalid-params-59f94692 +- Worktree: /Users/inmanx/Documents/lwltAPI-registration-invalid-params-59f94692 +- Base commit: 3ed3af1feb503358b98fb57d3e8d97ab59d98129 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Diagnose the current account-creation failure reported as “请求参数不符合要求”。 +- Correlate the operator form contract, API payload, server-side validation, privacy-safe runtime diagnostics, and account authorization tests. +- Apply the user's superseding product decision: passwords have no length restriction beyond being non-empty, and the first-login forced-password-change flow is removed. +- Update the account UI, API validation, authentication mapping, compatibility writes, documentation, and regression coverage as one coherent change. +- Do not create or change a real account, read secrets or request payloads, deploy, restart the service, or modify ERP behavior. + +## Intent And Constraints + +- Preserve the accepted fixed-scope `admin` / `team_lead` / `user` account and authorization model. +- Accept any non-empty password for login, account creation, administrator reset, and self-service password change; do not impose a minimum or maximum length in the application contract. +- Remove the first-login forced-password-change behavior while retaining voluntary self-service password changes, administrator resets, and session revocation after password changes. +- Keep the historical `must_change_password` database column as compatibility-only storage; runtime authorization and UI behavior must not depend on it, and password writes clear it to `false`. +- Use runtime diagnostics only for validation field names; do not persist account names, passwords, request bodies, or other user data. +- Reconcile this isolated feature with concurrent main-branch dashboard work only after the main worktree ownership gate is released. + +## Outcome + +- Privacy-safe diagnostics from the running standard service showed the two recent HTTP validation failures both had only `validation_paths=["password"]`; no request content was inspected. +- Confirmed the original mismatch: the API required 12–512 characters while the form submitted under `novalidate`, so a short password reached Zod validation and surfaced as the generic message. +- The initial length-guidance fix was superseded by the user's explicit direction. Account creation, reset, login, and self-service change now reject only an empty password and accept short non-empty values. +- Removed the “首次登录必须修改密码” option, forced-password-change screen state, forced route/mutation gate, response flag, and account-list badge. The normal voluntary “修改密码” control remains available. +- Existing `must_change_password` values no longer affect sessions or authorization; new account creation and password writes leave or force the compatibility column to `false`. +- Added regression assertions covering one-character account passwords, empty-password rejection, absence of length rules, and absence of the first-login forced-change contract. +- No live account, database, service process, deployment, ERP state, or external system was changed. + +## Verification + +- Focused account-form regression: 4/4 passed after the superseding product change. +- Focused account-authorization regression: 8/8 passed after the superseding product change. +- `node --check LianSyn-platform/app.js`: passed. +- `git diff --check`: passed after the final code and documentation update. +- `node --run check:repo`: 10/10 passed. +- `node --run check`: passed. +- `node --run test:control-plane`: 153/153 passed. +- `node --run test:legacy`: 260/260 passed, including the new four tests. +- `node --run build`: passed. +- `check_project_docs.py`: passed. +- `check_doc_drift.py --task-id 20260902-registration-invalid-params-59f94692`: passed. +- Verification used the bundled Node runtime and a temporary ignored `node_modules` symlink to the existing dependency tree because Node was not on the isolated shell `PATH`. + +## Follow-ups + +- Merge this isolated feature into `main` after the concurrent main-worktree task releases ownership; the user explicitly authorized the merge. +- Restart or redeploy the standard service only under separate explicit authorization before expecting backend behavior to change in the running process. + +## Promotion Candidates + +- Target documents: `10-project-memory/architecture/system-overview.md`, `10-project-memory/decisions/AUTH-001-fixed-scope-account-isolation.md`, and `20-business-memory/business-rules.md`. +- Proposed durable fact: application passwords are required to be non-empty but have no application-level length restriction; first-login forced password changes are disabled. Voluntary password change, administrator reset, and session revocation remain supported. +- Evidence: this task's focused regressions, full repository verification, and the linked privacy-safe diagnosis record. +- Future-task impact: account UI/API/schema changes must not reintroduce a length rule or `must_change_password`-based gate without a new product decision and migration plan. +- Human confirmation: explicitly provided by the user on 2026-09-02. + +## Supporting Records + +- [Account registration password validation evidence](../../50-evidence/topics/20260902-registration-invalid-params-59f94692__account-registration-password-validation.md) diff --git a/.project-docs/50-evidence/topics/20260902-registration-invalid-params-59f94692__account-registration-password-validation.md b/.project-docs/50-evidence/topics/20260902-registration-invalid-params-59f94692__account-registration-password-validation.md new file mode 100644 index 0000000..5c1c5b6 --- /dev/null +++ b/.project-docs/50-evidence/topics/20260902-registration-invalid-params-59f94692__account-registration-password-validation.md @@ -0,0 +1,34 @@ +# Account Registration Password Validation Evidence + +## Source + +- Read-only inspection on 2026-09-02 of the standard service's privacy-safe structured diagnostics. +- Active account form, API request builder, `accountCreateSchema`, authentication validation, and account authorization tests at base commit `3ed3af1feb503358b98fb57d3e8d97ab59d98129`. + +## Finding + +- The two recent `http.request.invalid` events both reported only the validation path `password`. +- At the inspected base commit, the server contract required an initial password length of 12–512 characters. +- The HTML field declared the same `minlength` and `maxlength`, but the account form used `novalidate` and the JavaScript request path did not perform its own validation before calling `/api/accounts`. +- Therefore a short initial password reached server-side Zod validation and the UI displayed the generic response “请求参数不符合要求。” instead of the actual password requirement. + +## User Decision And Resolution + +- The user explicitly superseded the original password-length contract: all password entry points now require only a non-empty value and impose no application-level minimum or maximum length. +- The user also directed removal of the first-login forced-password-change flow. The UI option, session flag, route gate, account badge, and forced panel state were removed; voluntary password changes remain available. +- The historical `must_change_password` database column is retained only for schema compatibility and is ignored by runtime authorization. Password writes clear it to `false`. +- This change was implemented and verified in an isolated feature worktree. No running service was restarted and no live account or database row was mutated. + +## Privacy And Safety + +- Only diagnostic event type and validation field paths were extracted. +- No password, account name, request body, environment file, database row, or other business/user content was read or stored. +- No live account request or runtime mutation was performed. + +## Confidence + +- High. Runtime field-path evidence and the inspected base contract identify the original mismatch; the replacement behavior is directly confirmed by the user's product decision and regression coverage. + +## Stale Trigger + +- Reassess if a future product decision introduces password policy requirements, a forced-password-change lifecycle, or a replacement for the compatibility column. diff --git a/LianSyn-platform/app-account-form.test.mjs b/LianSyn-platform/app-account-form.test.mjs new file mode 100644 index 0000000..4537997 --- /dev/null +++ b/LianSyn-platform/app-account-form.test.mjs @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import vm from 'node:vm'; + +const [appSource, indexSource] = await Promise.all([ + readFile(new URL('./app.js', import.meta.url), 'utf8'), + readFile(new URL('./index.html', import.meta.url), 'utf8') +]); + +function loadNamedFunction(name) { + const start = appSource.indexOf(`function ${name}(`); + assert.notEqual(start, -1, `${name} must exist`); + const signatureEnd = appSource.indexOf(') {', start); + assert.notEqual(signatureEnd, -1, `${name} must have a complete signature`); + const bodyStart = signatureEnd + 2; + let depth = 0; + let end = -1; + for (let index = bodyStart; index < appSource.length; index += 1) { + if (appSource[index] === '{') depth += 1; + if (appSource[index] === '}') { + depth -= 1; + if (depth === 0) { + end = index + 1; + break; + } + } + } + assert.notEqual(end, -1, `${name} must have a complete body`); + const context = {}; + vm.runInNewContext(`${appSource.slice(start, end)}; globalThis.loaded = ${name};`, context); + return context.loaded; +} + +test('account creation accepts any non-empty password and blocks an empty password before the request', () => { + const validate = loadNamedFunction('validateAccountCreationValues'); + const empty = validate({ + username: 'operator', + password: '', + role: 'user' + }); + 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); + assert.notEqual(createEnd, -1); + const createSource = appSource.slice(createStart, createEnd); + assert.ok(createSource.indexOf('if (!validation.ok)') < createSource.indexOf("apiRequest('/api/accounts'")); + assert.match(createSource, /if \(!validation\.ok\) \{[\s\S]+return;[\s\S]+apiRequest\('\/api\/accounts'/u); +}); + +test('account creation normalizes valid form values into the server contract', () => { + const validate = loadNamedFunction('validateAccountCreationValues'); + const result = validate({ + username: ' TeamLead ', + password: '123456', + role: 'team_lead' + }); + assert.equal(result.ok, true); + assert.equal(result.body.username, 'TeamLead'); + assert.equal(result.body.password, '123456'); + assert.equal(result.body.role, 'team_lead'); + 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); +}); + +test('account creation translates backend password validation into an actionable message', () => { + const messageFor = loadNamedFunction('accountCreationErrorMessage'); + const message = messageFor({ + errorCode: 'invalid_request', + details: ['password'], + message: '请求参数不符合要求。' + }); + assert.equal(message, '请输入初始密码。'); +}); + +test('account forms expose no length rule or first-login forced-password flow', () => { + assert.match(indexSource, /初始密码]+required>/u); + assert.doesNotMatch(indexSource, /accountMustChangePassword|首次登录必须修改密码|minlength="12"|12—512/u); + assert.doesNotMatch(appSource, /passwordChangeForced|must_change_password/u); +}); diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index 2d5c04a..670f263 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -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; @@ -262,7 +261,11 @@ async function apiRequest(path, options = {}) { showLoginPanel('登录已过期,请重新登录。'); } if (!response.ok) { - throw new Error(payload?.message || payload?.error || `请求失败:HTTP ${response.status}`); + const requestError = new Error(payload?.message || payload?.error || `请求失败:HTTP ${response.status}`); + requestError.status = response.status; + requestError.errorCode = payload?.error_code || ''; + requestError.details = payload?.details; + throw requestError; } return payload || {}; } @@ -356,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('/'); @@ -406,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; @@ -431,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); @@ -891,6 +889,62 @@ function accountRoleLabel(role) { return '普通用户'; } +function validateAccountCreationValues(values = {}) { + const username = String(values.username || '').trim(); + const password = String(values.password || ''); + const role = String(values.role || ''); + if (!username || username.length > 160) { + return { ok: false, field: 'accountUsername', message: '账号必须为 1—160 个字符。' }; + } + if (!password) { + return { ok: false, field: 'accountPassword', message: '请输入初始密码。' }; + } + if (!['admin', 'team_lead', 'user'].includes(role)) { + return { ok: false, field: 'accountRole', message: '请选择有效的账号角色。' }; + } + return { + ok: true, + body: { + username, + password, + role, + business_route_ids: [] + } + }; +} + +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 '请输入初始密码。'; + } + if (details.some((path) => path === 'username' || path.startsWith('username.'))) { + return '账号必须为 1—160 个字符。'; + } + if (details.some((path) => path === 'role' || path.startsWith('role.'))) { + return '请选择有效的账号角色。'; + } + } + return error?.message || String(error); +} + +function showAccountCreationValidationError(validation) { + const message = $('#accountMessage'); + if (message) message.textContent = validation.message; + const field = validation.field ? document.getElementById(validation.field) : null; + if (field) { + field.setAttribute('aria-invalid', 'true'); + field.focus(); + } +} + +function clearAccountCreationValidationErrors() { + for (const fieldId of ['accountUsername', 'accountPassword', 'accountRole']) { + document.getElementById(fieldId)?.removeAttribute('aria-invalid'); + } +} + function renderAccounts() { const container = $('#accountList'); if (!container) return; @@ -909,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; @@ -1048,19 +1102,23 @@ async function syncAccounts() { async function createAccountFromForm() { if (!isAdministrator() || accountSettingsBusy) return; + const validation = validateAccountCreationValues({ + username: $('#accountUsername').value, + password: $('#accountPassword').value, + role: $('#accountRole').value + }); + if (!validation.ok) { + showAccountCreationValidationError(validation); + return; + } + clearAccountCreationValidationErrors(); accountSettingsBusy = true; renderAccounts(); const message = $('#accountMessage'); try { const result = await apiRequest('/api/accounts', { method: 'POST', - body: { - username: String($('#accountUsername').value || '').trim(), - password: String($('#accountPassword').value || ''), - role: $('#accountRole').value, - must_change_password: $('#accountMustChangePassword').checked, - business_route_ids: [] - } + body: validation.body }); if (result.account) accountList = [...accountList.filter((item) => item.id !== result.account.id), result.account] .sort((left, right) => left.username.localeCompare(right.username, 'zh-CN')); @@ -1092,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 = '密码已重置,该账号的现有会话已全部撤销。'; @@ -5108,7 +5171,6 @@ async function initializeSession() { } showAuthenticatedApp(me.user); - if (me.user?.must_change_password) return true; if (isAdministrator()) { try { await syncAutomationSettings(); @@ -5209,7 +5271,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('全自动化设置读取失败')); } @@ -5232,9 +5293,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) => { @@ -5243,6 +5304,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; @@ -5258,7 +5323,6 @@ document.addEventListener('DOMContentLoaded', async () => { $('#currentPassword').value = ''; $('#newPassword').value = ''; $('#confirmNewPassword').value = ''; - passwordChangeForced = false; await initializeSession(); } catch (error) { errorNode.textContent = error.message || String(error); @@ -5379,7 +5443,7 @@ document.addEventListener('DOMContentLoaded', async () => { event.preventDefault(); createAccountFromForm().catch((error) => { const message = $('#accountMessage'); - if (message) message.textContent = error.message || String(error); + if (message) message.textContent = accountCreationErrorMessage(error); }); }); $('#accountList')?.addEventListener('change', (event) => { @@ -5661,7 +5725,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(() => {}); diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index 5374bef..a698d3f 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -71,11 +71,11 @@

ACCOUNT SECURITY

修改密码

-

新密码至少 12 个字符。修改后,其他已登录会话将失效。

+

修改后,其他已登录会话将失效。

- - + +
@@ -96,9 +96,8 @@
- + -