Added redirect package.
This commit is contained in:
parent
6730b6a319
commit
ca22f54c89
14
README.md
14
README.md
@ -455,6 +455,20 @@ Routes can return errors to indicate that something wrong happened. Ideally, the
|
||||
|
||||
The [error handler](https://echo.labstack.com/guide/error-handling/) is set to a provided route `pkg/handlers/error.go` in the `BuildRouter()` function. That means that if any middleware or route return an error, the request gets routed there. This route conveniently constructs and renders a `Page` which uses the template `templates/pages/error.go`. The status code is passed to the template so you can easily alter the markup depending on the error type.
|
||||
|
||||
### Redirects
|
||||
|
||||
The `pkg/redirect` package makes it easy to perform redirects, especially if you provide names for your routes. The `Redirect` type provides the ability to chain redirect options and also supports automatically handling HTMX redirects for boosted requests.
|
||||
|
||||
For example, if your route name is `user_profile` with a URL pattern of `/user/profile/:id`, you can perform a redirect by doing:
|
||||
|
||||
```go
|
||||
return redirect.New(ctx).
|
||||
Route("user_profile").
|
||||
Params(userID).
|
||||
Query(queryParams).
|
||||
Go()
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Since most of your web application logic will live in your routes, being able to easily test them is important. The following aims to help facilitate that.
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/mikestefanello/pagoda/pkg/middleware"
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/pkg/page"
|
||||
"github.com/mikestefanello/pagoda/pkg/redirect"
|
||||
"github.com/mikestefanello/pagoda/pkg/services"
|
||||
"github.com/mikestefanello/pagoda/templates"
|
||||
)
|
||||
@ -223,7 +224,10 @@ func (h *Auth) LoginSubmit(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
msg.Success(ctx, fmt.Sprintf("Welcome back, <strong>%s</strong>. You are now logged in.", u.Name))
|
||||
return redirect(ctx, routeNameHome)
|
||||
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameHome).
|
||||
Go()
|
||||
}
|
||||
|
||||
func (h *Auth) Logout(ctx echo.Context) error {
|
||||
@ -232,7 +236,9 @@ func (h *Auth) Logout(ctx echo.Context) error {
|
||||
} else {
|
||||
msg.Danger(ctx, "An error occurred. Please try again.")
|
||||
}
|
||||
return redirect(ctx, routeNameHome)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameHome).
|
||||
Go()
|
||||
}
|
||||
|
||||
func (h *Auth) RegisterPage(ctx echo.Context) error {
|
||||
@ -280,7 +286,9 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error {
|
||||
)
|
||||
case *ent.ConstraintError:
|
||||
msg.Warning(ctx, "A user with this email address already exists. Please log in.")
|
||||
return redirect(ctx, routeNameLogin)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameLogin).
|
||||
Go()
|
||||
default:
|
||||
return fail(err, "unable to create user")
|
||||
}
|
||||
@ -293,7 +301,9 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error {
|
||||
"user_id", u.ID,
|
||||
)
|
||||
msg.Info(ctx, "Your account has been created.")
|
||||
return redirect(ctx, routeNameLogin)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameLogin).
|
||||
Go()
|
||||
}
|
||||
|
||||
msg.Success(ctx, "Your account has been created. You are now logged in.")
|
||||
@ -301,7 +311,9 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error {
|
||||
// Send the verification email
|
||||
h.sendVerificationEmail(ctx, u)
|
||||
|
||||
return redirect(ctx, routeNameHome)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameHome).
|
||||
Go()
|
||||
}
|
||||
|
||||
func (h *Auth) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
||||
@ -384,7 +396,9 @@ func (h *Auth) ResetPasswordSubmit(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
msg.Success(ctx, "Your password has been updated.")
|
||||
return redirect(ctx, routeNameLogin)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameLogin).
|
||||
Go()
|
||||
}
|
||||
|
||||
func (h *Auth) VerifyEmail(ctx echo.Context) error {
|
||||
@ -395,7 +409,9 @@ func (h *Auth) VerifyEmail(ctx echo.Context) error {
|
||||
email, err := h.auth.ValidateEmailVerificationToken(token)
|
||||
if err != nil {
|
||||
msg.Warning(ctx, "The link is either invalid or has expired.")
|
||||
return redirect(ctx, routeNameHome)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameHome).
|
||||
Go()
|
||||
}
|
||||
|
||||
// Check if it matches the authenticated user
|
||||
@ -432,5 +448,7 @@ func (h *Auth) VerifyEmail(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
msg.Success(ctx, "Your email has been successfully verified.")
|
||||
return redirect(ctx, routeNameHome)
|
||||
return redirect.New(ctx).
|
||||
Route(routeNameHome).
|
||||
Go()
|
||||
}
|
||||
|
@ -3,10 +3,8 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||
"github.com/mikestefanello/pagoda/pkg/services"
|
||||
)
|
||||
|
||||
@ -31,30 +29,6 @@ func GetHandlers() []Handler {
|
||||
return handlers
|
||||
}
|
||||
|
||||
// redirect redirects to a given route by name with optional route parameters
|
||||
func redirect(ctx echo.Context, routeName string, routeParams ...any) error {
|
||||
return doRedirect(ctx, ctx.Echo().Reverse(routeName, routeParams...))
|
||||
}
|
||||
|
||||
// redirectWithQuery redirects to a given route by name with query parameters and optional route parameters
|
||||
func redirectWithQuery(ctx echo.Context, query url.Values, routeName string, routeParams ...any) error {
|
||||
dest := fmt.Sprintf("%s?%s", ctx.Echo().Reverse(routeName, routeParams...), query.Encode())
|
||||
return doRedirect(ctx, dest)
|
||||
}
|
||||
|
||||
// doRedirect performs a redirect to a given URL
|
||||
func doRedirect(ctx echo.Context, url string) error {
|
||||
if htmx.GetRequest(ctx).Boosted {
|
||||
htmx.Response{
|
||||
Redirect: url,
|
||||
}.Apply(ctx)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return ctx.Redirect(http.StatusFound, url)
|
||||
}
|
||||
}
|
||||
|
||||
// fail is a helper to fail a request by returning a 500 error and logging the error
|
||||
func fail(err error, log string) error {
|
||||
// The error handler will handle logging
|
||||
|
@ -3,12 +3,9 @@ package handlers
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||
"github.com/mikestefanello/pagoda/pkg/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -23,51 +20,6 @@ func TestGetSetHandlers(t *testing.T) {
|
||||
assert.Equal(t, h, got[0])
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
c.Web.GET("/path/:first/and/:second", func(c echo.Context) error {
|
||||
return nil
|
||||
}).Name = "redirect-test"
|
||||
|
||||
t.Run("no query", func(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(c.Web, "/abc")
|
||||
err := redirect(ctx, "redirect-test", "one", "two")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/path/one/and/two", ctx.Response().Header().Get(echo.HeaderLocation))
|
||||
assert.Equal(t, http.StatusFound, ctx.Response().Status)
|
||||
})
|
||||
|
||||
t.Run("no query htmx", func(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(c.Web, "/abc")
|
||||
ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
|
||||
err := redirect(ctx, "redirect-test", "one", "two")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/path/one/and/two", ctx.Response().Header().Get(htmx.HeaderRedirect))
|
||||
})
|
||||
|
||||
t.Run("query", func(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(c.Web, "/abc")
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
err := redirectWithQuery(ctx, q, "redirect-test", "one", "two")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(echo.HeaderLocation))
|
||||
assert.Equal(t, http.StatusFound, ctx.Response().Status)
|
||||
})
|
||||
|
||||
t.Run("query htmx", func(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(c.Web, "/abc")
|
||||
ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
err := redirectWithQuery(ctx, q, "redirect-test", "one", "two")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(htmx.HeaderRedirect))
|
||||
assert.Equal(t, http.StatusFound, ctx.Response().Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
err := fail(errors.New("err message"), "log message")
|
||||
require.IsType(t, new(echo.HTTPError), err)
|
||||
|
91
pkg/redirect/redirect.go
Normal file
91
pkg/redirect/redirect.go
Normal file
@ -0,0 +1,91 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||
)
|
||||
|
||||
// Redirect is a helper to perform HTTP redirects.
|
||||
type Redirect struct {
|
||||
ctx echo.Context
|
||||
url string
|
||||
routeName string
|
||||
routeParams []any
|
||||
status int
|
||||
query url.Values
|
||||
}
|
||||
|
||||
// New initializes a new Redirect
|
||||
func New(ctx echo.Context) *Redirect {
|
||||
return &Redirect{
|
||||
ctx: ctx,
|
||||
status: http.StatusFound,
|
||||
}
|
||||
}
|
||||
|
||||
// Route sets the route name to redirect to.
|
||||
// Use either this or URL()
|
||||
func (r *Redirect) Route(name string) *Redirect {
|
||||
r.routeName = name
|
||||
return r
|
||||
}
|
||||
|
||||
// Params sets the route params
|
||||
func (r *Redirect) Params(params ...any) *Redirect {
|
||||
r.routeParams = params
|
||||
return r
|
||||
}
|
||||
|
||||
// StatusCode sets the HTTP status code which defaults to http.StatusFound.
|
||||
// Does not apply to HTMX redirects.
|
||||
func (r *Redirect) StatusCode(code int) *Redirect {
|
||||
r.status = code
|
||||
return r
|
||||
}
|
||||
|
||||
// Query sets a URL query
|
||||
func (r *Redirect) Query(query url.Values) *Redirect {
|
||||
r.query = query
|
||||
return r
|
||||
}
|
||||
|
||||
// URL sets the URL to redirect to
|
||||
// Use either this or Route()
|
||||
func (r *Redirect) URL(url string) *Redirect {
|
||||
r.url = url
|
||||
return r
|
||||
}
|
||||
|
||||
// Go performs the redirect
|
||||
// If the request is HTMX boosted, an HTMX redirect will be performed instead of an HTTP redirect
|
||||
func (r *Redirect) Go() error {
|
||||
if r.routeName == "" && r.url == "" {
|
||||
return errors.New("no redirect provided")
|
||||
}
|
||||
|
||||
var dest string
|
||||
if r.url != "" {
|
||||
dest = r.url
|
||||
} else {
|
||||
dest = r.ctx.Echo().Reverse(r.routeName, r.routeParams...)
|
||||
}
|
||||
|
||||
if len(r.query) > 0 {
|
||||
dest = fmt.Sprintf("%s?%s", dest, r.query.Encode())
|
||||
}
|
||||
|
||||
if htmx.GetRequest(r.ctx).Boosted {
|
||||
htmx.Response{
|
||||
Redirect: dest,
|
||||
}.Apply(r.ctx)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return r.ctx.Redirect(r.status, dest)
|
||||
}
|
||||
}
|
77
pkg/redirect/redirect_test.go
Normal file
77
pkg/redirect/redirect_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||
"github.com/mikestefanello/pagoda/pkg/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
e := echo.New()
|
||||
e.GET("/path/:first/and/:second", func(c echo.Context) error {
|
||||
return nil
|
||||
}).Name = "test"
|
||||
|
||||
redirect := func() (*Redirect, echo.Context) {
|
||||
ctx, _ := tests.NewContext(e, "/")
|
||||
return New(ctx), ctx
|
||||
}
|
||||
|
||||
t.Run("route", func(t *testing.T) {
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
r, ctx := redirect()
|
||||
r.Route("test")
|
||||
r.Params("one", "two")
|
||||
r.Query(q)
|
||||
r.StatusCode(http.StatusTemporaryRedirect)
|
||||
require.NoError(t, r.Go())
|
||||
assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(echo.HeaderLocation))
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, ctx.Response().Status)
|
||||
})
|
||||
|
||||
t.Run("route htmx", func(t *testing.T) {
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
r, ctx := redirect()
|
||||
ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
|
||||
r.Route("test")
|
||||
r.Params("one", "two")
|
||||
r.Query(q)
|
||||
require.NoError(t, r.Go())
|
||||
assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(htmx.HeaderRedirect))
|
||||
})
|
||||
|
||||
t.Run("url", func(t *testing.T) {
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
r, ctx := redirect()
|
||||
r.URL("https://localhost.dev")
|
||||
r.Query(q)
|
||||
r.StatusCode(http.StatusTemporaryRedirect)
|
||||
require.NoError(t, r.Go())
|
||||
assert.Equal(t, "https://localhost.dev?a=1&b=2", ctx.Response().Header().Get(echo.HeaderLocation))
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, ctx.Response().Status)
|
||||
})
|
||||
|
||||
t.Run("url htmx", func(t *testing.T) {
|
||||
q := url.Values{}
|
||||
q.Add("a", "1")
|
||||
q.Add("b", "2")
|
||||
r, ctx := redirect()
|
||||
ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
|
||||
r.URL("https://localhost.dev")
|
||||
r.Query(q)
|
||||
require.NoError(t, r.Go())
|
||||
assert.Equal(t, "https://localhost.dev?a=1&b=2", ctx.Response().Header().Get(htmx.HeaderRedirect))
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user