package user import ( "fmt" "net/http" "codeberg.org/u1f320/pronouns.cc/backend/db" "codeberg.org/u1f320/pronouns.cc/backend/log" "codeberg.org/u1f320/pronouns.cc/backend/server" "emperror.dev/errors" "github.com/go-chi/render" ) type PatchUserRequest struct { DisplayName *string `json:"display_name"` Bio *string `json:"bio"` Links *[]string `json:"links"` Names *[]db.Name `json:"names"` Pronouns *[]db.Pronoun `json:"pronouns"` Fields *[]db.Field `json:"fields"` } // patchUser parses a PatchUserRequest and updates the user with the given ID. // TODO: could this be refactored to be less repetitive? names, pronouns, and fields are all validated in the same way func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() claims, _ := server.ClaimsFromContext(ctx) var req PatchUserRequest err := render.Decode(r, &req) if err != nil { return server.APIError{Code: server.ErrBadRequest} } // validate that *something* is set if req.DisplayName == nil && req.Bio == nil && req.Links == nil && req.Fields == nil { return server.APIError{ Code: server.ErrBadRequest, Details: "Data must not be empty", } } // validate display name/bio if req.DisplayName != nil && len(*req.DisplayName) > db.MaxDisplayNameLength { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("Display name too long (max %d, current %d)", db.MaxDisplayNameLength, len(*req.DisplayName)), } } if req.Bio != nil && len(*req.Bio) > db.MaxUserBioLength { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("Bio too long (max %d, current %d)", db.MaxUserBioLength, len(*req.Bio)), } } // validate links if req.Links != nil { if len(*req.Links) > db.MaxUserLinksLength { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("Too many links (max %d, current %d)", db.MaxUserLinksLength, len(*req.Links)), } } for i, link := range *req.Links { if len(link) > db.MaxLinkLength { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("Link %d too long (max %d, current %d)", i, db.MaxLinkLength, len(link)), } } } } if err := validateSlicePtr("name", req.Names); err != nil { return err } if err := validateSlicePtr("pronoun", req.Pronouns); err != nil { return err } if err := validateSlicePtr("field", req.Fields); err != nil { return err } // start transaction tx, err := s.DB.Begin(ctx) if err != nil { log.Errorf("creating transaction: %v", err) return err } defer tx.Rollback(ctx) u, err := s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.Links) if err != nil && errors.Cause(err) != db.ErrNothingToUpdate { log.Errorf("updating user: %v", err) return err } var ( names []db.Name pronouns []db.Pronoun fields []db.Field ) if req.Fields != nil { err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields) if err != nil { log.Errorf("setting fields for user %v: %v", claims.UserID, err) return err } } else { fields, err = s.DB.UserFields(ctx, claims.UserID) if err != nil { log.Errorf("getting fields for user %v: %v", claims.UserID, err) return err } } err = tx.Commit(ctx) if err != nil { log.Errorf("committing transaction: %v", err) return err } // echo the updated user back on success render.JSON(w, r, dbUserToResponse(u, fields, names, pronouns)) return nil } type validator interface { Validate() string } // validateSlicePtr validates a slice of validators. // If the slice is nil, a nil error is returned (assuming that the field is not required) func validateSlicePtr[T validator](typ string, slice *[]T) error { if slice == nil { return nil } // max 25 fields if len(*slice) > db.MaxFields { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("Too many %ss (max %d, current %d)", typ, db.MaxFields, len(*slice)), } } // validate all fields for i, pronouns := range *slice { if s := pronouns.Validate(); s != "" { return server.APIError{ Code: server.ErrBadRequest, Details: fmt.Sprintf("%s %d: %s", typ, i, s), } } } return nil }