feat: add web ui and dynamic private users
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
@@ -30,6 +31,9 @@ import (
|
||||
"github.com/emersion/go-webdav"
|
||||
)
|
||||
|
||||
//go:embed web/index.html
|
||||
var webIndex []byte
|
||||
|
||||
type Server struct {
|
||||
Store storage.Storage
|
||||
Auth *auth.Manager
|
||||
@@ -47,6 +51,8 @@ type Server struct {
|
||||
HTMLTitle string
|
||||
HTMLNotice string
|
||||
Log *opslog.Logger
|
||||
UserStore *auth.UserStore
|
||||
UserHome string
|
||||
}
|
||||
type fileInfo struct {
|
||||
Name string `json:"name"`
|
||||
@@ -69,6 +75,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("/chfs/rmfiles", s.remove)
|
||||
mux.HandleFunc("/chfs/exist", s.exist)
|
||||
mux.HandleFunc("/chfs/session", s.session)
|
||||
mux.HandleFunc("/chfs/me", s.me)
|
||||
mux.HandleFunc("/chfs/users", s.users)
|
||||
mux.HandleFunc("/chfs/newtext", s.newtext)
|
||||
mux.HandleFunc("/chfs/updatetext", s.updatetext)
|
||||
mux.HandleFunc("/chfs/upload", s.upload)
|
||||
@@ -450,6 +458,82 @@ func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (s *Server) me(w http.ResponseWriter, r *http.Request) {
|
||||
a := account(r, s)
|
||||
if a == nil {
|
||||
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))})
|
||||
}
|
||||
|
||||
func (s *Server) users(w http.ResponseWriter, r *http.Request) {
|
||||
a := account(r, s)
|
||||
if a == nil || !a.Admin {
|
||||
writeJSONError(w, http.StatusForbidden, "admin required")
|
||||
return
|
||||
}
|
||||
if s.UserStore == nil {
|
||||
writeJSONError(w, http.StatusNotImplemented, "user management disabled")
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
out := make([]map[string]any, 0, len(s.Auth.Accounts))
|
||||
for _, u := range s.Auth.Accounts {
|
||||
if u.Username == "" || u == s.Auth.Guest {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{"username": u.Username, "home": u.Home, "admin": u.Admin, "managed": u.PasswordHash != ""})
|
||||
}
|
||||
writeJSON(w, map[string]any{"users": out})
|
||||
case http.MethodPost:
|
||||
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
|
||||
}
|
||||
home := path.Join(strings.Trim(s.UserHome, "/"), req.Username)
|
||||
if unsafePath(home) {
|
||||
writeJSONError(w, http.StatusBadRequest, "invalid username")
|
||||
return
|
||||
}
|
||||
u, err := s.UserStore.Add(s.Auth, req.Username, req.Password, home)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := s.Store.MkdirAll(u.Home); err != nil {
|
||||
_ = s.UserStore.Delete(s.Auth, u.Username)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.logOp(r, fmt.Sprintf("created user '%s'", u.Username))
|
||||
writeJSON(w, map[string]any{"username": u.Username, "home": u.Home})
|
||||
case http.MethodDelete:
|
||||
username := r.URL.Query().Get("username")
|
||||
if err := auth.ValidateUsername(username); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "invalid username")
|
||||
return
|
||||
}
|
||||
if username == a.Username {
|
||||
writeJSONError(w, http.StatusBadRequest, "cannot delete current admin")
|
||||
return
|
||||
}
|
||||
if err := s.UserStore.Delete(s.Auth, username); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.logOp(r, fmt.Sprintf("deleted user '%s'", username))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
||||
a := account(r, s)
|
||||
rawDir, rawTitle, rawFilename := r.FormValue("filepath"), r.FormValue("title"), r.FormValue("filename")
|
||||
@@ -737,7 +821,7 @@ func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
p := strings.Trim(strings.TrimPrefix(r.URL.Path, "/chfs/shared/"), "/")
|
||||
if unsafePath(p) {
|
||||
if unsafePath(p) || !account(r, s).MaskFor(p).HasRead() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
@@ -842,8 +926,10 @@ func (s *Server) fileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
notice = html.EscapeString(notice)
|
||||
}
|
||||
page := bytes.ReplaceAll(webIndex, []byte("__CHFS_TITLE__"), []byte(html.EscapeString(title)))
|
||||
page = bytes.ReplaceAll(page, []byte("__CHFS_NOTICE__"), []byte(notice))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = fmt.Fprintf(w, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>%s</title></head><body><main><h1>%s</h1>%s<p><a href="/chfs/files?filepath=">Open file API</a> · <a href="/asset/api.html">API documentation</a></p></main></body></html>`, html.EscapeString(title), html.EscapeString(title), notice)
|
||||
_, _ = w.Write(page)
|
||||
}
|
||||
|
||||
func (s *Server) apiDoc(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -852,7 +938,7 @@ func (s *Server) apiDoc(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
const doc = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CHFS API</title></head><body><h1>CHFS API</h1><p>Compatibility API implemented by chfs-ng.</p><ul><li>/chfs/files</li><li>/chfs/session</li><li>/chfs/upload</li><li>/chfs/newdir</li><li>/chfs/rename</li><li>/chfs/rmfiles</li><li>/chfs/search</li><li>/chfs/downloaddir/<path></li><li>/chfs/shared/<path></li><li>/chfs/preview</li><li>/webdav</li></ul></body></html>`
|
||||
const doc = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CHFS API</title></head><body><h1>CHFS API</h1><p>Compatibility API implemented by chfs-ng.</p><ul><li>/chfs/files</li><li>/chfs/session</li><li>/chfs/me</li><li>/chfs/users (admin)</li><li>/chfs/upload</li><li>/chfs/newdir</li><li>/chfs/rename</li><li>/chfs/rmfiles</li><li>/chfs/search</li><li>/chfs/downloaddir/<path></li><li>/chfs/shared/<path></li><li>/chfs/preview</li><li>/webdav</li></ul></body></html>`
|
||||
_, _ = w.Write([]byte(doc))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user