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:
Drsakura
2026-10-22 17:24:19 +08:00
parent dc607e4ea3
commit 1ffcc1ea07
20 changed files with 905 additions and 585 deletions
+69 -4
View File
@@ -148,6 +148,12 @@ function ImportExportSettings() {
版本号以顶层 package.json 为准;远端取 Release(优先)或 tag。
--------------------------------------------------------------------------- */
function SystemUpdateSettings() {
const { user } = useAuth()
const isAdmin = user?.role === 'admin'
const [job, setJob] = useState(null)
const [updatePassword, setUpdatePassword] = useState('')
const [starting, setStarting] = useState(false)
const [connectionLost, setConnectionLost] = useState(false)
const brand = useBranding()
const [info, setInfo] = useState(null)
const [repo, setRepo] = useState('')
@@ -193,6 +199,38 @@ function SystemUpdateSettings() {
}
}
const updating = starting || (job && !['idle', 'completed', 'failed'].includes(job.state))
useEffect(() => {
if (!isAdmin) return
let alive = true
let timer
const poll = async () => {
try {
const { data } = await axios.get('/api/system/update/status', { timeout: 8000 })
if (alive) { setJob(data); setConnectionLost(false) }
} catch {
if (alive) setConnectionLost(true)
} finally {
if (alive) timer = setTimeout(poll, 2500)
}
}
poll()
return () => { alive = false; clearTimeout(timer) }
}, [isAdmin])
const install = async event => {
event.preventDefault()
setStarting(true)
setError('')
try {
const { data } = await axios.post('/api/system/update', { current_password: updatePassword }, { timeout: 60000 })
setJob(current => ({ ...current, ...data }))
setUpdatePassword('')
} catch (e) {
setError(e?.response?.data?.error || '未能确认更新任务状态,请查看下方进度后再试')
} finally { setStarting(false) }
}
const fmtTime = (value) => {
if (!value) return ''
try {
@@ -205,7 +243,7 @@ function SystemUpdateSettings() {
return (
<div className="settings-section">
<h3>系统与更新</h3>
<p className="settings-description">查看当前版本与运行环境,并从官方仓库检查是否有新版本。</p>
<p className="settings-description">查看当前版本;系统管理员可在此安装更新。</p>
<div className="settings-group">
<h4>当前版本</h4>
@@ -229,16 +267,16 @@ function SystemUpdateSettings() {
value={repo}
placeholder="owner/repo"
onChange={(e) => setRepo(e.target.value)}
disabled={checking}
disabled
/>
</div>
<p className="help-text">默认抓取官方仓库,也支持填完整地址(https://github.com/owner/repo)。</p>
<p className="help-text">更新源由服务器配置,网页安装使用此仓库的最新正式发布版本或版本标签。</p>
</div>
<div className="settings-group">
<h4>检查更新</h4>
<div className="settings-actions">
<button className="btn btn-primary" onClick={check} disabled={checking || loadingInfo}>
<button className="btn btn-primary" onClick={check} disabled={checking || loadingInfo || updating}>
{checking ? '检查中…' : '检查更新'}
</button>
</div>
@@ -300,6 +338,33 @@ function SystemUpdateSettings() {
</>
)}
</div>
{isAdmin && (
<div className="settings-group">
<h4>安装更新</h4>
<p className="help-text">系统先构建新版本并备份数据库,再切换应用和重启。重启时页面可能短暂断开,进度会自动恢复。</p>
{job?.capability && !job.capability.supported && <p className="help-text">{job.capability.reason}</p>}
{result?.hasUpdate && job?.capability?.supported && !updating && !(job?.state === 'completed' && job?.version === result.latest) && (
<form onSubmit={install}>
<div className="form-group">
<label htmlFor="update-current-password">管理员当前密码</label>
<input id="update-current-password" className="form-input" type="password" autoComplete="current-password"
required value={updatePassword} onChange={event => setUpdatePassword(event.target.value)} />
</div>
<button type="submit" className="btn btn-primary" disabled={starting || !updatePassword}>
{starting ? '正在创建任务…' : `安装 v${result.latest}`}
</button>
</form>
)}
{job && job.state !== 'idle' && (
<div role="status" className={`update-status ${job.state === 'failed' ? 'is-error' : job.state === 'completed' ? 'is-ok' : 'is-checking'}`}>
{updating && <span className="spinner" />}<span>{job.message}</span>
</div>
)}
{connectionLost && <p role="status" className="help-text">暂时无法连接服务,正在自动重试…</p>}
{job?.state === 'completed' && <button className="btn btn-primary" onClick={() => window.location.reload()}>刷新使用新版本</button>}
{job?.log && <details><summary>更新日志</summary><pre className="update-notes">{job.log}</pre></details>}
</div>
)}
</div>
)
}