diff --git a/api.go b/api.go index ca0c9ba8..8f0e4c37 100644 --- a/api.go +++ b/api.go @@ -2,12 +2,17 @@ package filemanager import ( "context" + "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" "os" + "strconv" "strings" + + "github.com/asdine/storm" ) func cleanURL(path string) (string, string) { @@ -54,6 +59,8 @@ func serveAPI(c *requestContext, w http.ResponseWriter, r *http.Request) (int, e } } + fmt.Println(c.us) + switch router { case "download": return downloadHandler(c, w, r) @@ -65,6 +72,12 @@ func serveAPI(c *requestContext, w http.ResponseWriter, r *http.Request) (int, e return search(c, w, r) case "resource": return resourceHandler(c, w, r) + case "users": + if !c.us.Admin && !(r.URL.Path == "/self" && r.Method == http.MethodPut) { + return http.StatusForbidden, nil + } + + return usersHandler(c, w, r) } return http.StatusNotFound, nil @@ -319,3 +332,242 @@ func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort return } + +func usersHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + switch r.Method { + case http.MethodGet: + return usersGetHandler(c, w, r) + case http.MethodPost: + return usersPostHandler(c, w, r) + case http.MethodDelete: + return usersDeleteHandler(c, w, r) + case http.MethodPut: + return usersPutHandler(c, w, r) + } + + return http.StatusNotImplemented, nil +} + +// usersGetHandler is used to handle the GET requests for /api/users. It can print a list +// of users or a specific user. The password hash is always removed before being sent to the +// client. +func usersGetHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // If the request is a list of users. + if r.URL.Path == "/" { + users := []User{} + + for _, user := range c.fm.Users { + // Copies the user and removes the password. + u := *user + u.Password = "" + users = append(users, u) + } + + return renderJSON(w, users) + } + + // Otherwise we just want one, specific, user. + sid := strings.TrimPrefix(r.URL.Path, "/") + sid = strings.TrimSuffix(sid, "/") + + id, err := strconv.Atoi(sid) + if err != nil { + return http.StatusNotFound, err + } + + // Searches for the user and prints the one who matches. + for _, user := range c.fm.Users { + if user.ID != id { + continue + } + + u := *user + u.Password = "" + return renderJSON(w, u) + } + + // If there aren't any matches, return Not Found. + return http.StatusNotFound, nil +} + +func usersPostHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // New users should be created on /api/users. + if r.URL.Path != "/" { + return http.StatusMethodNotAllowed, nil + } + + // If the request body is empty, send a Bad Request status. + if r.Body == nil { + return http.StatusBadRequest, nil + } + + var u User + + // Parses the user and checks for error. + err := json.NewDecoder(r.Body).Decode(&u) + if err != nil { + return http.StatusBadRequest, nil + } + + // The username and the password cannot be empty. + if u.Username == "" || u.Password == "" || u.FileSystem == "" { + return http.StatusBadRequest, errors.New("Username, password or scope are empty") + } + + // Initialize rules if they're not initialized. + if u.Rules == nil { + u.Rules = []*Rule{} + } + + // Initialize commands if not initialized. + if u.Commands == nil { + u.Commands = []string{} + } + + // It's a new user so the ID will be auto created. + if u.ID != 0 { + u.ID = 0 + } + + // Hashes the password. + pw, err := hashPassword(u.Password) + if err != nil { + return http.StatusInternalServerError, err + } + + u.Password = pw + + // Saves the user to the database. + err = c.fm.db.Save(&u) + if err == storm.ErrAlreadyExists { + return http.StatusConflict, err + } + + if err != nil { + return http.StatusInternalServerError, err + } + + // Saves the user to the memory. + c.fm.Users[u.Username] = &u + + // Set the Location header and return. + w.WriteHeader(http.StatusCreated) + w.Header().Set("Location", c.fm.RootURL()+"/api/users/"+strconv.Itoa(u.ID)) + return 0, nil +} + +func usersDeleteHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // New users should be created on /api/users. + if r.URL.Path == "/" { + return http.StatusMethodNotAllowed, nil + } + + // Otherwise we just want one, specific, user. + sid := strings.TrimPrefix(r.URL.Path, "/") + sid = strings.TrimSuffix(sid, "/") + + id, err := strconv.Atoi(sid) + if err != nil { + return http.StatusNotFound, err + } + + err = c.fm.db.DeleteStruct(&User{ID: id}) + if err == storm.ErrNotFound { + return http.StatusNotFound, err + } + + if err != nil { + return http.StatusInternalServerError, err + } + + for _, user := range c.fm.Users { + if user.ID == id { + delete(c.fm.Users, user.Username) + } + } + + return http.StatusOK, nil +} + +func usersPutHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // New users should be created on /api/users. + if r.URL.Path == "/" { + return http.StatusMethodNotAllowed, nil + } + + // Otherwise we just want one, specific, user. + sid := strings.TrimPrefix(r.URL.Path, "/") + sid = strings.TrimSuffix(sid, "/") + + id, err := strconv.Atoi(sid) + if err != nil && sid != "self" { + return http.StatusNotFound, err + } + + // If the request body is empty, send a Bad Request status. + if r.Body == nil { + return http.StatusBadRequest, nil + } + + var u User + + // Parses the user and checks for error. + err = json.NewDecoder(r.Body).Decode(&u) + if err != nil { + return http.StatusBadRequest, nil + } + + if sid == "self" { + if u.Password == "" { + return http.StatusBadRequest, errors.New("Password missing") + } + + pw, err := hashPassword(u.Password) + if err != nil { + fmt.Println(err) + return http.StatusInternalServerError, err + } + + c.us.Password = pw + err = c.fm.db.UpdateField(&User{ID: c.us.ID}, "Password", pw) + if err != nil { + fmt.Println(err) + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + } + + // The username and the filesystem cannot be empty. + if u.Username == "" || u.FileSystem == "" { + return http.StatusBadRequest, errors.New("Username, password or scope are empty") + } + + // Initialize rules if they're not initialized. + if u.Rules == nil { + u.Rules = []*Rule{} + } + + // Initialize commands if not initialized. + if u.Commands == nil { + u.Commands = []string{} + } + + ouser, ok := c.fm.Users[u.Username] + if !ok { + return http.StatusNotFound, nil + } + + u.ID = id + u.Password = ouser.Password + + // Updates the whole User struct because we always are supposed + // to send a new entire object. + err = c.fm.db.Save(&u) + if err != nil { + return http.StatusInternalServerError, err + } + + c.fm.Users[u.Username] = &u + return http.StatusOK, nil +}