diff --git a/app/http/endpoints/api/forms/createinput.go b/app/http/endpoints/api/forms/createinput.go index a6b1581..d270840 100644 --- a/app/http/endpoints/api/forms/createinput.go +++ b/app/http/endpoints/api/forms/createinput.go @@ -9,13 +9,6 @@ import ( "strconv" ) -type inputCreateBody struct { - Style component.TextStyleTypes `json:"style"` - Label string `json:"label"` - Placeholder *string `json:"placeholder"` - Optional bool `json:"optional"` -} - func CreateInput(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) @@ -70,7 +63,7 @@ func CreateInput(ctx *gin.Context) { // 2^30 chance of collision customId := utils.RandString(30) - formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, !data.Optional) + formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, data.Required) if err != nil { ctx.JSON(500, utils.ErrorJson(err)) return @@ -83,7 +76,7 @@ func CreateInput(ctx *gin.Context) { Style: uint8(data.Style), Label: data.Label, Placeholder: data.Placeholder, - Required: !data.Optional, + Required: data.Required, }) } diff --git a/app/http/endpoints/api/forms/updateform.go b/app/http/endpoints/api/forms/updateform.go index d215063..8142382 100644 --- a/app/http/endpoints/api/forms/updateform.go +++ b/app/http/endpoints/api/forms/updateform.go @@ -16,7 +16,7 @@ func UpdateForm(ctx *gin.Context) { return } - if len(data.Title) > 255 { + if len(data.Title) > 45 { ctx.JSON(400, utils.ErrorStr("Title is too long")) return } @@ -29,9 +29,9 @@ func UpdateForm(ctx *gin.Context) { form, ok, err := dbclient.Client.Forms.Get(formId) if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) - return - } + ctx.JSON(500, utils.ErrorJson(err)) + return + } if !ok { ctx.JSON(404, utils.ErrorStr("Form not found")) @@ -40,13 +40,13 @@ func UpdateForm(ctx *gin.Context) { if form.GuildId != guildId { ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild")) - return + return } if err := dbclient.Client.Forms.UpdateTitle(formId, data.Title); err != nil { - ctx.JSON(500, utils.ErrorJson(err)) - return - } + ctx.JSON(500, utils.ErrorJson(err)) + return + } ctx.JSON(200, utils.SuccessResponse) } diff --git a/app/http/endpoints/api/forms/updateinput.go b/app/http/endpoints/api/forms/updateinput.go index 066d9d9..7d2ff9c 100644 --- a/app/http/endpoints/api/forms/updateinput.go +++ b/app/http/endpoints/api/forms/updateinput.go @@ -72,7 +72,7 @@ func UpdateInput(ctx *gin.Context) { Style: uint8(data.Style), Label: data.Label, Placeholder: data.Placeholder, - Required: !data.Optional, + Required: data.Required, } if err := dbclient.Client.FormInput.Update(newInput); err != nil { diff --git a/app/http/endpoints/api/forms/updateinputs.go b/app/http/endpoints/api/forms/updateinputs.go new file mode 100644 index 0000000..d53b68a --- /dev/null +++ b/app/http/endpoints/api/forms/updateinputs.go @@ -0,0 +1,244 @@ +package forms + +import ( + "context" + "fmt" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/database" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/rxdn/gdl/objects/interaction/component" + "sort" + "strconv" + "strings" +) + +type ( + updateInputsBody struct { + Create []inputCreateBody `json:"create" validate:"omitempty,dive"` + Update []inputUpdateBody `json:"update" validate:"omitempty,dive"` + Delete []int `json:"delete" validate:"omitempty"` + } + + inputCreateBody struct { + Label string `json:"label" validate:"required,min=1,max=45"` + Placeholder *string `json:"placeholder,omitempty" validate:"omitempty,min=1,max=100"` + Position int `json:"position" validate:"required,min=1,max=5"` + Style component.TextStyleTypes `json:"style" validate:"required,min=1,max=2"` + Required bool `json:"required"` + } + + inputUpdateBody struct { + Id int `json:"id" validate:"required"` + inputCreateBody `validate:"required,dive"` + } +) + +var validate = validator.New() + +func UpdateInputs(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + + formId, err := strconv.Atoi(ctx.Param("form_id")) + if err != nil { + ctx.JSON(400, utils.ErrorStr("Invalid form ID")) + return + } + + var data updateInputsBody + if err := ctx.BindJSON(&data); err != nil { + ctx.JSON(400, utils.ErrorJson(err)) + return + } + + if err := validate.Struct(data); err != nil { + validationErrors, ok := err.(validator.ValidationErrors) + if !ok { + ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration")) + return + } + + formatted := "Your input contained the following errors:" + for _, validationError := range validationErrors { + formatted += fmt.Sprintf("\n%s", validationError.Error()) + } + + formatted = strings.TrimSuffix(formatted, "\n") + ctx.JSON(400, utils.ErrorStr(formatted)) + return + } + + fieldCount := len(data.Create) + len(data.Update) + if fieldCount <= 0 || fieldCount > 5 { + ctx.JSON(400, utils.ErrorStr("Forms must have between 1 and 5 inputs")) + return + } + + // Verify form exists and is from the right guild + form, ok, err := dbclient.Client.Forms.Get(formId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if !ok { + ctx.JSON(404, utils.ErrorStr("Form not found")) + return + } + + if form.GuildId != guildId { + ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild")) + return + } + + existingInputs, err := dbclient.Client.FormInput.GetInputs(formId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + // Verify that the UPDATE inputs exist + for _, input := range data.Update { + if !utils.ExistsMap(existingInputs, input.Id, idMapper) { + ctx.JSON(400, utils.ErrorStr("Input (to be updated) not found")) + return + } + } + + // Verify that the DELETE inputs exist + for _, id := range data.Delete { + if !utils.ExistsMap(existingInputs, id, idMapper) { + ctx.JSON(400, utils.ErrorStr("Input (to be deleted) not found")) + return + } + } + + // Ensure no overlap between DELETE and UPDATE + for _, id := range data.Delete { + if utils.ExistsMap(data.Update, id, idMapperBody) { + ctx.JSON(400, utils.ErrorStr("Delete and update overlap")) + return + } + } + + // Verify that we are updating ALL inputs, excluding the ones to be deleted + var remainingExisting []int + for _, input := range existingInputs { + if !utils.Exists(data.Delete, input.Id) { + remainingExisting = append(remainingExisting, input.Id) + } + } + + // Now verify that the contents match exactly + if len(remainingExisting) != len(data.Update) { + ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array")) + return + } + + for _, input := range data.Update { + if !utils.Exists(remainingExisting, input.Id) { + ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array")) + return + } + } + + // Verify that the positions are unique, and are in ascending order + if !arePositionsCorrect(data) { + ctx.JSON(400, utils.ErrorStr("Positions must be unique and in ascending order")) + return + } + + if err := saveInputs(formId, data, existingInputs); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + ctx.Status(204) +} + +func idMapper(input database.FormInput) int { + return input.Id +} + +func idMapperBody(input inputUpdateBody) int { + return input.Id +} + +func arePositionsCorrect(body updateInputsBody) bool { + var positions []int + for _, input := range body.Create { + positions = append(positions, input.Position) + } + + for _, input := range body.Update { + positions = append(positions, input.Position) + } + + sort.Slice(positions, func(i, j int) bool { + return positions[i] < positions[j] + }) + + for i, position := range positions { + if i+1 != position { + return false + } + } + + return true +} + +func saveInputs(formId int, data updateInputsBody, existingInputs []database.FormInput) error { + // We can now update in the database + tx, err := dbclient.Client.BeginTx() + if err != nil { + return err + } + + defer tx.Rollback(context.Background()) + + for _, id := range data.Delete { + if err := dbclient.Client.FormInput.DeleteTx(tx, id, formId); err != nil { + return err + } + } + + for _, input := range data.Update { + existing := utils.FindMap(existingInputs, input.Id, idMapper) + if existing == nil { + return fmt.Errorf("input %d does not exist", input.Id) + } + + wrapped := database.FormInput{ + Id: input.Id, + FormId: formId, + Position: input.Position, + CustomId: existing.CustomId, + Style: uint8(input.Style), + Label: input.Label, + Placeholder: input.Placeholder, + Required: input.Required, + } + + if err := dbclient.Client.FormInput.UpdateTx(tx, wrapped); err != nil { + return err + } + } + + for _, input := range data.Create { + if _, err := dbclient.Client.FormInput.CreateTx( + tx, + formId, + utils.RandString(30), + input.Position, + uint8(input.Style), + input.Label, + input.Placeholder, + input.Required, + ); err != nil { + return err + } + } + + return tx.Commit(context.Background()) +} diff --git a/app/http/server.go b/app/http/server.go index 7daa7f7..e6dda27 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -123,6 +123,7 @@ func StartServer() { guildAuthApiAdmin.PATCH("/forms/:form_id", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.UpdateForm) guildAuthApiAdmin.DELETE("/forms/:form_id", api_forms.DeleteForm) guildAuthApiAdmin.POST("/forms/:form_id", api_forms.CreateInput) + guildAuthApiAdmin.PATCH("/forms/:form_id/inputs", api_forms.UpdateInputs) guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id", api_forms.UpdateInput) guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput) guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput) diff --git a/frontend/src/components/manage/FormInputRow.svelte b/frontend/src/components/manage/FormInputRow.svelte index be3fd75..38fd356 100644 --- a/frontend/src/components/manage/FormInputRow.svelte +++ b/frontend/src/components/manage/FormInputRow.svelte @@ -17,11 +17,6 @@ {/if} - {#if withSaveButton} -
- {/if} {#if withDeleteButton}