feat: add user password management
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
<label><button>上传</button><input id="upload" type="file" multiple hidden></label>
|
||||
<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 class="card"><table><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th></th></tr></thead><tbody id="files"></tbody></table></div>
|
||||
</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=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[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='<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 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='<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>')}
|
||||
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='<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 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();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user