Use consts for route names and templates.
This commit is contained in:
parent
5af18e2473
commit
c2b6928fb4
@ -607,11 +607,11 @@ Cached pages are looked up for a key that matches the exact, full URL of the giv
|
|||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
The `Data` field on the `Page` is of type `interface{}` and is what allows your route to pass whatever it requires to the templates, alongside the `Page` itself.
|
The `Data` field on the `Page` is of type `any` and is what allows your route to pass whatever it requires to the templates, alongside the `Page` itself.
|
||||||
|
|
||||||
### Forms
|
### Forms
|
||||||
|
|
||||||
The `Form` field on the `Page` is similar to the `Data` field in that it's an `interface{}` 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 in that it's an `any` type but it's meant to store a struct that represents a form being rendered on the page.
|
||||||
|
|
||||||
An example of this pattern is:
|
An example of this pattern is:
|
||||||
|
|
||||||
@ -825,8 +825,8 @@ Once your `Page` is fully built, rendering it via the embedded `Controller` in y
|
|||||||
```go
|
```go
|
||||||
func (c *home) Get(ctx echo.Context) error {
|
func (c *home) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = "home"
|
page.Name = templates.PageHome
|
||||||
return c.RenderPage(ctx, page)
|
return c.RenderPage(ctx, page)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -55,7 +55,7 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
|
|||||||
buf, err = c.Container.TemplateRenderer.
|
buf, err = c.Container.TemplateRenderer.
|
||||||
Parse().
|
Parse().
|
||||||
Group("page:htmx").
|
Group("page:htmx").
|
||||||
Key(page.Name).
|
Key(string(page.Name)).
|
||||||
Base("htmx").
|
Base("htmx").
|
||||||
Files(
|
Files(
|
||||||
"htmx",
|
"htmx",
|
||||||
@ -73,8 +73,8 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
|
|||||||
buf, err = c.Container.TemplateRenderer.
|
buf, err = c.Container.TemplateRenderer.
|
||||||
Parse().
|
Parse().
|
||||||
Group("page").
|
Group("page").
|
||||||
Key(page.Name).
|
Key(string(page.Name)).
|
||||||
Base(page.Layout).
|
Base(string(page.Layout)).
|
||||||
Files(
|
Files(
|
||||||
fmt.Sprintf("layouts/%s", page.Layout),
|
fmt.Sprintf("layouts/%s", page.Layout),
|
||||||
fmt.Sprintf("pages/%s", page.Name),
|
fmt.Sprintf("pages/%s", page.Name),
|
||||||
@ -151,7 +151,7 @@ func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect redirects to a given route name with optional route parameters
|
// Redirect redirects to a given route name with optional route parameters
|
||||||
func (c *Controller) Redirect(ctx echo.Context, route string, routeParams ...interface{}) error {
|
func (c *Controller) Redirect(ctx echo.Context, route string, routeParams ...any) error {
|
||||||
url := ctx.Echo().Reverse(route, routeParams...)
|
url := ctx.Echo().Reverse(route, routeParams...)
|
||||||
|
|
||||||
if htmx.GetRequest(ctx).Boosted {
|
if htmx.GetRequest(ctx).Boosted {
|
||||||
|
@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@ -91,14 +92,14 @@ func TestController_RenderPage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the template cache
|
// Check the template cache
|
||||||
parsed, err := c.TemplateRenderer.Load("page", p.Name)
|
parsed, err := c.TemplateRenderer.Load("page", string(p.Name))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check that all expected templates were parsed.
|
// Check that all expected templates were parsed.
|
||||||
// This includes the name, layout and all components
|
// This includes the name, layout and all components
|
||||||
expectedTemplates := make(map[string]bool)
|
expectedTemplates := make(map[string]bool)
|
||||||
expectedTemplates[p.Name+config.TemplateExt] = true
|
expectedTemplates[fmt.Sprintf("%s%s", p.Name, config.TemplateExt)] = true
|
||||||
expectedTemplates[p.Layout+config.TemplateExt] = true
|
expectedTemplates[fmt.Sprintf("%s%s", p.Layout, config.TemplateExt)] = true
|
||||||
components, err := templates.Get().ReadDir("components")
|
components, err := templates.Get().ReadDir("components")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for _, f := range components {
|
for _, f := range components {
|
||||||
@ -124,13 +125,13 @@ func TestController_RenderPage(t *testing.T) {
|
|||||||
assert.Equal(t, "trigger", ctx.Response().Header().Get(htmx.HeaderTrigger))
|
assert.Equal(t, "trigger", ctx.Response().Header().Get(htmx.HeaderTrigger))
|
||||||
|
|
||||||
// Check the template cache
|
// Check the template cache
|
||||||
parsed, err := c.TemplateRenderer.Load("page:htmx", p.Name)
|
parsed, err := c.TemplateRenderer.Load("page:htmx", string(p.Name))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check that all expected templates were parsed.
|
// Check that all expected templates were parsed.
|
||||||
// This includes the name, htmx and all components
|
// This includes the name, htmx and all components
|
||||||
expectedTemplates := make(map[string]bool)
|
expectedTemplates := make(map[string]bool)
|
||||||
expectedTemplates[p.Name+config.TemplateExt] = true
|
expectedTemplates[fmt.Sprintf("%s%s", p.Name, config.TemplateExt)] = true
|
||||||
expectedTemplates["htmx"+config.TemplateExt] = true
|
expectedTemplates["htmx"+config.TemplateExt] = true
|
||||||
components, err := templates.Get().ReadDir("components")
|
components, err := templates.Get().ReadDir("components")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -16,7 +16,7 @@ type FormSubmission struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process processes a submission for a form
|
// Process processes a submission for a form
|
||||||
func (f *FormSubmission) Process(ctx echo.Context, form interface{}) error {
|
func (f *FormSubmission) Process(ctx echo.Context, form any) error {
|
||||||
f.Errors = make(map[string][]string)
|
f.Errors = make(map[string][]string)
|
||||||
f.IsSubmitted = true
|
f.IsSubmitted = true
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
echomw "github.com/labstack/echo/v4/middleware"
|
echomw "github.com/labstack/echo/v4/middleware"
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ type Page struct {
|
|||||||
Context echo.Context
|
Context echo.Context
|
||||||
|
|
||||||
// ToURL is a function to convert a route name and optional route parameters to a URL
|
// ToURL is a function to convert a route name and optional route parameters to a URL
|
||||||
ToURL func(name string, params ...interface{}) string
|
ToURL func(name string, params ...any) string
|
||||||
|
|
||||||
// Path stores the path of the current request
|
// Path stores the path of the current request
|
||||||
Path string
|
Path string
|
||||||
@ -44,24 +45,24 @@ type Page struct {
|
|||||||
|
|
||||||
// Data stores whatever additional data that needs to be passed to the templates.
|
// Data stores whatever additional data that needs to be passed to the templates.
|
||||||
// This is what the controller uses to pass the content of the page.
|
// This is what the controller uses to pass the content of the page.
|
||||||
Data interface{}
|
Data any
|
||||||
|
|
||||||
// Form stores a struct that represents a form on the page.
|
// 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
|
// 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
|
// It should also contain a Submission field of type FormSubmission if you wish to have validation
|
||||||
// messagesa and markup presented to the user
|
// messagesa and markup presented to the user
|
||||||
Form interface{}
|
Form any
|
||||||
|
|
||||||
// Layout stores the name of the layout base template file which will be used when the page is rendered.
|
// 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.
|
// This should match a template file located within the layouts directory inside the templates directory.
|
||||||
// The template extension should not be included in this value.
|
// The template extension should not be included in this value.
|
||||||
Layout string
|
Layout templates.Layout
|
||||||
|
|
||||||
// Name stores the name of the page as well as the name of the template file which will be used to render
|
// Name stores the name of the page as well as the name of the template file which will be used to render
|
||||||
// the content portion of the layout template.
|
// the content portion of the layout template.
|
||||||
// This should match a template file located within the pages directory inside the templates directory.
|
// This should match a template file located within the pages directory inside the templates directory.
|
||||||
// The template extension should not be included in this value.
|
// The template extension should not be included in this value.
|
||||||
Name string
|
Name templates.Page
|
||||||
|
|
||||||
// IsHome stores whether the requested page is the home page or not
|
// IsHome stores whether the requested page is the home page or not
|
||||||
IsHome bool
|
IsHome bool
|
||||||
|
@ -39,7 +39,7 @@ func GetFuncMap() template.FuncMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HasField checks if an interface contains a given field
|
// HasField checks if an interface contains a given field
|
||||||
func HasField(v interface{}, name string) bool {
|
func HasField(v any, name string) bool {
|
||||||
rv := reflect.ValueOf(v)
|
rv := reflect.ValueOf(v)
|
||||||
if rv.Kind() == reflect.Ptr {
|
if rv.Kind() == reflect.Ptr {
|
||||||
rv = rv.Elem()
|
rv = rv.Elem()
|
||||||
|
@ -32,7 +32,7 @@ func TestLink(t *testing.T) {
|
|||||||
assert.Equal(t, expected, link)
|
assert.Equal(t, expected, link)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetFuncMap(t *testing.T) {
|
func TestFile(t *testing.T) {
|
||||||
file := File("test.png")
|
file := File("test.png")
|
||||||
expected := fmt.Sprintf("/%s/test.png?v=%s", config.StaticPrefix, CacheBuster)
|
expected := fmt.Sprintf("/%s/test.png?v=%s", config.StaticPrefix, CacheBuster)
|
||||||
assert.Equal(t, expected, file)
|
assert.Equal(t, expected, file)
|
||||||
|
@ -70,6 +70,7 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc
|
|||||||
return next(c)
|
return next(c)
|
||||||
case services.InvalidPasswordTokenError:
|
case services.InvalidPasswordTokenError:
|
||||||
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
||||||
|
// TODO use the const for route name
|
||||||
return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
|
return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
|
||||||
default:
|
default:
|
||||||
return echo.NewHTTPError(
|
return echo.NewHTTPError(
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -27,8 +28,8 @@ type (
|
|||||||
|
|
||||||
func (c *about) Get(ctx echo.Context) error {
|
func (c *about) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = "about"
|
page.Name = templates.PageAbout
|
||||||
page.Title = "About"
|
page.Title = "About"
|
||||||
|
|
||||||
// This page will be cached!
|
// This page will be cached!
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
// this test package
|
// this test package
|
||||||
func TestAbout_Get(t *testing.T) {
|
func TestAbout_Get(t *testing.T) {
|
||||||
doc := request(t).
|
doc := request(t).
|
||||||
setRoute("about").
|
setRoute(routeNameAbout).
|
||||||
get().
|
get().
|
||||||
assertStatusCode(http.StatusOK).
|
assertStatusCode(http.StatusOK).
|
||||||
toDoc()
|
toDoc()
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -23,8 +24,8 @@ type (
|
|||||||
|
|
||||||
func (c *contact) Get(ctx echo.Context) error {
|
func (c *contact) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = "contact"
|
page.Name = templates.PageContact
|
||||||
page.Title = "Contact us"
|
page.Title = "Contact us"
|
||||||
page.Form = contactForm{}
|
page.Form = contactForm{}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -30,9 +31,9 @@ func (e *errorHandler) Get(err error, ctx echo.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
|
||||||
page.Title = http.StatusText(code)
|
page.Title = http.StatusText(code)
|
||||||
page.Name = "error"
|
page.Layout = templates.LayoutMain
|
||||||
|
page.Name = templates.PageError
|
||||||
page.StatusCode = code
|
page.StatusCode = code
|
||||||
page.HTMX.Request.Enabled = false
|
page.HTMX.Request.Enabled = false
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -26,8 +27,8 @@ type (
|
|||||||
|
|
||||||
func (c *forgotPassword) Get(ctx echo.Context) error {
|
func (c *forgotPassword) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "auth"
|
page.Layout = templates.LayoutAuth
|
||||||
page.Name = "forgot-password"
|
page.Name = templates.PageForgotPassword
|
||||||
page.Title = "Forgot password"
|
page.Title = "Forgot password"
|
||||||
page.Form = forgotPasswordForm{}
|
page.Form = forgotPasswordForm{}
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ func (c *forgotPassword) Post(ctx echo.Context) error {
|
|||||||
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
||||||
|
|
||||||
// Email the user
|
// Email the user
|
||||||
url := ctx.Echo().Reverse("reset_password", u.ID, pt.ID, token)
|
url := ctx.Echo().Reverse(routeNameResetPassword, u.ID, pt.ID, token)
|
||||||
err = c.Container.Mail.
|
err = c.Container.Mail.
|
||||||
Compose().
|
Compose().
|
||||||
To(u.Email).
|
To(u.Email).
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -21,8 +22,8 @@ type (
|
|||||||
|
|
||||||
func (c *home) Get(ctx echo.Context) error {
|
func (c *home) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = "home"
|
page.Name = templates.PageHome
|
||||||
page.Metatags.Description = "Welcome to the homepage."
|
page.Metatags.Description = "Welcome to the homepage."
|
||||||
page.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
|
page.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
|
||||||
page.Pager = controller.NewPager(ctx, 4)
|
page.Pager = controller.NewPager(ctx, 4)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -27,8 +28,8 @@ type (
|
|||||||
|
|
||||||
func (c *login) Get(ctx echo.Context) error {
|
func (c *login) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "auth"
|
page.Layout = templates.LayoutAuth
|
||||||
page.Name = "login"
|
page.Name = templates.PageLogin
|
||||||
page.Title = "Log in"
|
page.Title = "Log in"
|
||||||
page.Form = loginForm{}
|
page.Form = loginForm{}
|
||||||
|
|
||||||
@ -90,5 +91,5 @@ func (c *login) Post(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg.Success(ctx, fmt.Sprintf("Welcome back, <strong>%s</strong>. You are now logged in.", u.Name))
|
msg.Success(ctx, fmt.Sprintf("Welcome back, <strong>%s</strong>. You are now logged in.", u.Name))
|
||||||
return c.Redirect(ctx, "home")
|
return c.Redirect(ctx, routeNameHome)
|
||||||
}
|
}
|
||||||
|
@ -17,5 +17,5 @@ func (l *logout) Get(c echo.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
msg.Danger(c, "An error occurred. Please try again.")
|
msg.Danger(c, "An error occurred. Please try again.")
|
||||||
}
|
}
|
||||||
return l.Redirect(c, "home")
|
return l.Redirect(c, routeNameHome)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -27,8 +28,8 @@ type (
|
|||||||
|
|
||||||
func (c *register) Get(ctx echo.Context) error {
|
func (c *register) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "auth"
|
page.Layout = templates.LayoutAuth
|
||||||
page.Name = "register"
|
page.Name = templates.PageRegister
|
||||||
page.Title = "Register"
|
page.Title = "Register"
|
||||||
page.Form = registerForm{}
|
page.Form = registerForm{}
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ func (c *register) Post(ctx echo.Context) error {
|
|||||||
ctx.Logger().Infof("user created: %s", u.Name)
|
ctx.Logger().Infof("user created: %s", u.Name)
|
||||||
case *ent.ConstraintError:
|
case *ent.ConstraintError:
|
||||||
msg.Warning(ctx, "A user with this email address already exists. Please log in.")
|
msg.Warning(ctx, "A user with this email address already exists. Please log in.")
|
||||||
return c.Redirect(ctx, "login")
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
default:
|
default:
|
||||||
return c.Fail(err, "unable to create user")
|
return c.Fail(err, "unable to create user")
|
||||||
}
|
}
|
||||||
@ -85,7 +86,7 @@ func (c *register) Post(ctx echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger().Errorf("unable to log in: %v", err)
|
ctx.Logger().Errorf("unable to log in: %v", err)
|
||||||
msg.Info(ctx, "Your account has been created.")
|
msg.Info(ctx, "Your account has been created.")
|
||||||
return c.Redirect(ctx, "login")
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Success(ctx, "Your account has been created. You are now logged in.")
|
msg.Success(ctx, "Your account has been created. You are now logged in.")
|
||||||
@ -93,7 +94,7 @@ func (c *register) Post(ctx echo.Context) error {
|
|||||||
// Send the verification email
|
// Send the verification email
|
||||||
c.sendVerificationEmail(ctx, u)
|
c.sendVerificationEmail(ctx, u)
|
||||||
|
|
||||||
return c.Redirect(ctx, "home")
|
return c.Redirect(ctx, routeNameHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
func (c *register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
||||||
@ -105,7 +106,7 @@ func (c *register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
url := ctx.Echo().Reverse("verify_email", token)
|
url := ctx.Echo().Reverse(routeNameVerifyEmail, token)
|
||||||
err = c.Container.Mail.
|
err = c.Container.Mail.
|
||||||
Compose().
|
Compose().
|
||||||
To(usr.Email).
|
To(usr.Email).
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -23,8 +24,8 @@ type (
|
|||||||
|
|
||||||
func (c *resetPassword) Get(ctx echo.Context) error {
|
func (c *resetPassword) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "auth"
|
page.Layout = templates.LayoutAuth
|
||||||
page.Name = "reset-password"
|
page.Name = templates.PageResetPassword
|
||||||
page.Title = "Reset password"
|
page.Title = "Reset password"
|
||||||
page.Form = resetPasswordForm{}
|
page.Form = resetPasswordForm{}
|
||||||
|
|
||||||
@ -78,5 +79,5 @@ func (c *resetPassword) Post(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg.Success(ctx, "Your password has been updated.")
|
msg.Success(ctx, "Your password has been updated.")
|
||||||
return c.Redirect(ctx, "login")
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,24 @@ import (
|
|||||||
echomw "github.com/labstack/echo/v4/middleware"
|
echomw "github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
routeNameForgotPassword = "forgot_password"
|
||||||
|
routeNameForgotPasswordSubmit = "forgot_password.submit"
|
||||||
|
routeNameLogin = "login"
|
||||||
|
routeNameLoginSubmit = "login.submit"
|
||||||
|
routeNameLogout = "logout"
|
||||||
|
routeNameRegister = "register"
|
||||||
|
routeNameRegisterSubmit = "register.submit"
|
||||||
|
routeNameResetPassword = "reset_password"
|
||||||
|
routeNameResetPasswordSubmit = "reset_password.submit"
|
||||||
|
routeNameVerifyEmail = "verify_email"
|
||||||
|
routeNameContact = "contact"
|
||||||
|
routeNameContactSubmit = "contact.submit"
|
||||||
|
routeNameAbout = "about"
|
||||||
|
routeNameHome = "home"
|
||||||
|
routeNameSearch = "search"
|
||||||
|
)
|
||||||
|
|
||||||
// BuildRouter builds the router
|
// BuildRouter builds the router
|
||||||
func BuildRouter(c *services.Container) {
|
func BuildRouter(c *services.Container) {
|
||||||
// Static files with proper cache control
|
// Static files with proper cache control
|
||||||
@ -66,44 +84,44 @@ func BuildRouter(c *services.Container) {
|
|||||||
|
|
||||||
func navRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
func navRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
||||||
home := home{Controller: ctr}
|
home := home{Controller: ctr}
|
||||||
g.GET("/", home.Get).Name = "home"
|
g.GET("/", home.Get).Name = routeNameHome
|
||||||
|
|
||||||
search := search{Controller: ctr}
|
search := search{Controller: ctr}
|
||||||
g.GET("/search", search.Get).Name = "search"
|
g.GET("/search", search.Get).Name = routeNameSearch
|
||||||
|
|
||||||
about := about{Controller: ctr}
|
about := about{Controller: ctr}
|
||||||
g.GET("/about", about.Get).Name = "about"
|
g.GET("/about", about.Get).Name = routeNameAbout
|
||||||
|
|
||||||
contact := contact{Controller: ctr}
|
contact := contact{Controller: ctr}
|
||||||
g.GET("/contact", contact.Get).Name = "contact"
|
g.GET("/contact", contact.Get).Name = routeNameContact
|
||||||
g.POST("/contact", contact.Post).Name = "contact.post"
|
g.POST("/contact", contact.Post).Name = routeNameContactSubmit
|
||||||
}
|
}
|
||||||
|
|
||||||
func userRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
func userRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
||||||
logout := logout{Controller: ctr}
|
logout := logout{Controller: ctr}
|
||||||
g.GET("/logout", logout.Get, middleware.RequireAuthentication()).Name = "logout"
|
g.GET("/logout", logout.Get, middleware.RequireAuthentication()).Name = routeNameLogout
|
||||||
|
|
||||||
verifyEmail := verifyEmail{Controller: ctr}
|
verifyEmail := verifyEmail{Controller: ctr}
|
||||||
g.GET("/email/verify/:token", verifyEmail.Get).Name = "verify_email"
|
g.GET("/email/verify/:token", verifyEmail.Get).Name = routeNameVerifyEmail
|
||||||
|
|
||||||
noAuth := g.Group("/user", middleware.RequireNoAuthentication())
|
noAuth := g.Group("/user", middleware.RequireNoAuthentication())
|
||||||
login := login{Controller: ctr}
|
login := login{Controller: ctr}
|
||||||
noAuth.GET("/login", login.Get).Name = "login"
|
noAuth.GET("/login", login.Get).Name = routeNameLogin
|
||||||
noAuth.POST("/login", login.Post).Name = "login.post"
|
noAuth.POST("/login", login.Post).Name = routeNameLoginSubmit
|
||||||
|
|
||||||
register := register{Controller: ctr}
|
register := register{Controller: ctr}
|
||||||
noAuth.GET("/register", register.Get).Name = "register"
|
noAuth.GET("/register", register.Get).Name = routeNameRegister
|
||||||
noAuth.POST("/register", register.Post).Name = "register.post"
|
noAuth.POST("/register", register.Post).Name = routeNameRegisterSubmit
|
||||||
|
|
||||||
forgot := forgotPassword{Controller: ctr}
|
forgot := forgotPassword{Controller: ctr}
|
||||||
noAuth.GET("/password", forgot.Get).Name = "forgot_password"
|
noAuth.GET("/password", forgot.Get).Name = routeNameForgotPassword
|
||||||
noAuth.POST("/password", forgot.Post).Name = "forgot_password.post"
|
noAuth.POST("/password", forgot.Post).Name = routeNameForgotPasswordSubmit
|
||||||
|
|
||||||
resetGroup := noAuth.Group("/password/reset",
|
resetGroup := noAuth.Group("/password/reset",
|
||||||
middleware.LoadUser(c.ORM),
|
middleware.LoadUser(c.ORM),
|
||||||
middleware.LoadValidPasswordToken(c.Auth),
|
middleware.LoadValidPasswordToken(c.Auth),
|
||||||
)
|
)
|
||||||
reset := resetPassword{Controller: ctr}
|
reset := resetPassword{Controller: ctr}
|
||||||
resetGroup.GET("/token/:user/:password_token/:token", reset.Get).Name = "reset_password"
|
resetGroup.GET("/token/:user/:password_token/:token", reset.Get).Name = routeNameResetPassword
|
||||||
resetGroup.POST("/token/:user/:password_token/:token", reset.Post).Name = "reset_password.post"
|
resetGroup.POST("/token/:user/:password_token/:token", reset.Post).Name = routeNameResetPasswordSubmit
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ func (h *httpRequest) setClient(client http.Client) *httpRequest {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpRequest) setRoute(route string, params ...interface{}) *httpRequest {
|
func (h *httpRequest) setRoute(route string, params ...any) *httpRequest {
|
||||||
h.route = srv.URL + c.Web.Reverse(route, params)
|
h.route = srv.URL + c.Web.Reverse(route, params)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ func (h *httpResponse) assertStatusCode(code int) *httpResponse {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpResponse) assertRedirect(t *testing.T, route string, params ...interface{}) *httpResponse {
|
func (h *httpResponse) assertRedirect(t *testing.T, route string, params ...any) *httpResponse {
|
||||||
assert.Equal(t, c.Web.Reverse(route, params), h.Header.Get("Location"))
|
assert.Equal(t, c.Web.Reverse(route, params), h.Header.Get("Location"))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -22,8 +23,8 @@ type (
|
|||||||
|
|
||||||
func (c *search) Get(ctx echo.Context) error {
|
func (c *search) Get(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = "main"
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = "search"
|
page.Name = templates.PageSearch
|
||||||
|
|
||||||
// Fake search results
|
// Fake search results
|
||||||
var results []searchResult
|
var results []searchResult
|
||||||
|
@ -21,7 +21,7 @@ func (c *verifyEmail) Get(ctx echo.Context) error {
|
|||||||
email, err := c.Container.Auth.ValidateEmailVerificationToken(token)
|
email, err := c.Container.Auth.ValidateEmailVerificationToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.Warning(ctx, "The link is either invalid or has expired.")
|
msg.Warning(ctx, "The link is either invalid or has expired.")
|
||||||
return c.Redirect(ctx, "home")
|
return c.Redirect(ctx, routeNameHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it matches the authenticated user
|
// Check if it matches the authenticated user
|
||||||
@ -58,5 +58,5 @@ func (c *verifyEmail) Get(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg.Success(ctx, "Your email has been successfully verified.")
|
msg.Success(ctx, "Your email has been successfully verified.")
|
||||||
return c.Redirect(ctx, "home")
|
return c.Redirect(ctx, routeNameHome)
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ func (c *AuthClient) GenerateEmailVerificationToken(email string) (string, error
|
|||||||
// ValidateEmailVerificationToken validates an email verification token and returns the associated email address if
|
// ValidateEmailVerificationToken validates an email verification token and returns the associated email address if
|
||||||
// the token is valid and has not expired
|
// the token is valid and has not expired
|
||||||
func (c *AuthClient) ValidateEmailVerificationToken(token string) (string, error) {
|
func (c *AuthClient) ValidateEmailVerificationToken(token string) (string, error) {
|
||||||
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
t, err := jwt.Parse(token, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ type (
|
|||||||
client *CacheClient
|
client *CacheClient
|
||||||
key string
|
key string
|
||||||
group string
|
group string
|
||||||
data interface{}
|
data any
|
||||||
expiration time.Duration
|
expiration time.Duration
|
||||||
tags []string
|
tags []string
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ type (
|
|||||||
client *CacheClient
|
client *CacheClient
|
||||||
key string
|
key string
|
||||||
group string
|
group string
|
||||||
dataType interface{}
|
dataType any
|
||||||
}
|
}
|
||||||
|
|
||||||
// cacheFlush handles chaining a flush operation
|
// cacheFlush handles chaining a flush operation
|
||||||
@ -128,7 +128,7 @@ func (c *cacheSet) Group(group string) *cacheSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Data sets the data to cache
|
// Data sets the data to cache
|
||||||
func (c *cacheSet) Data(data interface{}) *cacheSet {
|
func (c *cacheSet) Data(data any) *cacheSet {
|
||||||
c.data = data
|
c.data = data
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -174,13 +174,13 @@ func (c *cacheGet) Group(group string) *cacheGet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Type sets the expected Go type of the data being retrieved from the cache
|
// Type sets the expected Go type of the data being retrieved from the cache
|
||||||
func (c *cacheGet) Type(expectedType interface{}) *cacheGet {
|
func (c *cacheGet) Type(expectedType any) *cacheGet {
|
||||||
c.dataType = expectedType
|
c.dataType = expectedType
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch fetches the data from the cache
|
// Fetch fetches the data from the cache
|
||||||
func (c *cacheGet) Fetch(ctx context.Context) (interface{}, error) {
|
func (c *cacheGet) Fetch(ctx context.Context) (any, error) {
|
||||||
if c.key == "" {
|
if c.key == "" {
|
||||||
return nil, errors.New("no cache key specified")
|
return nil, errors.New("no cache key specified")
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ type (
|
|||||||
subject string
|
subject string
|
||||||
body string
|
body string
|
||||||
template string
|
template string
|
||||||
templateData interface{}
|
templateData any
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ func (m *mail) Template(template string) *mail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TemplateData sets the data that will be passed to the template specified when calling Template()
|
// TemplateData sets the data that will be passed to the template specified when calling Template()
|
||||||
func (m *mail) TemplateData(data interface{}) *mail {
|
func (m *mail) TemplateData(data any) *mail {
|
||||||
m.templateData = data
|
m.templateData = data
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ type (
|
|||||||
task struct {
|
task struct {
|
||||||
client *TaskClient
|
client *TaskClient
|
||||||
typ string
|
typ string
|
||||||
payload interface{}
|
payload any
|
||||||
periodic *string
|
periodic *string
|
||||||
queue *string
|
queue *string
|
||||||
maxRetries *int
|
maxRetries *int
|
||||||
@ -75,7 +75,7 @@ func (t *TaskClient) New(typ string) *task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Payload sets the task payload data which will be sent to the task handler
|
// Payload sets the task payload data which will be sent to the task handler
|
||||||
func (t *task) Payload(payload interface{}) *task {
|
func (t *task) Payload(payload any) *task {
|
||||||
t.payload = payload
|
t.payload = payload
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ func (t *TemplateRenderer) Load(group, key string) (*TemplateParsed, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute executes a template with the given data and provides the output
|
// Execute executes a template with the given data and provides the output
|
||||||
func (t *TemplateParsed) Execute(data interface{}) (*bytes.Buffer, error) {
|
func (t *TemplateParsed) Execute(data any) (*bytes.Buffer, error) {
|
||||||
if t.Template == nil {
|
if t.Template == nil {
|
||||||
return nil, errors.New("cannot execute template: template not initialized")
|
return nil, errors.New("cannot execute template: template not initialized")
|
||||||
}
|
}
|
||||||
@ -209,7 +209,7 @@ func (t *templateBuilder) Store() (*TemplateParsed, error) {
|
|||||||
|
|
||||||
// Execute executes the template with the given data.
|
// Execute executes the template with the given data.
|
||||||
// If the template has not already been cached, this will parse and cache the template
|
// If the template has not already been cached, this will parse and cache the template
|
||||||
func (t *templateBuilder) Execute(data interface{}) (*bytes.Buffer, error) {
|
func (t *templateBuilder) Execute(data any) (*bytes.Buffer, error) {
|
||||||
tp, err := t.Store()
|
tp, err := t.Store()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -18,7 +18,7 @@ func NewValidator() *Validator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates a struct
|
// Validate validates a struct
|
||||||
func (v *Validator) Validate(i interface{}) error {
|
func (v *Validator) Validate(i any) error {
|
||||||
if err := v.validator.Struct(i); err != nil {
|
if err := v.validator.Struct(i); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,28 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Layout string
|
||||||
|
Page string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LayoutMain Layout = "main"
|
||||||
|
LayoutAuth Layout = "auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PageAbout Page = "about"
|
||||||
|
PageContact Page = "contact"
|
||||||
|
PageError Page = "error"
|
||||||
|
PageForgotPassword Page = "forgot-password"
|
||||||
|
PageHome Page = "home"
|
||||||
|
PageLogin Page = "login"
|
||||||
|
PageRegister Page = "register"
|
||||||
|
PageResetPassword Page = "reset-password"
|
||||||
|
PageSearch Page = "search"
|
||||||
|
)
|
||||||
|
|
||||||
//go:embed *
|
//go:embed *
|
||||||
var templates embed.FS
|
var templates embed.FS
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
_, err := Get().Open("pages/home.gohtml")
|
_, err := Get().Open(fmt.Sprintf("pages/%s.gohtml", PageHome))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetOS(t *testing.T) {
|
func TestGetOS(t *testing.T) {
|
||||||
_, err := GetOS().Open("pages/home.gohtml")
|
_, err := GetOS().Open(fmt.Sprintf("pages/%s.gohtml", PageHome))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user