From 4cf84192926ca221014195b08aeb1a637f9e2ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Galicz?= Date: Tue, 13 Aug 2024 07:50:49 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20:recycle:=20refactor=20feedback=20f?= =?UTF-8?q?orm=20handling=20logic=20for=20improve=E2=80=A6=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary by CodeRabbit - **New Features** - Introduced a feedback handling module that allows users to submit feedback through a form. - Implemented validation and error handling for user feedback submissions. - Added structured feedback form rendering and processing. - **Improvements** - Enhanced modularity of feedback handling by reorganizing handler registrations. - Updated event handling syntax in the feedback form template for improved compatibility. --- assets/templ/components/feedback.templ | 2 +- errs/form.go | 5 + errs/honeypot.go | 5 + handlers/feedback.go | 138 -------------------- handlers/feedback/main.go | 168 +++++++++++++++++++++++++ handlers/main.go | 3 +- 6 files changed, 181 insertions(+), 140 deletions(-) create mode 100644 errs/form.go create mode 100644 errs/honeypot.go delete mode 100644 handlers/feedback.go create mode 100644 handlers/feedback/main.go diff --git a/assets/templ/components/feedback.templ b/assets/templ/components/feedback.templ index 1cf6f3c4..84a6ab0e 100644 --- a/assets/templ/components/feedback.templ +++ b/assets/templ/components/feedback.templ @@ -54,7 +54,7 @@ templ FeedbackForm() { - diff --git a/errs/form.go b/errs/form.go new file mode 100644 index 00000000..43e5c594 --- /dev/null +++ b/errs/form.go @@ -0,0 +1,5 @@ +package errs + +import "errors" + +var ErrMessageRequired = errors.New("message required") diff --git a/errs/honeypot.go b/errs/honeypot.go new file mode 100644 index 00000000..8a5fbf6f --- /dev/null +++ b/errs/honeypot.go @@ -0,0 +1,5 @@ +package errs + +import "errors" + +var ErrHoneypotTriggered = errors.New("honeypot triggered") diff --git a/handlers/feedback.go b/handlers/feedback.go deleted file mode 100644 index 0ef9165e..00000000 --- a/handlers/feedback.go +++ /dev/null @@ -1,138 +0,0 @@ -package handlers - -import ( - "context" - "fmt" - - "github.com/blackfyre/wga/assets/templ/components" - "github.com/blackfyre/wga/utils" - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" -) - -type feedbackForm struct { - Email string `json:"email" form:"fp_email" query:"email"` - Message string `json:"message" form:"message" query:"message"` - Name string `json:"name" form:"fp_name" query:"name"` - HoneyPotName string `json:"honey_pot_name" form:"name" query:"honey_pot_name"` - HoneyPotEmail string `json:"honey_pot_email" form:"email" query:"honey_pot_email"` - ReferTo string `json:"refer_to"` -} - -func validateFeedbackForm(form feedbackForm) error { - - if form.HoneyPotEmail != "" || form.HoneyPotName != "" { - return fmt.Errorf("failed to parse form") - } - - if form.Email == "" { - return fmt.Errorf("email is required") - } - - if form.Message == "" { - return fmt.Errorf("message is required") - } - - return nil -} - -// registerFeedbackHandlers registers the feedback handlers for the application. -// It adds the GET and POST routes for the feedback form, handles form submission, -// and stores the feedback in the database. -// -// Parameters: -// - app: The PocketBase application instance. -// - p: The bluemonday policy for sanitizing HTML input. -// -// Returns: -// - An error if there was a problem registering the handlers, or nil otherwise. -func registerFeedbackHandlers(app *pocketbase.PocketBase) { - - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.GET("/feedback", func(c echo.Context) error { - return presentFeedbackForm(c, app) - }, utils.IsHtmxRequestMiddleware) - - e.Router.POST("/feedback", func(c echo.Context) error { - - postData := feedbackForm{ - ReferTo: c.Request().Header.Get("Referer"), - } - - if err := c.Bind(&postData); err != nil { - app.Logger().Error("Failed to parse form data", "error", err.Error()) - utils.SendToastMessage("Failed to parse form", "error", true, c, "") - return utils.ServerFaultError(c) - } - - if err := validateFeedbackForm(postData); err != nil { - app.Logger().Error("Failed to validate form data", "error", err.Error()) - utils.SendToastMessage(err.Error(), "error", true, c, "") - - if err == fmt.Errorf("failed to parse form") { - app.Logger().Error("Bot caught in honeypot", "error", err.Error()) - } - - return utils.ServerFaultError(c) - } - - collection, err := app.Dao().FindCollectionByNameOrId("feedbacks") - if err != nil { - app.Logger().Error("Database table not found", "error", err.Error()) - utils.SendToastMessage("Database table not found", "error", true, c, "") - return utils.ServerFaultError(c) - } - - record := models.NewRecord(collection) - - form := forms.NewRecordUpsert(app, record) - - err = form.LoadData(map[string]any{ - "email": postData.Email, - "name": postData.Name, - "message": postData.Message, - "refer_to": postData.ReferTo, - }) - if err != nil { - app.Logger().Error("Failed to process the feedback", "error", err.Error()) - return err - } - - if err := form.Submit(); err != nil { - - app.Logger().Error("Failed to store the feedback", "error", err.Error()) - - err := components.FeedbackForm().Render(context.Background(), c.Response().Writer) - - if err != nil { - app.Logger().Error("Failed to render the feedback form after form submission error", "error", err.Error()) - return utils.ServerFaultError(c) - } - - utils.SendToastMessage("Failed to store the feedback", "error", false, c, "") - - return utils.ServerFaultError(c) - } - - utils.SendToastMessage("Thank you! Your feedback is valuable to us!", "success", true, c, "") - - return nil - }, utils.IsHtmxRequestMiddleware) - - return nil - }) -} - -func presentFeedbackForm(c echo.Context, app *pocketbase.PocketBase) error { - err := components.FeedbackForm().Render(context.Background(), c.Response().Writer) - - if err != nil { - app.Logger().Error("Failed to render the feedback form", "error", err.Error()) - return utils.ServerFaultError(c) - } - - return err -} diff --git a/handlers/feedback/main.go b/handlers/feedback/main.go new file mode 100644 index 00000000..4bf0c5c3 --- /dev/null +++ b/handlers/feedback/main.go @@ -0,0 +1,168 @@ +package feedback + +import ( + "context" + "errors" + + "github.com/blackfyre/wga/assets/templ/components" + "github.com/blackfyre/wga/errs" + "github.com/blackfyre/wga/utils" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +type feedbackForm struct { + Email string `json:"email" form:"fp_email" query:"email"` + Message string `json:"message" form:"message" query:"message"` + Name string `json:"name" form:"fp_name" query:"name"` + HoneyPotName string `json:"honey_pot_name" form:"name" query:"honey_pot_name"` + HoneyPotEmail string `json:"honey_pot_email" form:"email" query:"honey_pot_email"` + ReferTo string `json:"refer_to"` +} + +// validateFeedbackForm validates the feedback form. +// It checks if the honey pot fields are empty and returns an error if they are not. +// It also checks if the email and message fields are empty and returns an error if they are. +// If all validations pass, it returns nil. +func validateFeedbackForm(form feedbackForm) error { + if form.HoneyPotName != "" || form.HoneyPotEmail != "" { + return errs.ErrHoneypotTriggered + } + + if form.Message == "" { + return errs.ErrMessageRequired + } + + return nil +} + +// presentFeedbackForm is a function that presents a feedback form to the user. +// It takes an echo.Context and a *pocketbase.PocketBase as parameters. +// It renders the feedback form using the components.FeedbackForm() function. +// If there is an error during rendering, it logs the error and returns a server fault error. +// Otherwise, it returns nil. +func presentFeedbackForm(c echo.Context, app *pocketbase.PocketBase) error { + err := components.FeedbackForm().Render(context.Background(), c.Response().Writer) + + if err != nil { + app.Logger().Error("Failed to render the feedback form", "error", err.Error()) + return utils.ServerFaultError(c) + } + + return err +} + +// processFeedbackForm processes the feedback form submitted by the user. +// It takes an echo.Context and a *pocketbase.PocketBase as parameters. +// The function binds the form data to the feedbackForm struct and validates it. +// If the form data fails to parse or validate, an error is logged and a server fault error is returned. +// If the form data is valid, it is saved using the saveFeedback function. +// If there is an error while saving the feedback, the feedback form is rendered again and a server fault error is returned. +// If the feedback is successfully saved, a success toast message is sent to the user. +// The function returns nil if there are no errors. +func processFeedbackForm(c echo.Context, app *pocketbase.PocketBase) error { + postData := feedbackForm{ + ReferTo: c.Request().Header.Get("Referer"), + } + + if err := c.Bind(&postData); err != nil { + app.Logger().Error("Failed to parse form data", "error", err.Error()) + utils.SendToastMessage("Failed to parse form", "error", true, c, "") + return utils.ServerFaultError(c) + } + + if err := validateFeedbackForm(postData); err != nil { + app.Logger().Error("Failed to validate form data", "error", err.Error()) + utils.SendToastMessage(err.Error(), "error", true, c, "") + + if errors.Is(err, errs.ErrHoneypotTriggered) { + app.Logger().Error("Bot caught in honeypot", "error", err.Error()) + } + + return utils.ServerFaultError(c) + } + + if err := saveFeedback(app, c, postData); err != nil { + + app.Logger().Error("Failed to store the feedback", "error", err.Error()) + + err := components.FeedbackForm().Render(context.Background(), c.Response().Writer) + + if err != nil { + app.Logger().Error("Failed to render the feedback form after form submission error", "error", err.Error()) + return utils.ServerFaultError(c) + } + + utils.SendToastMessage("Failed to store the feedback", "error", false, c, "") + + return utils.ServerFaultError(c) + } + + utils.SendToastMessage("Thank you! Your feedback is valuable to us!", "success", true, c, "") + + return nil +} + +// saveFeedback saves the feedback provided by the user. +// It takes the PocketBase app instance, the echo.Context c, and the postData feedbackForm as parameters. +// It returns an error if there is any issue with saving the feedback. +// +// The function first attempts to find the "feedbacks" collection in the database using the app.Dao().FindCollectionByNameOrId method. +// If the collection is not found, it logs an error and sends a toast message to the user indicating the issue. +// It then returns a server fault error using the utils.ServerFaultError function. +// +// Next, it creates a new record using the models.NewRecord method and the retrieved collection. +// +// It creates a new form using the forms.NewRecordUpsert method and the app instance and the created record. +// +// The function loads the data from the postData into the form using the form.LoadData method. +// If there is an error during the data loading process, it logs an error and returns the error. +// +// Finally, it submits the form using the form.Submit method and returns the result. +func saveFeedback(app *pocketbase.PocketBase, c echo.Context, postData feedbackForm) error { + collection, err := app.Dao().FindCollectionByNameOrId("feedbacks") + if err != nil { + app.Logger().Error("Database table not found", "error", err.Error()) + utils.SendToastMessage("Database table not found", "error", true, c, "") + return utils.ServerFaultError(c) + } + + record := models.NewRecord(collection) + + form := forms.NewRecordUpsert(app, record) + + err = form.LoadData(map[string]any{ + "email": postData.Email, + "name": postData.Name, + "message": postData.Message, + "refer_to": postData.ReferTo, + }) + if err != nil { + app.Logger().Error("Failed to process the feedback", "error", err.Error()) + return err + } + + return form.Submit() +} + +// RegisterHandlers registers the feedback handlers to the provided PocketBase application. +// It adds GET and POST routes for "/feedback" endpoint, which are responsible for presenting and processing feedback forms. +// The handlers use the given echo.Context and PocketBase app to handle the requests. +// The handlers also utilize the IsHtmxRequestMiddleware from the utils package. +// This function should be called before serving the application. +func RegisterHandlers(app *pocketbase.PocketBase) { + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.GET("/feedback", func(c echo.Context) error { + return presentFeedbackForm(c, app) + }, utils.IsHtmxRequestMiddleware) + + e.Router.POST("/feedback", func(c echo.Context) error { + return processFeedbackForm(c, app) + }, utils.IsHtmxRequestMiddleware) + + return nil + }) +} diff --git a/handlers/main.go b/handlers/main.go index 56d9d123..f89500bc 100644 --- a/handlers/main.go +++ b/handlers/main.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/blackfyre/wga/handlers/artworks" + "github.com/blackfyre/wga/handlers/feedback" "github.com/blackfyre/wga/handlers/inspire" "github.com/blackfyre/wga/handlers/postcards" "github.com/microcosm-cc/bluemonday" @@ -16,7 +17,7 @@ func RegisterHandlers(app *pocketbase.PocketBase) { p := bluemonday.NewPolicy() - registerFeedbackHandlers(app) + feedback.RegisterHandlers(app) registerMusicHandlers(app) registerGuestbookHandlers(app) registerArtist(app)