feat: add admin web updates and verified username changes
Add isolated update jobs with database backups, progress logs and application rollback. Remove default account hints and prevent default admin recreation after renaming. Enforce customer and order search scopes, secure session cookies, fix quotation rounding and amount wording, and update backend dependencies. Add API and deployment regression coverage.
This commit is contained in:
@@ -95,7 +95,6 @@ function LoginPage() {
|
||||
{busy ? '登录中…' : '登 录'}
|
||||
</button>
|
||||
|
||||
<p className="login-tip">首次使用默认账号 <code>admin</code>,登录后请在「个人资料」里修改密码。</p>
|
||||
</form>
|
||||
</div>
|
||||
<p className="login-copyright">{brand.name} · 外贸供应链管理系统</p>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SaveIcon, KeyIcon, CameraIcon, LogoutIcon, BuildingIcon, ShieldIcon,
|
||||
EyeIcon, EyeOffIcon, UserIcon
|
||||
} from '../components/Icons'
|
||||
import { useAuth, roleLabel } from '../auth'
|
||||
import { useAuth, roleLabel, hasPermission } from '../auth'
|
||||
import { setPageTitle } from '../branding'
|
||||
import { setUnsavedGuard, clearUnsavedGuard } from '../unsavedGuard'
|
||||
|
||||
@@ -60,6 +60,11 @@ function ProfilePage() {
|
||||
const [notice, setNotice] = useState(null)
|
||||
const fileRef = useRef(null)
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
const [usernamePassword, setUsernamePassword] = useState('')
|
||||
const [usernameBusy, setUsernameBusy] = useState(false)
|
||||
const [usernameNotice, setUsernameNotice] = useState(null)
|
||||
const canManageUsers = hasPermission(user, 'user.manage')
|
||||
const [password, setPassword] = useState({ current: '', next: '', confirm: '' })
|
||||
const [passwordBusy, setPasswordBusy] = useState(false)
|
||||
const [passwordNotice, setPasswordNotice] = useState(null)
|
||||
@@ -108,14 +113,16 @@ function ProfilePage() {
|
||||
phone: payload.phone,
|
||||
address: payload.address,
|
||||
avatar: payload.avatar,
|
||||
role: payload.role,
|
||||
org_id: payload.org_id === '' ? undefined : Number(payload.org_id)
|
||||
...(hasPermission(user, 'user.manage') ? {
|
||||
role: payload.role,
|
||||
org_id: payload.org_id === '' ? undefined : Number(payload.org_id)
|
||||
} : {})
|
||||
})
|
||||
const updated = response.data.user
|
||||
setUser(updated)
|
||||
setSaved(toForm(updated))
|
||||
return updated
|
||||
}, [setUser])
|
||||
}, [setUser, user])
|
||||
|
||||
/* 让外壳在跳转前能问一句「要不要先保存」 */
|
||||
useEffect(() => {
|
||||
@@ -155,6 +162,21 @@ function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const submitUsername = async event => {
|
||||
event.preventDefault()
|
||||
setUsernameBusy(true)
|
||||
setUsernameNotice(null)
|
||||
try {
|
||||
const { data } = await axios.patch('/api/me/username', { username: username.trim(), current_password: usernamePassword })
|
||||
setUser(data.user)
|
||||
setUsername('')
|
||||
setUsernamePassword('')
|
||||
setUsernameNotice({ type: 'ok', text: data.message })
|
||||
} catch (error) {
|
||||
setUsernameNotice({ type: 'error', text: error?.response?.data?.error || '修改用户名失败' })
|
||||
} finally { setUsernameBusy(false) }
|
||||
}
|
||||
|
||||
const submitPassword = async event => {
|
||||
event.preventDefault()
|
||||
if (!password.current) { setPasswordNotice({ type: 'error', text: '请输入当前密码' }); return }
|
||||
@@ -258,7 +280,7 @@ function ProfilePage() {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><BuildingIcon size={13} /> 所属组织</label>
|
||||
<select value={form.org_id} onChange={event => setField('org_id', event.target.value)}>
|
||||
<select disabled={!canManageUsers} value={form.org_id} onChange={event => setField('org_id', event.target.value)}>
|
||||
<option value="">未归属</option>
|
||||
{organizations.map(org => (
|
||||
<option key={org.id} value={org.id}>{org.name}{org.member_count ? `(${org.member_count} 人)` : ''}</option>
|
||||
@@ -267,7 +289,7 @@ function ProfilePage() {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><ShieldIcon size={13} /> 权限角色</label>
|
||||
<select value={form.role} onChange={event => setField('role', event.target.value)}>
|
||||
<select disabled={!canManageUsers} value={form.role} onChange={event => setField('role', event.target.value)}>
|
||||
{roles.map(role => (
|
||||
<option key={role.value} value={role.value}>{role.label}</option>
|
||||
))}
|
||||
@@ -293,6 +315,25 @@ function ProfilePage() {
|
||||
|
||||
{/* ---------------- 修改密码 ---------------- */}
|
||||
<section className="panel">
|
||||
<div className="panel-title"><UserIcon size={15} /> 修改用户名</div>
|
||||
<form className="profile-fields profile-fields-single" onSubmit={submitUsername}>
|
||||
<p className="field-hint">当前用户名:{user?.username}。修改后请使用新用户名登录,业务数据归属保持不变。</p>
|
||||
<div className="form-group">
|
||||
<label htmlFor="new-username">新用户名</label>
|
||||
<input id="new-username" autoComplete="username" value={username} minLength={3} maxLength={50} required
|
||||
onChange={event => setUsername(event.target.value)} placeholder="3–50 位字母、数字或 _ . @ -" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username-password">当前密码</label>
|
||||
<input id="username-password" type="password" autoComplete="current-password" required value={usernamePassword}
|
||||
onChange={event => setUsernamePassword(event.target.value)} placeholder="输入当前密码以验证身份" />
|
||||
</div>
|
||||
{usernameNotice && <p role="status" className={`profile-notice ${usernameNotice.type}`}>{usernameNotice.text}</p>}
|
||||
<button type="submit" className="btn btn-primary" disabled={usernameBusy}>
|
||||
{usernameBusy ? '提交中…' : '确认修改用户名'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="panel-title"><KeyIcon size={15} /> 修改密码</div>
|
||||
<form className="profile-fields profile-fields-single" onSubmit={submitPassword}>
|
||||
{[
|
||||
|
||||
Reference in New Issue
Block a user