2019-01-05 22:44:33 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2024-11-21 15:58:28 +00:00
|
|
|
"fmt"
|
2024-11-21 00:15:30 +00:00
|
|
|
"io"
|
2019-01-05 22:44:33 +00:00
|
|
|
"net/http"
|
2024-08-03 15:34:12 +00:00
|
|
|
"reflect"
|
2019-01-05 22:44:33 +00:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
|
2022-06-13 14:13:10 +00:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
"golang.org/x/text/language"
|
2020-05-31 23:12:36 +00:00
|
|
|
|
2023-06-15 01:08:09 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/errors"
|
2024-08-24 22:02:33 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/files"
|
2024-10-07 22:44:53 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/storage"
|
2023-06-15 01:08:09 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/users"
|
2019-01-05 22:44:33 +00:00
|
|
|
)
|
|
|
|
|
2020-12-24 17:22:48 +00:00
|
|
|
var (
|
|
|
|
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
|
|
|
)
|
|
|
|
|
2023-12-01 23:47:00 +00:00
|
|
|
// SortingSettings represents the sorting settings.
|
|
|
|
type Sorting struct {
|
|
|
|
By string `json:"by"`
|
|
|
|
Asc bool `json:"asc"`
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
type UserRequest struct {
|
|
|
|
What string `json:"what"`
|
|
|
|
Which []string `json:"which"`
|
|
|
|
Data *users.User `json:"data"`
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// userGetHandler retrieves a user by ID.
|
|
|
|
// @Summary Retrieve a user by ID
|
|
|
|
// @Description Returns a user's details based on their ID.
|
|
|
|
// @Tags Users
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param id path int true "User ID" or "self"
|
|
|
|
// @Success 200 {object} users.User "User details"
|
|
|
|
// @Failure 403 {object} map[string]string "Forbidden"
|
|
|
|
// @Failure 404 {object} map[string]string "Not Found"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal Server Error"
|
|
|
|
// @Router /api/users/{id} [get]
|
|
|
|
func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
givenUserIdString := r.URL.Query().Get("id")
|
|
|
|
|
|
|
|
// since api self is used to validate a logged in user
|
|
|
|
w.Header().Add("X-Renew-Token", "false")
|
|
|
|
|
|
|
|
var givenUserId uint
|
|
|
|
if givenUserIdString == "self" {
|
|
|
|
givenUserId = d.user.ID
|
|
|
|
} else if givenUserIdString == "" {
|
|
|
|
if !d.user.Perm.Admin {
|
|
|
|
return http.StatusForbidden, nil
|
|
|
|
}
|
|
|
|
users, err := store.Users.Gets(config.Server.Root)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
|
|
|
|
for _, u := range users {
|
|
|
|
u.Password = ""
|
|
|
|
}
|
|
|
|
for _, u := range users {
|
|
|
|
u.ApiKeys = nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
sort.Slice(users, func(i, j int) bool {
|
|
|
|
return users[i].ID < users[j].ID
|
|
|
|
})
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
return renderJSON(w, r, users)
|
|
|
|
} else {
|
|
|
|
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
|
|
|
|
givenUserId = uint(num)
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
if givenUserId != d.user.ID && !d.user.Perm.Admin {
|
|
|
|
return http.StatusForbidden, nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Fetch the user details
|
|
|
|
u, err := store.Users.Get(config.Server.Root, givenUserId)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err == errors.ErrNotExist {
|
|
|
|
return http.StatusNotFound, err
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Remove the password from the response if the user is not an admin
|
2019-01-05 22:44:33 +00:00
|
|
|
u.Password = ""
|
2024-11-21 00:15:30 +00:00
|
|
|
u.ApiKeys = nil
|
2022-02-22 09:58:22 +00:00
|
|
|
if !d.user.Perm.Admin {
|
2022-02-21 19:17:42 +00:00
|
|
|
u.Scope = ""
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
|
2019-01-05 22:44:33 +00:00
|
|
|
return renderJSON(w, r, u)
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// userDeleteHandler deletes a user by ID.
|
|
|
|
// @Summary Delete a user by ID
|
|
|
|
// @Description Deletes a user identified by their ID.
|
|
|
|
// @Tags Users
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param id path int true "User ID"
|
|
|
|
// @Success 200 "User deleted successfully"
|
|
|
|
// @Failure 403 {object} map[string]string "Forbidden"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal Server Error"
|
|
|
|
// @Router /api/users/{id} [delete]
|
|
|
|
func userDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
givenUserIdString := r.URL.Query().Get("id")
|
|
|
|
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
|
|
|
|
givenUserId := uint(num)
|
|
|
|
|
2024-11-21 15:58:28 +00:00
|
|
|
if givenUserId == d.user.ID {
|
|
|
|
return http.StatusForbidden, fmt.Errorf("cannot delete your own user")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !d.user.Perm.Admin {
|
|
|
|
return http.StatusForbidden, fmt.Errorf("cannot delete users without admin permissions")
|
|
|
|
}
|
|
|
|
|
|
|
|
if givenUserId == 1 {
|
|
|
|
return http.StatusForbidden, fmt.Errorf("cannot delete the default admin user")
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Delete the user
|
|
|
|
err := store.Users.Delete(givenUserId)
|
2021-01-11 21:33:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return errToStatus(err), err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
return http.StatusOK, nil
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// usersPostHandler creates a new user.
|
|
|
|
// @Summary Create a new user
|
|
|
|
// @Description Adds a new user to the system.
|
|
|
|
// @Tags Users
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param data body users.User true "User data to create a new user"
|
|
|
|
// @Success 201 {object} users.User "Created user"
|
|
|
|
// @Failure 400 {object} map[string]string "Bad Request"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal Server Error"
|
|
|
|
// @Router /api/users [post]
|
|
|
|
func usersPostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
if !d.user.Perm.Admin {
|
|
|
|
return http.StatusForbidden, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the user's scope
|
|
|
|
_, _, err := files.GetRealPath(config.Server.Root, d.user.Scope)
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Read the JSON body
|
|
|
|
body, err := io.ReadAll(r.Body)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
// Parse the JSON into the UserRequest struct
|
|
|
|
var req UserRequest
|
|
|
|
if err = json.Unmarshal(body, &req); err != nil {
|
2019-01-05 22:44:33 +00:00
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(req.Which) != 0 {
|
|
|
|
return http.StatusBadRequest, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.Data.Password == "" {
|
|
|
|
return http.StatusBadRequest, errors.ErrEmptyPassword
|
|
|
|
}
|
|
|
|
|
2024-10-07 22:44:53 +00:00
|
|
|
err = storage.CreateUser(*req.Data, req.Data.Perm.Admin)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2023-02-16 08:11:12 +00:00
|
|
|
w.Header().Set("Location", "/settings/users/"+strconv.FormatUint(uint64(req.Data.ID), 10))
|
2019-01-05 22:44:33 +00:00
|
|
|
return http.StatusCreated, nil
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// userPutHandler updates an existing user's details.
|
|
|
|
// @Summary Update a user's details
|
|
|
|
// @Description Updates the details of a user identified by ID.
|
|
|
|
// @Tags Users
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param id path int true "User ID"
|
|
|
|
// @Param data body users.User true "User data to update"
|
|
|
|
// @Success 200 {object} users.User "Updated user details"
|
|
|
|
// @Failure 400 {object} map[string]string "Bad Request"
|
|
|
|
// @Failure 403 {object} map[string]string "Forbidden"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal Server Error"
|
|
|
|
// @Router /api/users/{id} [put]
|
|
|
|
func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
givenUserIdString := r.URL.Query().Get("id")
|
|
|
|
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
|
|
|
|
givenUserId := uint(num)
|
|
|
|
|
|
|
|
if givenUserId != d.user.ID && !d.user.Perm.Admin {
|
|
|
|
return http.StatusForbidden, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the user's scope
|
|
|
|
_, _, err := files.GetRealPath(config.Server.Root, d.user.Scope)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Read the JSON body
|
|
|
|
body, err := io.ReadAll(r.Body)
|
2024-08-24 22:02:33 +00:00
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
return http.StatusInternalServerError, err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
defer r.Body.Close()
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Parse the JSON into the UserRequest struct
|
|
|
|
var req UserRequest
|
|
|
|
if err = json.Unmarshal(body, &req); err != nil {
|
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If `Which` is not specified, default to updating all fields
|
2024-08-03 15:34:12 +00:00
|
|
|
if len(req.Which) == 0 || req.Which[0] == "all" {
|
|
|
|
req.Which = []string{}
|
|
|
|
v := reflect.ValueOf(req.Data)
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
|
|
v = v.Elem()
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-08-03 15:34:12 +00:00
|
|
|
t := v.Type()
|
2024-11-21 00:15:30 +00:00
|
|
|
|
|
|
|
// Dynamically populate fields to update
|
2024-08-03 15:34:12 +00:00
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
field := t.Field(i)
|
2024-09-16 21:01:16 +00:00
|
|
|
if field.Name == "Password" && req.Data.Password != "" {
|
|
|
|
req.Which = append(req.Which, field.Name)
|
|
|
|
} else if field.Name != "Password" && field.Name != "Fs" {
|
2024-08-03 15:34:12 +00:00
|
|
|
req.Which = append(req.Which, field.Name)
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Process the fields to update
|
|
|
|
for _, field := range req.Which {
|
|
|
|
// Title case field names
|
|
|
|
field = cases.Title(language.English, cases.NoLower).String(field)
|
|
|
|
|
|
|
|
// Handle password update
|
|
|
|
if field == "Password" {
|
2019-01-05 22:44:33 +00:00
|
|
|
if !d.user.Perm.Admin && d.user.LockPassword {
|
|
|
|
return http.StatusForbidden, nil
|
|
|
|
}
|
|
|
|
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Prevent non-admins from modifying certain fields
|
|
|
|
for _, restrictedField := range NonModifiableFieldsForNonAdmin {
|
|
|
|
if !d.user.Perm.Admin && field == restrictedField {
|
2020-12-24 17:22:48 +00:00
|
|
|
return http.StatusForbidden, nil
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
// Perform the user update
|
|
|
|
err = store.Users.Update(req.Data, req.Which...)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Return the updated user (with the password hidden) as JSON response
|
|
|
|
req.Data.Password = ""
|
|
|
|
return renderJSON(w, r, req.Data)
|
|
|
|
}
|