diff --git a/README.md b/README.md index b7bca7c..7aefe2e 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ If you wish to require either authentication or non-authentication for a given r ### Email verification -Most web applications require the user to verify their email address (or other form of contact information). The `User` entity has a field `Verified` to indicate if they have verified themself. When a user successfully registers, an email is sent to them containing a link with a token that will verify their account when visited. This route is currently accessible at `/email/verify/:token` and handled by `routes/VerifyEmail`. +Most web applications require the user to verify their email address (or other form of contact information). The `User` entity has a field `Verified` to indicate if they have verified themself. When a user successfully registers, an email is sent to them containing a link with a token that will verify their account when visited. This route is currently accessible at `/email/verify/:token` and handled by `pkg/handlers/auth.go`. There is currently no enforcement that a `User` must be verified in order to access the application. If that is something you desire, it will have to be added in yourself. It was not included because you may want partial access of certain features until the user verifies; or no access at all. @@ -529,7 +529,6 @@ func (c *home) Get(ctx echo.Context) error { Using the `echo.Context`, the `Page` will be initialized with the following fields populated: - `Context`: The passed in _context_ -- `ToURL`: A function the templates can use to generate a URL with a given route name and parameters - `Path`: The requested URL path - `URL`: The requested URL - `StatusCode`: Defaults to 200 @@ -638,7 +637,7 @@ The `Data` field on the `Page` is of type `any` and is what allows your route to ### Forms -The `Form` field on the `Page` is similar to the `Data` field in that it's an `any` type but it's meant to store a struct that represents a form being rendered on the page. +The `Form` field on the `Page` is similar to the `Data` field, but it's meant to store a struct that represents a form being rendered on the page. An example of this pattern is: @@ -646,10 +645,12 @@ An example of this pattern is: type ContactForm struct { Email string `form:"email" validate:"required,email"` Message string `form:"message" validate:"required"` - Submission form.Submission + form.Submission } ``` +Embedding `form.Submission` satisfies the `form.Form` interface and makes dealing with submissions and validation extremely easy. + Then in your page: ```go @@ -663,41 +664,39 @@ This will either initialize a new form to be rendered, or load one previously st Form submission processing is made extremely simple by leveraging functionality provided by [Echo binding](https://echo.labstack.com/guide/binding/), [validator](https://github.com/go-playground/validator) and the `Submission` struct located in `pkg/form/submission.go`. -Using the example form above, these are the steps you would take within the _POST_ callback for your route: +Using the example form above, this is all you would have to do within the _POST_ callback for your route: + +Start by submitting the form along with the request context. This will: +1. Store a pointer to the form so that your _GET_ callback can access the form values (shown previously). That allows the form to easily be re-rendered with any validation errors it may have as well as the values that were provided. +2. Parse the input in the _POST_ data to map to the struct so the fields becomes populated. This uses the `form` struct tags to map form input values to the struct fields. +3. Validate the values in the struct fields according to the rules provided in the optional `validate` struct tags. -Start by setting the form in the request context. This stores a pointer to the form so that your _GET_ callback can access the form values (shown previously). It also will parse the input in the POST data to map to the struct so it becomes populated. This uses the `form` struct tags to map form values to the struct fields. ```go var input ContactForm -if err := form.Set(ctx, &input); err != nil { +err := form.Submit(ctx, &input) +``` + +Check the error returned, and act accordingly. For example: +```go +switch err.(type) { +case nil: + // All good! +case validator.ValidationErrors: + // The form input was not valid, so re-render the form + return c.Page(ctx) +default: + // Request failed, show the error page return err } ``` -Process the submission which uses [validator](https://github.com/go-playground/validator) to check for validation errors: -```go -if err := input.Submission.Process(ctx, input); err != nil { - // Something went wrong... -} -``` - -Check if the form submission has any validation errors: -```go -if !input.Submission.HasErrors() { - // All good, now execute something! -} -``` - -In the event of a validation error, you most likely want to re-render the form with the values provided and any error messages. Since you stored a pointer to the _form_ in the context in the first step, you can first have the _POST_ handler call the _GET_: -```go -if input.Submission.HasErrors() { - return c.GetCallback(ctx) -} -``` - And finally, your template: ```html - +
+ + +
+ ``` Second, render the error messages, if there are any for a given field: ```go -{{template "field-errors" (.Form.Submission.GetFieldErrors "Email")}} +{{template "field-errors" (.Form.GetFieldErrors "Email")}} ``` ### Headers @@ -768,7 +767,7 @@ e.GET("/user/profile/:user", profile.Get).Name = "user_profile" And you want to generate a URL in the template, you can: ```go -{{call .ToURL "user_profile" 1} +{{url "user_profile" 1} ``` Which will generate: `/user/profile/1` @@ -776,7 +775,7 @@ Which will generate: `/user/profile/1` There is also a helper function provided in the [funcmap](#funcmap) to generate links which has the benefit of adding an _active_ class if the link URL matches the current path. This is especially useful for navigation menus. ```go -{{link (call .ToURL "user_profile" .AuthUser.ID) "Profile" .Path "extra-class"}} +{{link (url "user_profile" .AuthUser.ID) "Profile" .Path "extra-class"}} ``` Will generate: @@ -923,7 +922,7 @@ To make things easier and less repetitive, parameters given to the _template ren The `funcmap` package provides a _function map_ (`template.FuncMap`) which will be included for all templates rendered with the [template renderer](#template-renderer). Aside from a few custom functions, [sprig](https://github.com/Masterminds/sprig) is included which provides over 100 commonly used template functions. The full list is available [here](http://masterminds.github.io/sprig/). -To include additional custom functions, add to the slice in `GetFuncMap()` and define the function in the package. It will then become automatically available in all templates. +To include additional custom functions, add to the map in `NewFuncMap()` and define the function in the package. It will then become automatically available in all templates. ## Cache diff --git a/pkg/controller/page.go b/pkg/controller/page.go index 2e269c9..875adc2 100644 --- a/pkg/controller/page.go +++ b/pkg/controller/page.go @@ -7,6 +7,7 @@ import ( "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/pkg/context" + "github.com/mikestefanello/pagoda/pkg/form" "github.com/mikestefanello/pagoda/pkg/htmx" "github.com/mikestefanello/pagoda/pkg/msg" "github.com/mikestefanello/pagoda/templates" @@ -34,9 +35,6 @@ type Page struct { // Context stores the request context Context echo.Context - // ToURL is a function to convert a route name and optional route parameters to a URL - ToURL func(name string, params ...any) string - // Path stores the path of the current request Path string @@ -50,8 +48,8 @@ type Page struct { // Form stores a struct that represents a form on the page. // This should be a struct with fields for each form field, using both "form" and "validate" tags // It should also contain a Submission field of type FormSubmission if you wish to have validation - // messagesa and markup presented to the user - Form any + // messages and markup presented to the user + Form form.Form // Layout stores the name of the layout base template file which will be used when the page is rendered. // This should match a template file located within the layouts directory inside the templates directory. @@ -67,7 +65,7 @@ type Page struct { // IsHome stores whether the requested page is the home page or not IsHome bool - // IsAuth stores whether or not the user is authenticated + // IsAuth stores whether the user is authenticated IsAuth bool // AuthUser stores the authenticated user @@ -125,7 +123,6 @@ type Page struct { func NewPage(ctx echo.Context) Page { p := Page{ Context: ctx, - ToURL: ctx.Echo().Reverse, Path: ctx.Request().URL.Path, URL: ctx.Request().URL.String(), StatusCode: http.StatusOK, diff --git a/pkg/controller/page_test.go b/pkg/controller/page_test.go index 996985c..e7c0f2f 100644 --- a/pkg/controller/page_test.go +++ b/pkg/controller/page_test.go @@ -17,7 +17,6 @@ func TestNewPage(t *testing.T) { ctx, _ := tests.NewContext(c.Web, "/") p := NewPage(ctx) assert.Same(t, ctx, p.Context) - assert.NotNil(t, p.ToURL) assert.Equal(t, "/", p.Path) assert.Equal(t, "/", p.URL) assert.Equal(t, http.StatusOK, p.StatusCode) diff --git a/pkg/form/form.go b/pkg/form/form.go index 3104ddf..6832d15 100644 --- a/pkg/form/form.go +++ b/pkg/form/form.go @@ -1,13 +1,40 @@ package form import ( - "fmt" - "net/http" - "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/pkg/context" ) +// Form represents a form that can be submitted and validated +type Form interface { + // Submit marks the form as submitted, stores a pointer to it in the context, binds the request + // values to the struct fields, and validates the input based on the struct tags. + // Returns a validator.ValidationErrors if the form values were not valid. + // Returns an echo.HTTPError if the request failed to process. + Submit(c echo.Context, form any) error + + // IsSubmitted returns true if the form was submitted + IsSubmitted() bool + + // IsValid returns true if the form has no validation errors + IsValid() bool + + // IsDone returns true if the form was submitted and has no validation errors + IsDone() bool + + // FieldHasErrors returns true if a given struct field has validation errors + FieldHasErrors(fieldName string) bool + + // SetFieldError sets a validation error message for a given struct field + SetFieldError(fieldName string, message string) + + // GetFieldErrors returns the validation errors for a given struct field + GetFieldErrors(fieldName string) []string + + // GetFieldStatusClass returns a CSS class to be used for a given struct field + GetFieldStatusClass(fieldName string) string +} + // Get gets a form from the context or initializes a new copy if one is not set func Get[T any](ctx echo.Context) *T { if v := ctx.Get(context.FormKey); v != nil { @@ -17,18 +44,13 @@ func Get[T any](ctx echo.Context) *T { return &v } -// Set sets a form in the context and binds the request values to it -func Set(ctx echo.Context, form any) error { - ctx.Set(context.FormKey, form) - - if err := ctx.Bind(form); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("unable to bind form: %v", err)) - } - - return nil -} - // Clear removes the form set in the context func Clear(ctx echo.Context) { ctx.Set(context.FormKey, nil) } + +// Submit submits a form +// See Form.Submit() +func Submit(ctx echo.Context, form Form) error { + return form.Submit(ctx, form) +} diff --git a/pkg/form/form_test.go b/pkg/form/form_test.go index 61a39e0..c297150 100644 --- a/pkg/form/form_test.go +++ b/pkg/form/form_test.go @@ -1,18 +1,34 @@ package form import ( - "net/http" - "net/http/httptest" - "strings" "testing" "github.com/labstack/echo/v4" + "github.com/mikestefanello/pagoda/pkg/context" "github.com/mikestefanello/pagoda/pkg/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestContextFuncs(t *testing.T) { +type mockForm struct { + called bool + Submission +} + +func (m *mockForm) Submit(_ echo.Context, _ any) error { + m.called = true + return nil +} + +func TestSubmit(t *testing.T) { + m := mockForm{} + ctx, _ := tests.NewContext(echo.New(), "/") + err := Submit(ctx, &m) + require.NoError(t, err) + assert.True(t, m.called) +} + +func TestGetClear(t *testing.T) { e := echo.New() type example struct { @@ -26,29 +42,17 @@ func TestContextFuncs(t *testing.T) { assert.NotNil(t, form) }) - t.Run("set bad request", func(t *testing.T) { - // Set with a bad request - req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("abc=abc")) - ctx := e.NewContext(req, httptest.NewRecorder()) - var form example - err := Set(ctx, &form) - assert.Error(t, err) - }) - - t.Run("set", func(t *testing.T) { - // Set and parse the values - req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("name=abc")) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - ctx := e.NewContext(req, httptest.NewRecorder()) - var form example - err := Set(ctx, &form) - require.NoError(t, err) - assert.Equal(t, "abc", form.Name) + t.Run("get non-empty context", func(t *testing.T) { + form := example{ + Name: "test", + } + ctx, _ := tests.NewContext(e, "/") + ctx.Set(context.FormKey, &form) // Get again and expect the values were stored got := Get[example](ctx) require.NotNil(t, got) - assert.Equal(t, "abc", form.Name) + assert.Equal(t, "test", form.Name) // Clear Clear(ctx) diff --git a/pkg/form/submission.go b/pkg/form/submission.go index 777b6f5..7e5513a 100644 --- a/pkg/form/submission.go +++ b/pkg/form/submission.go @@ -1,65 +1,80 @@ package form import ( + "fmt" + "net/http" + "github.com/go-playground/validator/v10" + "github.com/mikestefanello/pagoda/pkg/context" "github.com/labstack/echo/v4" ) -// Submission represents the state of the submission of a form, not including the form itself +// Submission represents the state of the submission of a form, not including the form itself. +// This satisfies the Form interface. type Submission struct { - // IsSubmitted indicates if the form has been submitted - IsSubmitted bool + // isSubmitted indicates if the form has been submitted + isSubmitted bool - // Errors stores a slice of error message strings keyed by form struct field name - Errors map[string][]string + // errors stores a slice of error message strings keyed by form struct field name + errors map[string][]string } -// Process processes a submission for a form -func (f *Submission) Process(ctx echo.Context, form any) error { - f.Errors = make(map[string][]string) - f.IsSubmitted = true +func (f *Submission) Submit(ctx echo.Context, form any) error { + f.isSubmitted = true + + // Set in context so the form can later be retrieved + ctx.Set(context.FormKey, form) + + // Bind the values from the incoming request to the form struct + if err := ctx.Bind(form); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("unable to bind form: %v", err)) + } // Validate the form if err := ctx.Validate(form); err != nil { f.setErrorMessages(err) + return err } return nil } -// HasErrors indicates if the submission has any validation errors -func (f Submission) HasErrors() bool { - if f.Errors == nil { - return false - } - return len(f.Errors) > 0 +func (f *Submission) IsSubmitted() bool { + return f.isSubmitted } -// FieldHasErrors indicates if a given field on the form has any validation errors -func (f Submission) FieldHasErrors(fieldName string) bool { +func (f *Submission) IsValid() bool { + if f.errors == nil { + return true + } + return len(f.errors) == 0 +} + +func (f *Submission) IsDone() bool { + return f.IsSubmitted() && f.IsValid() +} + +func (f *Submission) FieldHasErrors(fieldName string) bool { return len(f.GetFieldErrors(fieldName)) > 0 } -// SetFieldError sets an error message for a given field name func (f *Submission) SetFieldError(fieldName string, message string) { - if f.Errors == nil { - f.Errors = make(map[string][]string) + if f.errors == nil { + f.errors = make(map[string][]string) } - f.Errors[fieldName] = append(f.Errors[fieldName], message) + f.errors[fieldName] = append(f.errors[fieldName], message) } -// GetFieldErrors gets the errors for a given field name -func (f Submission) GetFieldErrors(fieldName string) []string { - if f.Errors == nil { +func (f *Submission) GetFieldErrors(fieldName string) []string { + if f.errors == nil { return []string{} } - return f.Errors[fieldName] + return f.errors[fieldName] } -// GetFieldStatusClass returns an HTML class based on the status of the field -func (f Submission) GetFieldStatusClass(fieldName string) string { - if f.IsSubmitted { +func (f *Submission) GetFieldStatusClass(fieldName string) string { + if f.isSubmitted { if f.FieldHasErrors(fieldName) { return "is-danger" } @@ -68,12 +83,6 @@ func (f Submission) GetFieldStatusClass(fieldName string) string { return "" } -// IsDone indicates if the submission is considered done which is when it has been submitted -// and there are no errors. -func (f Submission) IsDone() bool { - return f.IsSubmitted && !f.HasErrors() -} - // setErrorMessages sets errors messages on the submission for all fields that failed validation func (f *Submission) setErrorMessages(err error) { // Only this is supported right now diff --git a/pkg/form/submission_test.go b/pkg/form/submission_test.go index fe10d6b..3b3a9dc 100644 --- a/pkg/form/submission_test.go +++ b/pkg/form/submission_test.go @@ -1,40 +1,59 @@ package form import ( + "net/http" + "net/http/httptest" + "strings" "testing" + "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/pkg/services" - "github.com/mikestefanello/pagoda/pkg/tests" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFormSubmission(t *testing.T) { type formTest struct { - Name string `validate:"required"` - Email string `validate:"required,email"` - Submission Submission + Name string `form:"name" validate:"required"` + Email string `form:"email" validate:"required,email"` + Submission } e := echo.New() e.Validator = services.NewValidator() - ctx, _ := tests.NewContext(e, "/") - form := formTest{ - Name: "", - Email: "a@a.com", - } - err := form.Submission.Process(ctx, form) - require.NoError(t, err) - assert.True(t, form.Submission.HasErrors()) - assert.True(t, form.Submission.FieldHasErrors("Name")) - assert.False(t, form.Submission.FieldHasErrors("Email")) - require.Len(t, form.Submission.GetFieldErrors("Name"), 1) - assert.Len(t, form.Submission.GetFieldErrors("Email"), 0) - assert.Equal(t, "This field is required.", form.Submission.GetFieldErrors("Name")[0]) - assert.Equal(t, "is-danger", form.Submission.GetFieldStatusClass("Name")) - assert.Equal(t, "is-success", form.Submission.GetFieldStatusClass("Email")) - assert.False(t, form.Submission.IsDone()) + t.Run("valid request", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("email=a@a.com")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + ctx := e.NewContext(req, httptest.NewRecorder()) + + var form formTest + err := form.Submit(ctx, &form) + assert.IsType(t, validator.ValidationErrors{}, err) + + assert.Empty(t, form.Name) + assert.Equal(t, "a@a.com", form.Email) + assert.False(t, form.IsValid()) + assert.True(t, form.FieldHasErrors("Name")) + assert.False(t, form.FieldHasErrors("Email")) + require.Len(t, form.GetFieldErrors("Name"), 1) + assert.Len(t, form.GetFieldErrors("Email"), 0) + assert.Equal(t, "This field is required.", form.GetFieldErrors("Name")[0]) + assert.Equal(t, "is-danger", form.GetFieldStatusClass("Name")) + assert.Equal(t, "is-success", form.GetFieldStatusClass("Email")) + assert.False(t, form.IsDone()) + + formInCtx := Get[formTest](ctx) + require.NotNil(t, formInCtx) + assert.Equal(t, form.Email, formInCtx.Email) + }) + + t.Run("invalid request", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("abc=abc")) + ctx := e.NewContext(req, httptest.NewRecorder()) + var form formTest + err := form.Submit(ctx, &form) + assert.IsType(t, new(echo.HTTPError), err) + }) } diff --git a/pkg/funcmap/funcmap.go b/pkg/funcmap/funcmap.go index 71ee254..af7c079 100644 --- a/pkg/funcmap/funcmap.go +++ b/pkg/funcmap/funcmap.go @@ -6,10 +6,10 @@ import ( "reflect" "strings" - "github.com/mikestefanello/pagoda/config" - "github.com/Masterminds/sprig" + "github.com/labstack/echo/v4" "github.com/labstack/gommon/random" + "github.com/mikestefanello/pagoda/config" ) var ( @@ -17,29 +17,28 @@ var ( CacheBuster = random.String(10) ) -// GetFuncMap provides a template function map -func GetFuncMap() template.FuncMap { - // See http://masterminds.github.io/sprig/ for available funcs - funcMap := sprig.FuncMap() - - // Provide a list of custom functions - // Expand this as you add more functions to this package - // Avoid using a name already in use by sprig - f := template.FuncMap{ - "hasField": HasField, - "file": File, - "link": Link, - } - - for k, v := range f { - funcMap[k] = v - } - - return funcMap +type funcMap struct { + web *echo.Echo } -// HasField checks if an interface contains a given field -func HasField(v any, name string) bool { +// NewFuncMap provides a template function map +func NewFuncMap(web *echo.Echo) template.FuncMap { + fm := &funcMap{web: web} + + // See http://masterminds.github.io/sprig/ for all provided funcs + funcs := sprig.FuncMap() + + // Include all the custom functions + funcs["hasField"] = fm.hasField + funcs["file"] = fm.file + funcs["link"] = fm.link + funcs["url"] = fm.url + + return funcs +} + +// hasField checks if an interface contains a given field +func (fm *funcMap) hasField(v any, name string) bool { rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { rv = rv.Elem() @@ -50,13 +49,13 @@ func HasField(v any, name string) bool { return rv.FieldByName(name).IsValid() } -// File appends a cache buster to a given filepath so it can remain cached until the app is restarted -func File(filepath string) string { +// file appends a cache buster to a given filepath so it can remain cached until the app is restarted +func (fm *funcMap) file(filepath string) string { return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster) } -// Link outputs HTML for a link element, providing the ability to dynamically set the active class -func Link(url, text, currentPath string, classes ...string) template.HTML { +// link outputs HTML for a link element, providing the ability to dynamically set the active class +func (fm *funcMap) link(url, text, currentPath string, classes ...string) template.HTML { if currentPath == url { classes = append(classes, "is-active") } @@ -64,3 +63,8 @@ func Link(url, text, currentPath string, classes ...string) template.HTML { html := fmt.Sprintf(`%s`, strings.Join(classes, " "), url, text) return template.HTML(html) } + +// url generates a URL from a given route name and optional parameters +func (fm *funcMap) url(routeName string, params ...any) string { + return fm.web.Reverse(routeName, params...) +} diff --git a/pkg/funcmap/funcmap_test.go b/pkg/funcmap/funcmap_test.go index afc0119..5ddf3bc 100644 --- a/pkg/funcmap/funcmap_test.go +++ b/pkg/funcmap/funcmap_test.go @@ -4,36 +4,60 @@ import ( "fmt" "testing" + "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/config" "github.com/stretchr/testify/assert" ) +func TestNewFuncMap(t *testing.T) { + f := NewFuncMap(echo.New()) + assert.NotNil(t, f["hasField"]) + assert.NotNil(t, f["link"]) + assert.NotNil(t, f["file"]) + assert.NotNil(t, f["url"]) +} + func TestHasField(t *testing.T) { type example struct { name string } var e example - assert.True(t, HasField(e, "name")) - assert.False(t, HasField(e, "abcd")) + f := new(funcMap) + assert.True(t, f.hasField(e, "name")) + assert.False(t, f.hasField(e, "abcd")) } func TestLink(t *testing.T) { - link := string(Link("/abc", "Text", "/abc")) + f := new(funcMap) + + link := string(f.link("/abc", "Text", "/abc")) expected := `Text` assert.Equal(t, expected, link) - link = string(Link("/abc", "Text", "/abc", "first", "second")) + link = string(f.link("/abc", "Text", "/abc", "first", "second")) expected = `Text` assert.Equal(t, expected, link) - link = string(Link("/abc", "Text", "/def")) + link = string(f.link("/abc", "Text", "/def")) expected = `Text` assert.Equal(t, expected, link) } func TestFile(t *testing.T) { - file := File("test.png") + f := new(funcMap) + + file := f.file("test.png") expected := fmt.Sprintf("/%s/test.png?v=%s", config.StaticPrefix, CacheBuster) assert.Equal(t, expected, file) } + +func TestUrl(t *testing.T) { + f := new(funcMap) + f.web = echo.New() + f.web.GET("/mypath/:id", func(c echo.Context) error { + return nil + }).Name = "test" + out := f.url("test", 5) + assert.Equal(t, "/mypath/5", out) +} diff --git a/pkg/handlers/auth.go b/pkg/handlers/auth.go index 567b663..d324c4f 100644 --- a/pkg/handlers/auth.go +++ b/pkg/handlers/auth.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent/user" @@ -38,14 +39,14 @@ type ( } forgotPasswordForm struct { - Email string `form:"email" validate:"required,email"` - Submission form.Submission + Email string `form:"email" validate:"required,email"` + form.Submission } loginForm struct { - Email string `form:"email" validate:"required,email"` - Password string `form:"password" validate:"required"` - Submission form.Submission + Email string `form:"email" validate:"required,email"` + Password string `form:"password" validate:"required"` + form.Submission } registerForm struct { @@ -53,13 +54,13 @@ type ( Email string `form:"email" validate:"required,email"` Password string `form:"password" validate:"required"` ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"` - Submission form.Submission + form.Submission } resetPasswordForm struct { Password string `form:"password" validate:"required"` ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"` - Submission form.Submission + form.Submission } ) @@ -114,17 +115,14 @@ func (c *Auth) ForgotPasswordSubmit(ctx echo.Context) error { return c.ForgotPasswordPage(ctx) } - // Set the form in context and parse the form values - if err := form.Set(ctx, &input); err != nil { - return err - } + err := form.Submit(ctx, &input) - if err := input.Submission.Process(ctx, input); err != nil { - return c.Fail(err, "unable to process form submission") - } - - if input.Submission.HasErrors() { + switch err.(type) { + case nil: + case validator.ValidationErrors: return c.ForgotPasswordPage(ctx) + default: + return err } // Attempt to load the user @@ -179,23 +177,20 @@ func (c *Auth) LoginSubmit(ctx echo.Context) error { var input loginForm authFailed := func() error { - input.Submission.SetFieldError("Email", "") - input.Submission.SetFieldError("Password", "") + input.SetFieldError("Email", "") + input.SetFieldError("Password", "") msg.Danger(ctx, "Invalid credentials. Please try again.") return c.LoginPage(ctx) } - // Set in context and parse the form values - if err := form.Set(ctx, &input); err != nil { - return err - } + err := form.Submit(ctx, &input) - if err := input.Submission.Process(ctx, input); err != nil { - return c.Fail(err, "unable to process form submission") - } - - if input.Submission.HasErrors() { + switch err.(type) { + case nil: + case validator.ValidationErrors: return c.LoginPage(ctx) + default: + return err } // Attempt to load the user @@ -250,17 +245,14 @@ func (c *Auth) RegisterPage(ctx echo.Context) error { func (c *Auth) RegisterSubmit(ctx echo.Context) error { var input registerForm - // Set in context and parse the form values - if err := form.Set(ctx, &input); err != nil { - return c.Fail(err, "unable to parse register form") - } + err := form.Submit(ctx, &input) - if err := input.Submission.Process(ctx, input); err != nil { - return c.Fail(err, "unable to process form submission") - } - - if input.Submission.HasErrors() { + switch err.(type) { + case nil: + case validator.ValidationErrors: return c.RegisterPage(ctx) + default: + return err } // Hash the password @@ -341,17 +333,14 @@ func (c *Auth) ResetPasswordPage(ctx echo.Context) error { func (c *Auth) ResetPasswordSubmit(ctx echo.Context) error { var input resetPasswordForm - // Set in context and parse the form values - if err := form.Set(ctx, &input); err != nil { - return c.Fail(err, "unable to parse password reset form") - } + err := form.Submit(ctx, &input) - if err := input.Submission.Process(ctx, input); err != nil { - return c.Fail(err, "unable to process form submission") - } - - if input.Submission.HasErrors() { + switch err.(type) { + case nil: + case validator.ValidationErrors: return c.ResetPasswordPage(ctx) + default: + return err } // Hash the new password diff --git a/pkg/handlers/contact.go b/pkg/handlers/contact.go index af4a45e..7de83df 100644 --- a/pkg/handlers/contact.go +++ b/pkg/handlers/contact.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" + "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/pkg/controller" "github.com/mikestefanello/pagoda/pkg/form" @@ -25,7 +26,7 @@ type ( Email string `form:"email" validate:"required,email"` Department string `form:"department" validate:"required,oneof=sales marketing hr"` Message string `form:"message" validate:"required"` - Submission form.Submission + form.Submission } ) @@ -57,26 +58,25 @@ func (c *Contact) Page(ctx echo.Context) error { func (c *Contact) Submit(ctx echo.Context) error { var input contactForm - // Store in context and parse the form values - if err := form.Set(ctx, &input); err != nil { + err := form.Submit(ctx, &input) + + switch err.(type) { + case nil: + case validator.ValidationErrors: + return c.Page(ctx) + default: return err } - if err := input.Submission.Process(ctx, input); err != nil { - return c.Fail(err, "unable to process form submission") - } + err = c.mail. + Compose(). + To(input.Email). + Subject("Contact form submitted"). + Body(fmt.Sprintf("The message is: %s", input.Message)). + Send(ctx) - if !input.Submission.HasErrors() { - err := c.mail. - Compose(). - To(input.Email). - Subject("Contact form submitted"). - Body(fmt.Sprintf("The message is: %s", input.Message)). - Send(ctx) - - if err != nil { - return c.Fail(err, "unable to send email") - } + if err != nil { + return c.Fail(err, "unable to send email") } return c.Page(ctx) diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 50de881..ec07b2c 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -48,8 +48,8 @@ func Danger(ctx echo.Context, message string) { Set(ctx, TypeDanger, message) } -// Set adds a new flash message of a given type into the session storage -// Errors will logged and not returned +// Set adds a new flash message of a given type into the session storage. +// Errors will be logged and not returned. func Set(ctx echo.Context, typ Type, message string) { if sess, err := getSession(ctx); err == nil { sess.AddFlash(message, string(typ)) @@ -57,8 +57,8 @@ func Set(ctx echo.Context, typ Type, message string) { } } -// Get gets flash messages of a given type from the session storage -// Errors will logged and not returned +// Get gets flash messages of a given type from the session storage. +// Errors will be logged and not returned. func Get(ctx echo.Context, typ Type) []string { var msgs []string diff --git a/pkg/services/container.go b/pkg/services/container.go index 87543fa..3384c0b 100644 --- a/pkg/services/container.go +++ b/pkg/services/container.go @@ -183,7 +183,7 @@ func (c *Container) initAuth() { // initTemplateRenderer initializes the template renderer func (c *Container) initTemplateRenderer() { - c.TemplateRenderer = NewTemplateRenderer(c.Config) + c.TemplateRenderer = NewTemplateRenderer(c.Config, c.Web) } // initMail initialize the mail client diff --git a/pkg/services/template_renderer.go b/pkg/services/template_renderer.go index dbb4529..178a4f6 100644 --- a/pkg/services/template_renderer.go +++ b/pkg/services/template_renderer.go @@ -8,6 +8,7 @@ import ( "io/fs" "sync" + "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/config" "github.com/mikestefanello/pagoda/pkg/funcmap" "github.com/mikestefanello/pagoda/templates" @@ -53,10 +54,10 @@ type ( ) // NewTemplateRenderer creates a new TemplateRenderer -func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer { +func NewTemplateRenderer(cfg *config.Config, web *echo.Echo) *TemplateRenderer { return &TemplateRenderer{ templateCache: sync.Map{}, - funcMap: funcmap.GetFuncMap(), + funcMap: funcmap.NewFuncMap(web), config: cfg, } } diff --git a/templates/layouts/auth.gohtml b/templates/layouts/auth.gohtml index 2fafea0..6279dd7 100644 --- a/templates/layouts/auth.gohtml +++ b/templates/layouts/auth.gohtml @@ -19,9 +19,9 @@ {{template "content" .}}
- Login ◌ - Create an account ◌ - Forgot password? + Login ◌ + Create an account ◌ + Forgot password?
diff --git a/templates/layouts/main.gohtml b/templates/layouts/main.gohtml index 2b04046..e256c9c 100644 --- a/templates/layouts/main.gohtml +++ b/templates/layouts/main.gohtml @@ -9,7 +9,7 @@