Files
Agent-System-Frontend/src/components/AdminUsersModal.tsx
T
weichen 8db7093a9d Fix password form semantics and per-type setting inputs
Why: the change-password modal rendered loose password inputs outside any
<form> with no autocomplete or username association (browser password-form
warnings; Enter did not submit), and its 401 handling collapsed every
cause into "wrong current password". SettingInput masked ALL free-text
config values as password fields.

Evidence: vitest 24 files / 116 tests incl. new settingInputAttrs matrix;
knip and tsc -b && vite build pass; e2e changepw-form spec (driven by the
dev-repo e2e.sh plain phase) covers form submit via Enter, autocomplete
trio, readonly username and the classified error copy.

Impact: ChangePasswordModal uses form onSubmit, htmlFor/id association,
username/current-password/new-password autocomplete and reuses
humanChangePasswordError; SettingInput renders text inputs unless the
field is secret.

Refs: 2026-10-12 diagnostic report, increment 3.
2026-10-12 03:48:13 +00:00

74 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 管理员用户管理已迁「系统管理」分区(components/adminSections.tsx UsersPanel,
// 重设计方案 阶段②)。本文件只保留登录者主动改密弹窗(与强制改密共用 API)。
import { useState } from 'react'
import { api } from '../api'
import { store, useStore } from '../store'
import { Icon } from '@ui'
import { humanChangePasswordError } from './LoginPage'
function Shell({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
return (
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
<div className="modal ad-card" role="dialog" aria-label={title} style={{ maxWidth: 640 }}>
<div className="modal-header">
<b>{title}</b>
<span className="spacer" />
<button className="ad-mini" onClick={onClose} aria-label="关闭"><Icon name="x" size={13} /></button>
</div>
<div className="ad-body">{children}</div>
</div>
</div>
)
}
/** 主动改密(账户菜单入口):用户名(只读关联)+ 当前密码 + 新密码 + 确认。
* 表单语义(2026-10-12 增量 3):<form onSubmit> 承载提交(Enter 可用)、autocomplete
* 三段标注(username/current-password/new-password)、label 与 input 关联;
* 错误文案复用 humanChangePasswordError——401 不再一刀切成「当前密码不正确」。 */
export function ChangePasswordModal({ onClose }: { onClose: () => void }) {
const me = useStore(s => s.me)
const [current, setCurrent] = useState('')
const [next, setNext] = useState('')
const [confirm, setConfirm] = useState('')
const [err, setErr] = useState('')
const [ok, setOk] = useState(false)
const [busy, setBusy] = useState(false)
const submit = async () => {
setErr('')
if (next !== confirm) { setErr('两次输入的新密码不一致'); return }
if (next.length < 8) { setErr('新密码至少 8 位'); return }
setBusy(true)
try {
await api.changePassword(current, next)
setOk(true)
store.toast('密码已更新')
setTimeout(onClose, 900)
} catch (e) {
setErr(humanChangePasswordError(String(e)))
} finally { setBusy(false) }
}
return (
<Shell title="修改密码" onClose={onClose}>
{ok ? <div className="ad-notice">密码已更新,其他设备的会话已注销。</div> : (
<form className="ad-changepw" onSubmit={e => { e.preventDefault(); submit() }}>
<label className="lg-label" htmlFor="changepw-username">用户名</label>
<input id="changepw-username" className="lg-input" value={me?.owner ?? ''} readOnly autoComplete="username" />
<label className="lg-label" htmlFor="changepw-current">当前密码</label>
<input id="changepw-current" className="lg-input" type="password" autoComplete="current-password"
value={current} onChange={e => setCurrent(e.target.value)} autoFocus />
<label className="lg-label" htmlFor="changepw-next">新密码(≥8 位)</label>
<input id="changepw-next" className="lg-input" type="password" autoComplete="new-password"
value={next} onChange={e => setNext(e.target.value)} />
<label className="lg-label" htmlFor="changepw-confirm">确认新密码</label>
<input id="changepw-confirm" className="lg-input" type="password" autoComplete="new-password"
value={confirm} onChange={e => setConfirm(e.target.value)} />
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
<button className="lg-submit" type="submit" disabled={busy || !current || !next || !confirm}>{busy ? '提交中…' : '更新密码'}</button>
</form>
)}
</Shell>
)
}