From eafc6c6499a380d8d29450870368d58875d10ce5 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 22 Sep 2026 01:20:24 +0800 Subject: [PATCH] feat: add user password management --- chfs-ng/internal/api/server.go | 56 ++++++++++++++++++++++++++++- chfs-ng/internal/api/web/index.html | 9 +++-- chfs-ng/internal/auth/users.go | 13 +++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 12a776c..0ba47df 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -77,6 +77,7 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("/chfs/session", s.session) mux.HandleFunc("/chfs/me", s.me) mux.HandleFunc("/chfs/users", s.users) + mux.HandleFunc("/chfs/password", s.password) mux.HandleFunc("/chfs/newtext", s.newtext) mux.HandleFunc("/chfs/updatetext", s.updatetext) 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") 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) { @@ -513,6 +548,25 @@ func (s *Server) users(w http.ResponseWriter, r *http.Request) { } s.logOp(r, fmt.Sprintf("created user '%s'", u.Username)) 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: username := r.URL.Query().Get("username") if err := auth.ValidateUsername(username); err != nil { diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index 61f27d5..a0bfea0 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -29,7 +29,7 @@ table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:13px 15p - +
名称大小修改时间
@@ -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=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[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"} 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='
'+esc(e.message)+"
"}} function render(items){const body=$("files");body.innerHTML="";if(!items.length){body.innerHTML='这里还没有文件';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='
'+icon+''+ (f.dir?''+esc(f.name)+'':''+esc(f.name)+'')+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'
'+(f.dir?'':'')+'
';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()} @@ -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=""}) $("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='
🔎 '+esc(f.path)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'';body.appendChild(row)}} +function changePassword(){show('

修改密码

')} +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('

登录

')} 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()} 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()}) -async function showUsers(){try{const d=await api("/chfs/users");let html='

用户管理

';for(const u of d.users)html+='

'+esc(u.username)+' · '+esc(u.home||"")+(u.managed&&!u.admin?' ':"")+"

";html+='
';show(html)}catch(e){alert(e.message)}} +async function showUsers(){try{const d=await api("/chfs/users");let html='

用户管理

';for(const u of d.users)html+='

'+esc(u.username)+' · '+esc(u.home||"")+(u.managed&&!u.admin?' ':"")+"

";html+='
';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 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)}} init(); diff --git a/chfs-ng/internal/auth/users.go b/chfs-ng/internal/auth/users.go index 5701564..e535350 100644 --- a/chfs-ng/internal/auth/users.go +++ b/chfs-ng/internal/auth/users.go @@ -109,6 +109,19 @@ func (u *UserStore) Add(m *Manager, username, password, home string) (*Account, 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 { a, ok := m.Accounts[username] if !ok || a.Admin || a.PasswordHash == "" {