Initial commit of password reset workflow.
This commit is contained in:
parent
b4de8e58f9
commit
e6a5fa58c7
6 changed files with 184 additions and 16 deletions
39
auth/auth.go
39
auth/auth.go
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"goweb/config"
|
"goweb/config"
|
||||||
"goweb/ent"
|
"goweb/ent"
|
||||||
|
"goweb/ent/passwordtoken"
|
||||||
"goweb/ent/user"
|
"goweb/ent/user"
|
||||||
|
|
||||||
"github.com/labstack/echo-contrib/session"
|
"github.com/labstack/echo-contrib/session"
|
||||||
|
|
@ -20,6 +22,13 @@ const (
|
||||||
sessionKeyAuthenticated = "authenticated"
|
sessionKeyAuthenticated = "authenticated"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NotAuthenticatedError struct{}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e NotAuthenticatedError) Error() string {
|
||||||
|
return "user not authenticated"
|
||||||
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
orm *ent.Client
|
orm *ent.Client
|
||||||
|
|
@ -61,7 +70,7 @@ func (c *Client) GetAuthenticatedUserID(ctx echo.Context) (int, error) {
|
||||||
return sess.Values[sessionKeyUserID].(int), nil
|
return sess.Values[sessionKeyUserID].(int), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, errors.New("user not authenticated")
|
return 0, NotAuthenticatedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
|
func (c *Client) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
|
||||||
|
|
@ -71,7 +80,7 @@ func (c *Client) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
|
||||||
First(ctx.Request().Context())
|
First(ctx.Request().Context())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("user not authenticated")
|
return nil, NotAuthenticatedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) HashPassword(password string) (string, error) {
|
func (c *Client) HashPassword(password string) (string, error) {
|
||||||
|
|
@ -106,6 +115,32 @@ func (c *Client) GeneratePasswordResetToken(ctx echo.Context, userID int) (strin
|
||||||
return token, pt, err
|
return token, pt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetValidPasswordToken(ctx echo.Context, token string) (*ent.PasswordToken, error) {
|
||||||
|
// Hash the token in order to match in the database
|
||||||
|
hash, err := c.HashPassword(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query to find a matching token
|
||||||
|
pt, err := c.orm.PasswordToken.
|
||||||
|
Query().
|
||||||
|
Where(passwordtoken.Hash(hash)).
|
||||||
|
First(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token is no longer valid
|
||||||
|
if pt.CreatedAt.Before(time.Now().Add(-c.config.App.PasswordTokenExpiration)) {
|
||||||
|
return nil, errors.New("token has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) RandomToken(length int) string {
|
func (c *Client) RandomToken(length int) string {
|
||||||
b := make([]byte, length)
|
b := make([]byte, length)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ package context
|
||||||
const (
|
const (
|
||||||
AuthenticatedUserKey = "auth_user"
|
AuthenticatedUserKey = "auth_user"
|
||||||
FormKey = "form"
|
FormKey = "form"
|
||||||
|
PasswordTokenKey = "password_token"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"goweb/auth"
|
"goweb/auth"
|
||||||
"goweb/context"
|
"goweb/context"
|
||||||
"goweb/ent"
|
"goweb/ent"
|
||||||
|
"goweb/msg"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
@ -13,17 +14,39 @@ import (
|
||||||
func LoadAuthenticatedUser(authClient *auth.Client) echo.MiddlewareFunc {
|
func LoadAuthenticatedUser(authClient *auth.Client) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
if user, err := authClient.GetAuthenticatedUser(c); err == nil {
|
u, err := authClient.GetAuthenticatedUser(c)
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *ent.NotFoundError:
|
case *ent.NotFoundError:
|
||||||
c.Logger().Debug("auth user not found")
|
c.Logger().Debug("auth user not found")
|
||||||
|
case auth.NotAuthenticatedError:
|
||||||
case nil:
|
case nil:
|
||||||
c.Set(context.AuthenticatedUserKey, user)
|
c.Set(context.AuthenticatedUserKey, u)
|
||||||
c.Logger().Info("auth user loaded in to context: %d", user.ID)
|
c.Logger().Info("auth user loaded in to context: %d", u.ID)
|
||||||
default:
|
default:
|
||||||
c.Logger().Errorf("error querying for authenticated user: %v", err)
|
c.Logger().Errorf("error querying for authenticated user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return next(c)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadValidPasswordToken(authClient *auth.Client) echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
tokenParam := c.Param("password_token")
|
||||||
|
if tokenParam == "" {
|
||||||
|
c.Logger().Warn("missing password token path parameter")
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := authClient.GetValidPasswordToken(c, tokenParam)
|
||||||
|
if err != nil {
|
||||||
|
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
||||||
|
return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(context.PasswordTokenKey, token)
|
||||||
|
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
routes/reset_password.go
Normal file
82
routes/reset_password.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"goweb/controller"
|
||||||
|
"goweb/msg"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ResetPassword struct {
|
||||||
|
controller.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetPasswordForm struct {
|
||||||
|
Password string `form:"password" validate:"required" label:"Password"`
|
||||||
|
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password" label:"Confirm password"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *ResetPassword) Get(c echo.Context) error {
|
||||||
|
p := controller.NewPage(c)
|
||||||
|
p.Layout = "auth"
|
||||||
|
p.Name = "reset-password"
|
||||||
|
p.Title = "Reset password"
|
||||||
|
return r.RenderPage(c, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResetPassword) Post(c echo.Context) error {
|
||||||
|
fail := func(message string, err error) error {
|
||||||
|
c.Logger().Errorf("%s: %v", message, err)
|
||||||
|
msg.Danger(c, "An error occurred. Please try again.")
|
||||||
|
return r.Get(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
succeed := func() error {
|
||||||
|
msg.Success(c, "Your password has been updated.")
|
||||||
|
return r.Redirect(c, "login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the form values
|
||||||
|
form := new(ResetPassword)
|
||||||
|
if err := c.Bind(form); err != nil {
|
||||||
|
return fail("unable to parse forgot password form", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the form
|
||||||
|
if err := c.Validate(form); err != nil {
|
||||||
|
r.SetValidationErrorMessages(c, err, form)
|
||||||
|
return r.Get(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to load the user
|
||||||
|
//u, err := f.Container.ORM.User.
|
||||||
|
// Query().
|
||||||
|
// Where(user.Email(form.Email)).
|
||||||
|
// First(c.Request().Context())
|
||||||
|
//
|
||||||
|
//if err != nil {
|
||||||
|
// switch err.(type) {
|
||||||
|
// case *ent.NotFoundError:
|
||||||
|
// return succeed()
|
||||||
|
// default:
|
||||||
|
// return fail("error querying user during forgot password", err)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//// Generate the token
|
||||||
|
//token, _, err := f.Container.Auth.GeneratePasswordResetToken(c, u.ID)
|
||||||
|
//if err != nil {
|
||||||
|
// return fail("error generating password reset token", err)
|
||||||
|
//}
|
||||||
|
//c.Logger().Infof("generated password reset token for user %d", u.ID)
|
||||||
|
//
|
||||||
|
//// Email the user
|
||||||
|
//err = f.Container.Mail.Send(c, u.Email, fmt.Sprintf("Go here to reset your password: %s", token)) // TODO: route
|
||||||
|
//if err != nil {
|
||||||
|
// return fail("error sending password reset email", err)
|
||||||
|
//}
|
||||||
|
|
||||||
|
return succeed()
|
||||||
|
}
|
||||||
|
|
@ -71,11 +71,11 @@ func BuildRouter(c *container.Container) {
|
||||||
c.Web.Validator = &Validator{validator: validator.New()}
|
c.Web.Validator = &Validator{validator: validator.New()}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
navRoutes(g, ctr)
|
navRoutes(c, g, ctr)
|
||||||
userRoutes(g, ctr)
|
userRoutes(c, g, ctr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func navRoutes(g *echo.Group, ctr controller.Controller) {
|
func navRoutes(c *container.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 = "home"
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ func navRoutes(g *echo.Group, ctr controller.Controller) {
|
||||||
g.POST("/contact", contact.Post).Name = "contact.post"
|
g.POST("/contact", contact.Post).Name = "contact.post"
|
||||||
}
|
}
|
||||||
|
|
||||||
func userRoutes(g *echo.Group, ctr controller.Controller) {
|
func userRoutes(c *container.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 = "logout"
|
||||||
|
|
||||||
|
|
@ -103,4 +103,9 @@ func userRoutes(g *echo.Group, ctr controller.Controller) {
|
||||||
forgot := ForgotPassword{Controller: ctr}
|
forgot := ForgotPassword{Controller: ctr}
|
||||||
noAuth.GET("/password", forgot.Get).Name = "forgot_password"
|
noAuth.GET("/password", forgot.Get).Name = "forgot_password"
|
||||||
noAuth.POST("/password", forgot.Post).Name = "forgot_password.post"
|
noAuth.POST("/password", forgot.Post).Name = "forgot_password.post"
|
||||||
|
|
||||||
|
resetGroup := noAuth.Group("/password/reset", middleware.LoadValidPasswordToken(c.Auth))
|
||||||
|
reset := ResetPassword{Controller: ctr}
|
||||||
|
resetGroup.GET("/token/:password_token", reset.Get).Name = "reset_password"
|
||||||
|
resetGroup.POST("/token/:password_token", reset.Post).Name = "reset_password.post"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
templates/pages/reset-password.gohtml
Normal file
22
templates/pages/reset-password.gohtml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<form method="post">
|
||||||
|
<div class="field">
|
||||||
|
<label for="password" class="label">Password</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="password" id="password" name="password" placeholder="*******" class="input" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password-confirm" class="label">Confirm password</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="password" id="password-confirm" name="password-confirm" placeholder="*******" class="input" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary">Update password</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{template "csrf" .}}
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue