2024-11-02 21:36:07 +10:00

334 lines
9.3 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"time"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/config"
"npm/internal/entity/auth"
"npm/internal/entity/user"
"npm/internal/errors"
"npm/internal/logger"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
type setAuthModel struct {
// The json tags are required, as the change password form decodes into this object
Type string `json:"type"`
Secret string `json:"secret"`
CurrentSecret string `json:"current_secret"`
}
// GetUsers returns all users
// Route: GET /users
func GetUsers() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, users)
}
}
}
// GetUser returns a specific user
// Route: GET /users/{userID}
func GetUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID, _, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
item, err := user.GetByID(userID)
switch err {
case gorm.ErrRecordNotFound:
h.NotFound(w, r)
case nil:
// nolint: errcheck,gosec
item.Expand(middleware.GetExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, item)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
// UpdateUser updates a user
// Route: PUT /users/{userID}
func UpdateUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID, self, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
userObject, err := user.GetByID(userID)
switch err {
case gorm.ErrRecordNotFound:
h.NotFound(w, r)
case nil:
// nolint: errcheck,gosec
userObject.Expand([]string{"capabilities"})
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &userObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if userObject.IsDisabled && self {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil)
return
}
if err = userObject.Save(); err != nil {
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
logger.Error("UpdateUserError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
}
return
}
if !self {
err = userObject.SaveCapabilities()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
}
// nolint: errcheck,gosec
userObject.Expand(middleware.GetExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
// DeleteUser removes a user
// Route: DELETE /users/{userID}
func DeleteUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var userID uint
var err error
if userID, err = getURLParamInt(r, "userID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
myUserID, _ := r.Context().Value(c.UserIDCtxKey).(uint)
if myUserID == userID {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil)
return
}
item, err := user.GetByID(userID)
switch err {
case gorm.ErrRecordNotFound:
h.NotFound(w, r)
case nil:
if err := item.Delete(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, true)
}
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
// CreateUser creates a user
// Route: POST /users
func CreateUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newUser user.Model
err := json.Unmarshal(bodyBytes, &newUser)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newUser.Save(); err != nil {
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
logger.Error("UpdateUserError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
}
return
}
// Set the permissions to full-admin for this user
if !config.IsSetup {
newUser.Capabilities = []string{user.CapabilityFullAdmin}
}
// nolint: errcheck,gosec
err = newUser.SaveCapabilities()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
// newUser has been saved, now save their auth
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
newUser.Auth.UserID = newUser.ID
if newUser.Auth.Type == auth.TypeLocal {
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
if err != nil {
logger.Error("SetPasswordError", err)
}
}
if err = newUser.Auth.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
return
}
newUser.Auth.Secret = ""
}
if !config.IsSetup {
config.IsSetup = true
logger.Info("A new user was created, leaving Setup Mode")
}
h.ResultResponseJSON(w, r, http.StatusOK, newUser)
}
}
// DeleteUsers is only available in debug mode for cypress tests
// Route: DELETE /users
func DeleteUsers() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
err := user.DeleteAll()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// also change setup to true
config.IsSetup = false
logger.Info("Users have been wiped, entering Setup Mode")
h.ResultResponseJSON(w, r, http.StatusOK, true)
}
}
}
func getUserIDFromRequest(r *http.Request) (uint, bool, error) {
userIDstr := chi.URLParam(r, "userID")
selfUserID, _ := r.Context().Value(c.UserIDCtxKey).(uint)
var userID uint
self := false
if userIDstr == "me" {
// Get user id from Token
userID = selfUserID
self = true
} else {
var userIDerr error
if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil {
return 0, false, userIDerr
}
self = selfUserID == userID
}
return userID, self, nil
}
// SetAuth sets a auth method. This can be used for "me" and `2` for example
// Route: POST /users/:userID/auth
func SetAuth() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newAuth setAuthModel
err := json.Unmarshal(bodyBytes, &newAuth)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
userID, isSelf, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
// Load user
thisUser, thisUserErr := user.GetByID(userID)
if thisUserErr == gorm.ErrRecordNotFound {
h.NotFound(w, r)
return
} else if thisUserErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
return
}
if thisUser.IsSystem {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
return
}
// Load existing auth for user
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
if userAuthErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
return
}
if isSelf {
// confirm that the current_secret given is valid for the one stored in the database
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
if validateErr != nil {
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
// Sleep for 1 second to prevent brute force password guessing
time.Sleep(time.Second)
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
return
}
}
if newAuth.Type == auth.TypeLocal {
err := userAuth.SetPassword(newAuth.Secret)
if err != nil {
logger.Error("SetPasswordError", err)
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
if err = userAuth.Save(); err != nil {
logger.Error("AuthSaveError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
return
}
userAuth.Secret = ""
// todo: add to audit-log
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
}
}