feat: add user password management

This commit is contained in:
OpenClaw Agent
2026-09-22 01:20:24 +08:00
parent 77572e610b
commit eafc6c6499
3 changed files with 74 additions and 4 deletions
+55 -1
View File
@@ -77,6 +77,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("/chfs/session", s.session) mux.HandleFunc("/chfs/session", s.session)
mux.HandleFunc("/chfs/me", s.me) mux.HandleFunc("/chfs/me", s.me)
mux.HandleFunc("/chfs/users", s.users) mux.HandleFunc("/chfs/users", s.users)
mux.HandleFunc("/chfs/password", s.password)
mux.HandleFunc("/chfs/newtext", s.newtext) mux.HandleFunc("/chfs/newtext", s.newtext)
mux.HandleFunc("/chfs/updatetext", s.updatetext) mux.HandleFunc("/chfs/updatetext", s.updatetext)
mux.HandleFunc("/chfs/upload", s.upload) mux.HandleFunc("/chfs/upload", s.upload)
@@ -464,7 +465,41 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusUnauthorized, "not authenticated") writeJSONError(w, http.StatusUnauthorized, "not authenticated")
return return
} }
writeJSON(w, map[string]any{"username": a.Username, "home": a.Home, "admin": a.Admin, "mask": permString(a.MaskFor(a.Home))}) writeJSON(w, map[string]any{"username": a.Username, "home": a.Home, "admin": a.Admin, "mask": permString(a.MaskFor(a.Home)), "managed": a.PasswordHash != ""})
}
func (s *Server) password(w http.ResponseWriter, r *http.Request) {
a := account(r, s)
if a == nil || a.Username == "" {
writeJSONError(w, http.StatusUnauthorized, "not authenticated")
return
}
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var req struct {
CurrentPassword string `json:"currentPassword"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid json")
return
}
if a.Password != req.CurrentPassword && a.PasswordHash != auth.PasswordHash(req.CurrentPassword) {
writeJSONError(w, http.StatusBadRequest, "current password incorrect")
return
}
if s.UserStore == nil {
writeJSONError(w, http.StatusNotImplemented, "user management disabled")
return
}
if err := s.UserStore.UpdatePassword(s.Auth, a.Username, req.Password); err != nil {
writeJSONError(w, http.StatusBadRequest, err.Error())
return
}
s.logOp(r, "changed password")
writeJSON(w, map[string]any{"ok": true})
} }
func (s *Server) users(w http.ResponseWriter, r *http.Request) { func (s *Server) users(w http.ResponseWriter, r *http.Request) {
@@ -513,6 +548,25 @@ func (s *Server) users(w http.ResponseWriter, r *http.Request) {
} }
s.logOp(r, fmt.Sprintf("created user '%s'", u.Username)) s.logOp(r, fmt.Sprintf("created user '%s'", u.Username))
writeJSON(w, map[string]any{"username": u.Username, "home": u.Home}) writeJSON(w, map[string]any{"username": u.Username, "home": u.Home})
case http.MethodPut:
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid json")
return
}
if err := auth.ValidateUsername(req.Username); err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid username")
return
}
if err := s.UserStore.UpdatePassword(s.Auth, req.Username, req.Password); err != nil {
writeJSONError(w, http.StatusBadRequest, err.Error())
return
}
s.logOp(r, fmt.Sprintf("reset password for '%s'", req.Username))
writeJSON(w, map[string]any{"ok": true})
case http.MethodDelete: case http.MethodDelete:
username := r.URL.Query().Get("username") username := r.URL.Query().Get("username")
if err := auth.ValidateUsername(username); err != nil { if err := auth.ValidateUsername(username); err != nil {
+6 -3
View File
@@ -29,7 +29,7 @@ table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:13px 15p
<button onclick="newFolder()">新建文件夹</button><button onclick="newText()">新建文本</button> <button onclick="newFolder()">新建文件夹</button><button onclick="newText()">新建文本</button>
<label><button>上传</button><input id="upload" type="file" multiple hidden></label> <label><button>上传</button><input id="upload" type="file" multiple hidden></label>
<input id="search" placeholder="搜索文件名,回车执行"> <input id="search" placeholder="搜索文件名,回车执行">
<button id="usersBtn" onclick="showUsers()" hidden>用户管理</button> <button id="usersBtn" onclick="showUsers()" hidden>用户管理</button><button class="ghost" id="pwdBtn" onclick="changePassword()" hidden>修改密码</button>
</div> </div>
<div class="card"><table><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th></th></tr></thead><tbody id="files"></tbody></table></div> <div class="card"><table><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th></th></tr></thead><tbody id="files"></tbody></table></div>
</main> </main>
@@ -40,7 +40,7 @@ async function api(url,opt={}){const r=await fetch(url,opt);if(!r.ok){let t="请
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]))} function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]))}
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"} function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";await refresh()} async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";await refresh()}
function updateUser(){$("who").textContent=me.username?me.username: "访客";$("loginBtn").hidden=!!me.username;$("logoutBtn").hidden=!me.username;$("usersBtn").hidden=!me.admin} function updateUser(){$("who").textContent=me.username?me.username: "访客";$("loginBtn").hidden=!!me.username;$("logoutBtn").hidden=!me.username;$("usersBtn").hidden=!me.admin;$("pwdBtn").hidden=!me.username||!me.managed}
async function refresh(){try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);render(data.files||[]);$("crumb").textContent="/"+current;$("notice").innerHTML=""}catch(e){$("notice").innerHTML='<div class="notice">'+esc(e.message)+"</div>"}} async function refresh(){try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);render(data.files||[]);$("crumb").textContent="/"+current;$("notice").innerHTML=""}catch(e){$("notice").innerHTML='<div class="notice">'+esc(e.message)+"</div>"}}
function render(items){const body=$("files");body.innerHTML="";if(!items.length){body.innerHTML='<tr><td colspan="4" class="empty">这里还没有文件</td></tr>';return}for(const f of items){const p=current?current+"/"+f.name:f.name;const row=document.createElement("tr");const icon=f.dir?"📁":"📄";row.innerHTML='<td><div class="name"><span>'+icon+'</span>'+ (f.dir?'<a href="#" onclick="openDir('+JSON.stringify(p)+');return false">'+esc(f.name)+'</a>':'<span>'+esc(f.name)+'</span>')+'</div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td><div class="actions">'+(f.dir?'<button class="ghost" onclick="downloadDir('+JSON.stringify(p)+')">ZIP</button>':'<button class="ghost" onclick="downloadFile('+JSON.stringify(p)+')">下载</button>')+'<button class="ghost" onclick="renameItem('+JSON.stringify(p)+')">改名</button><button class="danger" onclick="removeItem('+JSON.stringify(p)+')">删除</button></div></td>';body.appendChild(row)}} function render(items){const body=$("files");body.innerHTML="";if(!items.length){body.innerHTML='<tr><td colspan="4" class="empty">这里还没有文件</td></tr>';return}for(const f of items){const p=current?current+"/"+f.name:f.name;const row=document.createElement("tr");const icon=f.dir?"📁":"📄";row.innerHTML='<td><div class="name"><span>'+icon+'</span>'+ (f.dir?'<a href="#" onclick="openDir('+JSON.stringify(p)+');return false">'+esc(f.name)+'</a>':'<span>'+esc(f.name)+'</span>')+'</div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td><div class="actions">'+(f.dir?'<button class="ghost" onclick="downloadDir('+JSON.stringify(p)+')">ZIP</button>':'<button class="ghost" onclick="downloadFile('+JSON.stringify(p)+')">下载</button>')+'<button class="ghost" onclick="renameItem('+JSON.stringify(p)+')">改名</button><button class="danger" onclick="removeItem('+JSON.stringify(p)+')">删除</button></div></td>';body.appendChild(row)}}
function openDir(p){current=p;refresh()}function goUp(){if(!current)return;const root=me.home||"";if(current===root)return;let p=current.split("/");p.pop();let n=p.join("/");if(root&&!(n===root||n.startsWith(root+"/")))n=root;current=n;refresh()} function openDir(p){current=p;refresh()}function goUp(){if(!current)return;const root=me.home||"";if(current===root)return;let p=current.split("/");p.pop();let n=p.join("/");if(root&&!(n===root||n.startsWith(root+"/")))n=root;current=n;refresh()}
@@ -53,13 +53,16 @@ async function newText(){const n=prompt("文件名","new.txt");if(!n)return;cons
$("upload").addEventListener("change",async e=>{const fd=new FormData();fd.append("folder",current);for(const f of e.target.files)fd.append("file",f);try{await api("/chfs/upload",{method:"POST",body:fd});refresh()}catch(x){alert(x.message)}e.target.value=""}) $("upload").addEventListener("change",async e=>{const fd=new FormData();fd.append("folder",current);for(const f of e.target.files)fd.append("file",f);try{await api("/chfs/upload",{method:"POST",body:fd});refresh()}catch(x){alert(x.message)}e.target.value=""})
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){alert(x.message)}}) $("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){alert(x.message)}})
function renderSearch(items){$("crumb").textContent="搜索结果";const body=$("files");body.innerHTML="";for(const f of items){const row=document.createElement("tr");row.innerHTML='<td><div class="name">🔎 <a href="#" onclick="openDir('+JSON.stringify(f.dir?f.path:f.path.split("/").slice(0,-1).join("/")).replace(/^"/,'\\"')+');return false">'+esc(f.path)+'</a></div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td></td>';body.appendChild(row)}} function renderSearch(items){$("crumb").textContent="搜索结果";const body=$("files");body.innerHTML="";for(const f of items){const row=document.createElement("tr");row.innerHTML='<td><div class="name">🔎 <a href="#" onclick="openDir('+JSON.stringify(f.dir?f.path:f.path.split("/").slice(0,-1).join("/")).replace(/^"/,'\\"')+');return false">'+esc(f.path)+'</a></div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td></td>';body.appendChild(row)}}
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});alert("密码已更新");closeModal()}catch(e){alert(e.message)}}
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')} function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){alert(e.message)}} async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){alert(e.message)}}
async function logout(){await fetch("/chfs/session",{method:"DELETE"});location.reload()} async function logout(){await fetch("/chfs/session",{method:"DELETE"});location.reload()}
function show(html){$("dialog").innerHTML=html;$("modal").classList.add("show")}function closeModal(){$("modal").classList.remove("show")} function show(html){$("dialog").innerHTML=html;$("modal").classList.add("show")}function closeModal(){$("modal").classList.remove("show")}
$("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()}) $("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()})
async function showUsers(){try{const d=await api("/chfs/users");let html='<h2>用户管理</h2><div class="field"><input id="nu" placeholder="用户名"><input id="np" type="password" placeholder="初始密码"><button onclick="createUser()">创建用户</button></div><div>';for(const u of d.users)html+='<p><b>'+esc(u.username)+'</b> · '+esc(u.home||"")+(u.managed&&!u.admin?' <button class="danger" onclick="deleteUser('+JSON.stringify(u.username)+')">删除</button>':"")+"</p>";html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){alert(e.message)}} async function showUsers(){try{const d=await api("/chfs/users");let html='<h2>用户管理</h2><div class="field"><input id="nu" placeholder="用户名"><input id="np" type="password" placeholder="初始密码"><button onclick="createUser()">创建用户</button></div><div>';for(const u of d.users)html+='<p><b>'+esc(u.username)+'</b> · '+esc(u.home||"")+(u.managed&&!u.admin?' <button class="ghost" onclick="resetUser('+JSON.stringify(u.username)+')">重置密码</button> <button class="danger" onclick="deleteUser('+JSON.stringify(u.username)+')">删除</button>':"")+"</p>";html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){alert(e.message)}}
async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$("nu").value,password:$("np").value})});showUsers()}catch(e){alert(e.message)}} async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$("nu").value,password:$("np").value})});showUsers()}catch(e){alert(e.message)}}
async function resetUser(u){const p=prompt("为 "+u+" 设置新密码");if(!p)return;try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});alert("密码已更新")}catch(e){alert(e.message)}}
async function deleteUser(u){if(!confirm("删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){alert(e.message)}} async function deleteUser(u){if(!confirm("删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){alert(e.message)}}
init(); init();
</script> </script>
+13
View File
@@ -109,6 +109,19 @@ func (u *UserStore) Add(m *Manager, username, password, home string) (*Account,
return a, u.Save(m) return a, u.Save(m)
} }
func (u *UserStore) UpdatePassword(m *Manager, username, password string) error {
if strings.TrimSpace(password) == "" {
return fmt.Errorf("password required")
}
a, ok := m.Accounts[username]
if !ok || a.PasswordHash == "" {
return fmt.Errorf("managed user not found")
}
a.PasswordHash = PasswordHash(password)
a.Password = ""
return u.Save(m)
}
func (u *UserStore) Delete(m *Manager, username string) error { func (u *UserStore) Delete(m *Manager, username string) error {
a, ok := m.Accounts[username] a, ok := m.Accounts[username]
if !ok || a.Admin || a.PasswordHash == "" { if !ok || a.Admin || a.PasswordHash == "" {