Merge pull request #65 from mikestefanello/handlers-3
Switch from routes to self-registering handlers to group related routes.
This commit is contained in:
commit
400b9b36ba
81
README.md
81
README.md
@ -45,8 +45,8 @@
|
|||||||
* [Email verification](#email-verification)
|
* [Email verification](#email-verification)
|
||||||
* [Routes](#routes)
|
* [Routes](#routes)
|
||||||
* [Custom middleware](#custom-middleware)
|
* [Custom middleware](#custom-middleware)
|
||||||
* [Controller / Dependencies](#controller--dependencies)
|
* [Controller](#controller)
|
||||||
* [Patterns](#patterns)
|
* [Handlers](#handlers)
|
||||||
* [Errors](#errors)
|
* [Errors](#errors)
|
||||||
* [Testing](#testing)
|
* [Testing](#testing)
|
||||||
* [HTTP server](#http-server)
|
* [HTTP server](#http-server)
|
||||||
@ -306,7 +306,7 @@ This executes a database query to return the _password token_ entity with a give
|
|||||||
|
|
||||||
## Sessions
|
## Sessions
|
||||||
|
|
||||||
Sessions are provided and handled via [Gorilla sessions](https://github.com/gorilla/sessions) and configured as middleware in the router located at `pkg/routes/router.go`. Session data is currently stored in cookies but there are many [options](https://github.com/gorilla/sessions#store-implementations) available if you wish to use something else.
|
Sessions are provided and handled via [Gorilla sessions](https://github.com/gorilla/sessions) and configured as middleware in the router located at `pkg/handlers/router.go`. Session data is currently stored in cookies but there are many [options](https://github.com/gorilla/sessions#store-implementations) available if you wish to use something else.
|
||||||
|
|
||||||
Here's a simple example of loading data from a session and saving new values:
|
Here's a simple example of loading data from a session and saving new values:
|
||||||
|
|
||||||
@ -384,7 +384,7 @@ To generate a new verification token, the `AuthClient` has a method `GenerateEma
|
|||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `pkg/routes/router.go`. Since the _Echo_ instance is a _Service_ on the `Container` which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
|
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `pkg/handlers/router.go`. Since the _Echo_ instance is a _Service_ on the `Container` which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
|
||||||
|
|
||||||
### Custom middleware
|
### Custom middleware
|
||||||
|
|
||||||
@ -392,56 +392,83 @@ By default, a middleware stack is included in the router that makes sense for mo
|
|||||||
|
|
||||||
A `middleware` package is included which you can easily add to along with the custom middleware provided.
|
A `middleware` package is included which you can easily add to along with the custom middleware provided.
|
||||||
|
|
||||||
### Controller / Dependencies
|
### Controller
|
||||||
|
|
||||||
The `Controller`, which is described in a section below, serves two purposes for routes:
|
The `Controller`, which is described in a section below, provides base functionality which can be embedded in each handler, most importantly `Page` rendering.
|
||||||
|
|
||||||
1) It provides base functionality which can be embedded in each route, most importantly `Page` rendering (described in the `Controller` section below)
|
|
||||||
2) It stores a pointer to the `Container`, making all _Services_ available within your route
|
|
||||||
|
|
||||||
While using the `Controller` is not required for your routes, it will certainly make development easier.
|
While using the `Controller` is not required for your routes, it will certainly make development easier.
|
||||||
|
|
||||||
See the following section for the proposed pattern.
|
### Handlers
|
||||||
|
|
||||||
### Patterns
|
A `Handler` is a simple type that handles one or more of your routes and allows you to group related routes together (ie, authentication). All provided handlers are located in `pkg/handlers`. _Handlers_ also handle self-registering their routes with the router.
|
||||||
|
|
||||||
These patterns are not required, but were designed to make development as easy as possible.
|
#### Example
|
||||||
|
|
||||||
To declare a new route that will have methods to handle a GET and POST request, for example, start with a new _struct_ type, that embeds the `Controller`:
|
The provided patterns are not required, but were designed to make development as easy as possible.
|
||||||
|
|
||||||
|
For this example, we'll create a new handler which includes a GET and POST route and uses the ORM. Start by creating a file at `pkg/handlers/example.go`.
|
||||||
|
|
||||||
|
1) Define the handler type:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type home struct {
|
type Example struct {
|
||||||
|
orm *ent.Client
|
||||||
controller.Controller
|
controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *home) Get(ctx echo.Context) error {}
|
|
||||||
|
|
||||||
func (c *home) Post(ctx echo.Context) error {}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then create the route and add to the router:
|
2) Register the handler so the router automatically includes it
|
||||||
|
|
||||||
```go
|
```go
|
||||||
home := home{Controller: controller.NewController(c)}
|
func init() {
|
||||||
g.GET("/", home.Get).Name = "home"
|
Register(new(Example))
|
||||||
g.POST("/", home.Post).Name = "home.post"
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Your route will now have all methods available on the `Controller` as well as access to the `Container`. It's not required to name the route methods to match the HTTP method.
|
3) Initialize the handler (and inject any required dependencies from the _Container_)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (e *Example) Init(c *services.Container) error {
|
||||||
|
e.Controller = controller.NewController(c)
|
||||||
|
e.orm = c.ORM
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4) Declare the routes
|
||||||
|
|
||||||
**It is highly recommended** that you provide a `Name` for your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs.
|
**It is highly recommended** that you provide a `Name` for your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (e *Example) Routes(g *echo.Group) {
|
||||||
|
g.GET("/example", e.Page).Name = "example"
|
||||||
|
g.POST("/example", c.PageSubmit).Name = "example.submit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5) Implement your routes
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (e *Example) Page(ctx echo.Context) error {
|
||||||
|
// add your code here
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Example) PageSubmit(ctx echo.Context) error {
|
||||||
|
// add your code here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
Routes can return errors to indicate that something wrong happened. Ideally, the error is of type `*echo.HTTPError` to indicate the intended HTTP response code. You can use `return echo.NewHTTPError(http.StatusInternalServerError)`, for example. If an error of a different type is returned, an _Internal Server Error_ is assumed.
|
Routes can return errors to indicate that something wrong happened. Ideally, the error is of type `*echo.HTTPError` to indicate the intended HTTP response code. You can use `return echo.NewHTTPError(http.StatusInternalServerError)`, for example. If an error of a different type is returned, an _Internal Server Error_ is assumed.
|
||||||
|
|
||||||
The [error handler](https://echo.labstack.com/guide/error-handling/) is set to a provided route `pkg/routes/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.
|
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.
|
||||||
|
|
||||||
### Testing
|
### 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.
|
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.
|
||||||
|
|
||||||
The test setup and helpers reside in `pkg/routes/router_test.go`.
|
The test setup and helpers reside in `pkg/handlers/router_test.go`.
|
||||||
|
|
||||||
Only a brief example of route tests were provided in order to highlight what is available. Adding full tests did not seem logical since these routes will most likely be changed or removed in your project.
|
Only a brief example of route tests were provided in order to highlight what is available. Adding full tests did not seem logical since these routes will most likely be changed or removed in your project.
|
||||||
|
|
||||||
@ -451,7 +478,7 @@ When the route tests initialize, a new `Container` is created which provides ful
|
|||||||
|
|
||||||
#### Request / Response helpers
|
#### Request / Response helpers
|
||||||
|
|
||||||
With the test HTTP server setup, test helpers for making HTTP requests and evaluating responses are made available to reduce the amount of code you need to write. See `httpRequest` and `httpResponse` within `pkg/routes/router_test.go`.
|
With the test HTTP server setup, test helpers for making HTTP requests and evaluating responses are made available to reduce the amount of code you need to write. See `httpRequest` and `httpResponse` within `pkg/handlers/router_test.go`.
|
||||||
|
|
||||||
Here is an example how to easily make a request and evaluate the response:
|
Here is an example how to easily make a request and evaluate the response:
|
||||||
|
|
||||||
@ -1126,7 +1153,7 @@ Finally, the service is started with `async.Server.Run(mux)`.
|
|||||||
|
|
||||||
## Static files
|
## Static files
|
||||||
|
|
||||||
Static files are currently configured in the router (`pkg/routes/router.go`) to be served from the `static` directory. If you wish to change the directory, alter the constant `config.StaticDir`. The URL prefix for static files is `/files` which is controlled via the `config.StaticPrefix` constant.
|
Static files are currently configured in the router (`pkg/handler/router.go`) to be served from the `static` directory. If you wish to change the directory, alter the constant `config.StaticDir`. The URL prefix for static files is `/files` which is controlled via the `config.StaticPrefix` constant.
|
||||||
|
|
||||||
### Cache control headers
|
### Cache control headers
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/routes"
|
"github.com/mikestefanello/pagoda/pkg/handlers"
|
||||||
"github.com/mikestefanello/pagoda/pkg/services"
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +23,9 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Build the router
|
// Build the router
|
||||||
routes.BuildRouter(c)
|
if err := handlers.BuildRouter(c); err != nil {
|
||||||
|
c.Web.Logger.Fatalf("failed to build the router: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
go func() {
|
go func() {
|
||||||
|
451
pkg/handlers/auth.go
Normal file
451
pkg/handlers/auth.go
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/ent"
|
||||||
|
"github.com/mikestefanello/pagoda/ent/user"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/middleware"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Auth struct {
|
||||||
|
auth *services.AuthClient
|
||||||
|
mail *services.MailClient
|
||||||
|
orm *ent.Client
|
||||||
|
controller.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
forgotPasswordForm struct {
|
||||||
|
Email string `form:"email" validate:"required,email"`
|
||||||
|
Submission controller.FormSubmission
|
||||||
|
}
|
||||||
|
|
||||||
|
loginForm struct {
|
||||||
|
Email string `form:"email" validate:"required,email"`
|
||||||
|
Password string `form:"password" validate:"required"`
|
||||||
|
Submission controller.FormSubmission
|
||||||
|
}
|
||||||
|
|
||||||
|
registerForm struct {
|
||||||
|
Name string `form:"name" validate:"required"`
|
||||||
|
Email string `form:"email" validate:"required,email"`
|
||||||
|
Password string `form:"password" validate:"required"`
|
||||||
|
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
|
||||||
|
Submission controller.FormSubmission
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPasswordForm struct {
|
||||||
|
Password string `form:"password" validate:"required"`
|
||||||
|
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
|
||||||
|
Submission controller.FormSubmission
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(new(Auth))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) Init(ct *services.Container) error {
|
||||||
|
c.Controller = controller.NewController(ct)
|
||||||
|
c.orm = ct.ORM
|
||||||
|
c.auth = ct.Auth
|
||||||
|
c.mail = ct.Mail
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) Routes(g *echo.Group) {
|
||||||
|
g.GET("/logout", c.Logout, middleware.RequireAuthentication()).Name = routeNameLogout
|
||||||
|
g.GET("/email/verify/:token", c.VerifyEmail).Name = routeNameVerifyEmail
|
||||||
|
|
||||||
|
noAuth := g.Group("/user", middleware.RequireNoAuthentication())
|
||||||
|
noAuth.GET("/login", c.LoginPage).Name = routeNameLogin
|
||||||
|
noAuth.POST("/login", c.LoginSubmit).Name = routeNameLoginSubmit
|
||||||
|
noAuth.GET("/register", c.RegisterPage).Name = routeNameRegister
|
||||||
|
noAuth.POST("/register", c.RegisterSubmit).Name = routeNameRegisterSubmit
|
||||||
|
noAuth.GET("/password", c.ForgotPasswordPage).Name = routeNameForgotPassword
|
||||||
|
noAuth.POST("/password", c.ForgotPasswordSubmit).Name = routeNameForgotPasswordSubmit
|
||||||
|
|
||||||
|
resetGroup := noAuth.Group("/password/reset",
|
||||||
|
middleware.LoadUser(c.orm),
|
||||||
|
middleware.LoadValidPasswordToken(c.auth),
|
||||||
|
)
|
||||||
|
resetGroup.GET("/token/:user/:password_token/:token", c.ResetPasswordPage).Name = routeNameResetPassword
|
||||||
|
resetGroup.POST("/token/:user/:password_token/:token", c.ResetPasswordSubmit).Name = routeNameResetPasswordSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) ForgotPasswordPage(ctx echo.Context) error {
|
||||||
|
page := controller.NewPage(ctx)
|
||||||
|
page.Layout = templates.LayoutAuth
|
||||||
|
page.Name = templates.PageForgotPassword
|
||||||
|
page.Title = "Forgot password"
|
||||||
|
page.Form = forgotPasswordForm{}
|
||||||
|
|
||||||
|
if form := ctx.Get(context.FormKey); form != nil {
|
||||||
|
page.Form = form.(*forgotPasswordForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RenderPage(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) ForgotPasswordSubmit(ctx echo.Context) error {
|
||||||
|
var form forgotPasswordForm
|
||||||
|
ctx.Set(context.FormKey, &form)
|
||||||
|
|
||||||
|
succeed := func() error {
|
||||||
|
ctx.Set(context.FormKey, nil)
|
||||||
|
msg.Success(ctx, "An email containing a link to reset your password will be sent to this address if it exists in our system.")
|
||||||
|
return c.ForgotPasswordPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the form values
|
||||||
|
if err := ctx.Bind(&form); err != nil {
|
||||||
|
return c.Fail(err, "unable to parse forgot password form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := form.Submission.Process(ctx, form); err != nil {
|
||||||
|
return c.Fail(err, "unable to process form submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Submission.HasErrors() {
|
||||||
|
return c.ForgotPasswordPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to load the user
|
||||||
|
u, err := c.orm.User.
|
||||||
|
Query().
|
||||||
|
Where(user.Email(strings.ToLower(form.Email))).
|
||||||
|
Only(ctx.Request().Context())
|
||||||
|
|
||||||
|
switch err.(type) {
|
||||||
|
case *ent.NotFoundError:
|
||||||
|
return succeed()
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
return c.Fail(err, "error querying user during forgot password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the token
|
||||||
|
token, pt, err := c.auth.GeneratePasswordResetToken(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "error generating password reset token")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
||||||
|
|
||||||
|
// Email the user
|
||||||
|
url := ctx.Echo().Reverse(routeNameResetPassword, u.ID, pt.ID, token)
|
||||||
|
err = c.mail.
|
||||||
|
Compose().
|
||||||
|
To(u.Email).
|
||||||
|
Subject("Reset your password").
|
||||||
|
Body(fmt.Sprintf("Go here to reset your password: %s", url)).
|
||||||
|
Send(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "error sending password reset email")
|
||||||
|
}
|
||||||
|
|
||||||
|
return succeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) LoginPage(ctx echo.Context) error {
|
||||||
|
page := controller.NewPage(ctx)
|
||||||
|
page.Layout = templates.LayoutAuth
|
||||||
|
page.Name = templates.PageLogin
|
||||||
|
page.Title = "Log in"
|
||||||
|
page.Form = loginForm{}
|
||||||
|
|
||||||
|
if form := ctx.Get(context.FormKey); form != nil {
|
||||||
|
page.Form = form.(*loginForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RenderPage(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) LoginSubmit(ctx echo.Context) error {
|
||||||
|
var form loginForm
|
||||||
|
ctx.Set(context.FormKey, &form)
|
||||||
|
|
||||||
|
authFailed := func() error {
|
||||||
|
form.Submission.SetFieldError("Email", "")
|
||||||
|
form.Submission.SetFieldError("Password", "")
|
||||||
|
msg.Danger(ctx, "Invalid credentials. Please try again.")
|
||||||
|
return c.LoginPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the form values
|
||||||
|
if err := ctx.Bind(&form); err != nil {
|
||||||
|
return c.Fail(err, "unable to parse login form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := form.Submission.Process(ctx, form); err != nil {
|
||||||
|
return c.Fail(err, "unable to process form submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Submission.HasErrors() {
|
||||||
|
return c.LoginPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to load the user
|
||||||
|
u, err := c.orm.User.
|
||||||
|
Query().
|
||||||
|
Where(user.Email(strings.ToLower(form.Email))).
|
||||||
|
Only(ctx.Request().Context())
|
||||||
|
|
||||||
|
switch err.(type) {
|
||||||
|
case *ent.NotFoundError:
|
||||||
|
return authFailed()
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
return c.Fail(err, "error querying user during login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the password is correct
|
||||||
|
err = c.auth.CheckPassword(form.Password, u.Password)
|
||||||
|
if err != nil {
|
||||||
|
return authFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the user in
|
||||||
|
err = c.auth.Login(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "unable to log in user")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, fmt.Sprintf("Welcome back, <strong>%s</strong>. You are now logged in.", u.Name))
|
||||||
|
return c.Redirect(ctx, routeNameHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) Logout(ctx echo.Context) error {
|
||||||
|
if err := c.auth.Logout(ctx); err == nil {
|
||||||
|
msg.Success(ctx, "You have been logged out successfully.")
|
||||||
|
} else {
|
||||||
|
msg.Danger(ctx, "An error occurred. Please try again.")
|
||||||
|
}
|
||||||
|
return c.Redirect(ctx, routeNameHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) RegisterPage(ctx echo.Context) error {
|
||||||
|
page := controller.NewPage(ctx)
|
||||||
|
page.Layout = templates.LayoutAuth
|
||||||
|
page.Name = templates.PageRegister
|
||||||
|
page.Title = "Register"
|
||||||
|
page.Form = registerForm{}
|
||||||
|
|
||||||
|
if form := ctx.Get(context.FormKey); form != nil {
|
||||||
|
page.Form = form.(*registerForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RenderPage(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) RegisterSubmit(ctx echo.Context) error {
|
||||||
|
var form registerForm
|
||||||
|
ctx.Set(context.FormKey, &form)
|
||||||
|
|
||||||
|
// Parse the form values
|
||||||
|
if err := ctx.Bind(&form); err != nil {
|
||||||
|
return c.Fail(err, "unable to parse register form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := form.Submission.Process(ctx, form); err != nil {
|
||||||
|
return c.Fail(err, "unable to process form submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Submission.HasErrors() {
|
||||||
|
return c.RegisterPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password
|
||||||
|
pwHash, err := c.auth.HashPassword(form.Password)
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "unable to hash password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt creating the user
|
||||||
|
u, err := c.orm.User.
|
||||||
|
Create().
|
||||||
|
SetName(form.Name).
|
||||||
|
SetEmail(form.Email).
|
||||||
|
SetPassword(pwHash).
|
||||||
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
switch err.(type) {
|
||||||
|
case nil:
|
||||||
|
ctx.Logger().Infof("user created: %s", u.Name)
|
||||||
|
case *ent.ConstraintError:
|
||||||
|
msg.Warning(ctx, "A user with this email address already exists. Please log in.")
|
||||||
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
|
default:
|
||||||
|
return c.Fail(err, "unable to create user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the user in
|
||||||
|
err = c.auth.Login(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Errorf("unable to log in: %v", err)
|
||||||
|
msg.Info(ctx, "Your account has been created.")
|
||||||
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, "Your account has been created. You are now logged in.")
|
||||||
|
|
||||||
|
// Send the verification email
|
||||||
|
c.sendVerificationEmail(ctx, u)
|
||||||
|
|
||||||
|
return c.Redirect(ctx, routeNameHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
||||||
|
// Generate a token
|
||||||
|
token, err := c.auth.GenerateEmailVerificationToken(usr.Email)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Errorf("unable to generate email verification token: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
url := ctx.Echo().Reverse(routeNameVerifyEmail, token)
|
||||||
|
err = c.mail.
|
||||||
|
Compose().
|
||||||
|
To(usr.Email).
|
||||||
|
Subject("Confirm your email address").
|
||||||
|
Body(fmt.Sprintf("Click here to confirm your email address: %s", url)).
|
||||||
|
Send(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Errorf("unable to send email verification link: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Info(ctx, "An email was sent to you to verify your email address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) ResetPasswordPage(ctx echo.Context) error {
|
||||||
|
page := controller.NewPage(ctx)
|
||||||
|
page.Layout = templates.LayoutAuth
|
||||||
|
page.Name = templates.PageResetPassword
|
||||||
|
page.Title = "Reset password"
|
||||||
|
page.Form = resetPasswordForm{}
|
||||||
|
|
||||||
|
if form := ctx.Get(context.FormKey); form != nil {
|
||||||
|
page.Form = form.(*resetPasswordForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RenderPage(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) ResetPasswordSubmit(ctx echo.Context) error {
|
||||||
|
var form resetPasswordForm
|
||||||
|
ctx.Set(context.FormKey, &form)
|
||||||
|
|
||||||
|
// Parse the form values
|
||||||
|
if err := ctx.Bind(&form); err != nil {
|
||||||
|
return c.Fail(err, "unable to parse password reset form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := form.Submission.Process(ctx, form); err != nil {
|
||||||
|
return c.Fail(err, "unable to process form submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Submission.HasErrors() {
|
||||||
|
return c.ResetPasswordPage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the new password
|
||||||
|
hash, err := c.auth.HashPassword(form.Password)
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "unable to hash password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the requesting user
|
||||||
|
usr := ctx.Get(context.UserKey).(*ent.User)
|
||||||
|
|
||||||
|
// Update the user
|
||||||
|
_, err = usr.
|
||||||
|
Update().
|
||||||
|
SetPassword(hash).
|
||||||
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "unable to update password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all password tokens for this user
|
||||||
|
err = c.auth.DeletePasswordTokens(ctx, usr.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "unable to delete password tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, "Your password has been updated.")
|
||||||
|
return c.Redirect(ctx, routeNameLogin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Auth) VerifyEmail(ctx echo.Context) error {
|
||||||
|
var usr *ent.User
|
||||||
|
|
||||||
|
// Validate the token
|
||||||
|
token := ctx.Param("token")
|
||||||
|
email, err := c.auth.ValidateEmailVerificationToken(token)
|
||||||
|
if err != nil {
|
||||||
|
msg.Warning(ctx, "The link is either invalid or has expired.")
|
||||||
|
return c.Redirect(ctx, routeNameHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it matches the authenticated user
|
||||||
|
if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
|
||||||
|
authUser := u.(*ent.User)
|
||||||
|
|
||||||
|
if authUser.Email == email {
|
||||||
|
usr = authUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query to find a matching user, if needed
|
||||||
|
if usr == nil {
|
||||||
|
usr, err = c.orm.User.
|
||||||
|
Query().
|
||||||
|
Where(user.Email(email)).
|
||||||
|
Only(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "query failed loading email verification token user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the user, if needed
|
||||||
|
if !usr.Verified {
|
||||||
|
usr, err = usr.
|
||||||
|
Update().
|
||||||
|
SetVerified(true).
|
||||||
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Fail(err, "failed to set user as verified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, "Your email has been successfully verified.")
|
||||||
|
return c.Redirect(ctx, routeNameHome)
|
||||||
|
}
|
@ -1,17 +1,23 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
"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/services"
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
const (
|
||||||
|
routeNameContact = "contact"
|
||||||
|
routeNameContactSubmit = "contact.submit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
contact struct {
|
Contact struct {
|
||||||
|
mail *services.MailClient
|
||||||
controller.Controller
|
controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +29,22 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *contact) Get(ctx echo.Context) error {
|
func init() {
|
||||||
|
Register(new(Contact))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Contact) Init(ct *services.Container) error {
|
||||||
|
c.Controller = controller.NewController(ct)
|
||||||
|
c.mail = ct.Mail
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Contact) Routes(g *echo.Group) {
|
||||||
|
g.GET("/contact", c.Page).Name = routeNameContact
|
||||||
|
g.POST("/contact", c.Submit).Name = routeNameContactSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Contact) Page(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = templates.LayoutMain
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = templates.PageContact
|
page.Name = templates.PageContact
|
||||||
@ -37,7 +58,7 @@ func (c *contact) Get(ctx echo.Context) error {
|
|||||||
return c.RenderPage(ctx, page)
|
return c.RenderPage(ctx, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *contact) Post(ctx echo.Context) error {
|
func (c *Contact) Submit(ctx echo.Context) error {
|
||||||
var form contactForm
|
var form contactForm
|
||||||
ctx.Set(context.FormKey, &form)
|
ctx.Set(context.FormKey, &form)
|
||||||
|
|
||||||
@ -51,7 +72,7 @@ func (c *contact) Post(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !form.Submission.HasErrors() {
|
if !form.Submission.HasErrors() {
|
||||||
err := c.Container.Mail.
|
err := c.mail.
|
||||||
Compose().
|
Compose().
|
||||||
To(form.Email).
|
To(form.Email).
|
||||||
Subject("Contact form submitted").
|
Subject("Contact form submitted").
|
||||||
@ -63,5 +84,5 @@ func (c *contact) Post(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Get(ctx)
|
return c.Page(ctx)
|
||||||
}
|
}
|
@ -1,20 +1,19 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
"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/mikestefanello/pagoda/templates"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorHandler struct {
|
type Error struct {
|
||||||
controller.Controller
|
controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *errorHandler) Get(err error, ctx echo.Context) {
|
func (e *Error) Page(err error, ctx echo.Context) {
|
||||||
if ctx.Response().Committed || context.IsCanceledError(err) {
|
if ctx.Response().Committed || context.IsCanceledError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -31,9 +30,9 @@ func (e *errorHandler) Get(err error, ctx echo.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Title = http.StatusText(code)
|
|
||||||
page.Layout = templates.LayoutMain
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = templates.PageError
|
page.Name = templates.PageError
|
||||||
|
page.Title = http.StatusText(code)
|
||||||
page.StatusCode = code
|
page.StatusCode = code
|
||||||
page.HTMX.Request.Enabled = false
|
page.HTMX.Request.Enabled = false
|
||||||
|
|
27
pkg/handlers/handlers.go
Normal file
27
pkg/handlers/handlers.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handlers []Handler
|
||||||
|
|
||||||
|
// Handler handles one or more HTTP routes
|
||||||
|
type Handler interface {
|
||||||
|
// Routes allows for self-registration of HTTP routes on the router
|
||||||
|
Routes(g *echo.Group)
|
||||||
|
|
||||||
|
// Init provides the service container to initialize
|
||||||
|
Init(*services.Container) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers a handler
|
||||||
|
func Register(h Handler) {
|
||||||
|
handlers = append(handlers, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHandlers returns all handlers
|
||||||
|
func GetHandlers() []Handler {
|
||||||
|
return handlers
|
||||||
|
}
|
@ -1,19 +1,30 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
routeNameAbout = "about"
|
||||||
|
routeNameHome = "home"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
about struct {
|
Pages struct {
|
||||||
controller.Controller
|
controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post struct {
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
aboutData struct {
|
aboutData struct {
|
||||||
ShowCacheWarning bool
|
ShowCacheWarning bool
|
||||||
FrontendTabs []aboutTab
|
FrontendTabs []aboutTab
|
||||||
@ -26,7 +37,47 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *about) Get(ctx echo.Context) error {
|
func init() {
|
||||||
|
Register(new(Pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Pages) Init(ct *services.Container) error {
|
||||||
|
c.Controller = controller.NewController(ct)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Pages) Routes(g *echo.Group) {
|
||||||
|
g.GET("/", c.Home).Name = routeNameHome
|
||||||
|
g.GET("/about", c.About).Name = routeNameAbout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Pages) Home(ctx echo.Context) error {
|
||||||
|
page := controller.NewPage(ctx)
|
||||||
|
page.Layout = templates.LayoutMain
|
||||||
|
page.Name = templates.PageHome
|
||||||
|
page.Metatags.Description = "Welcome to the homepage."
|
||||||
|
page.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
|
||||||
|
page.Pager = controller.NewPager(ctx, 4)
|
||||||
|
page.Data = c.fetchPosts(&page.Pager)
|
||||||
|
|
||||||
|
return c.RenderPage(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchPosts is an mock example of fetching posts to illustrate how paging works
|
||||||
|
func (c *Pages) fetchPosts(pager *controller.Pager) []post {
|
||||||
|
pager.SetItems(20)
|
||||||
|
posts := make([]post, 20)
|
||||||
|
|
||||||
|
for k := range posts {
|
||||||
|
posts[k] = post{
|
||||||
|
Title: fmt.Sprintf("Post example #%d", k+1),
|
||||||
|
Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Pages) About(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = templates.LayoutMain
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = templates.PageAbout
|
page.Name = templates.PageAbout
|
@ -1,4 +1,4 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// Simple example of how to test routes and their markup using the test HTTP server spun up within
|
// Simple example of how to test routes and their markup using the test HTTP server spun up within
|
||||||
// this test package
|
// this test package
|
||||||
func TestAbout_Get(t *testing.T) {
|
func TestPages__About(t *testing.T) {
|
||||||
doc := request(t).
|
doc := request(t).
|
||||||
setRoute(routeNameAbout).
|
setRoute(routeNameAbout).
|
||||||
get().
|
get().
|
66
pkg/handlers/router.go
Normal file
66
pkg/handlers/router.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/labstack/echo-contrib/session"
|
||||||
|
echomw "github.com/labstack/echo/v4/middleware"
|
||||||
|
"github.com/mikestefanello/pagoda/config"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/middleware"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildRouter builds the router
|
||||||
|
func BuildRouter(c *services.Container) error {
|
||||||
|
// Static files with proper cache control
|
||||||
|
// funcmap.File() should be used in templates to append a cache key to the URL in order to break cache
|
||||||
|
// after each server restart
|
||||||
|
c.Web.Group("", middleware.CacheControl(c.Config.Cache.Expiration.StaticFile)).
|
||||||
|
Static(config.StaticPrefix, config.StaticDir)
|
||||||
|
|
||||||
|
// Non-static file route group
|
||||||
|
g := c.Web.Group("")
|
||||||
|
|
||||||
|
// Force HTTPS, if enabled
|
||||||
|
if c.Config.HTTP.TLS.Enabled {
|
||||||
|
g.Use(echomw.HTTPSRedirect())
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Use(
|
||||||
|
echomw.RemoveTrailingSlashWithConfig(echomw.TrailingSlashConfig{
|
||||||
|
RedirectCode: http.StatusMovedPermanently,
|
||||||
|
}),
|
||||||
|
echomw.Recover(),
|
||||||
|
echomw.Secure(),
|
||||||
|
echomw.RequestID(),
|
||||||
|
echomw.Gzip(),
|
||||||
|
echomw.Logger(),
|
||||||
|
middleware.LogRequestID(),
|
||||||
|
echomw.TimeoutWithConfig(echomw.TimeoutConfig{
|
||||||
|
Timeout: c.Config.App.Timeout,
|
||||||
|
}),
|
||||||
|
session.Middleware(sessions.NewCookieStore([]byte(c.Config.App.EncryptionKey))),
|
||||||
|
middleware.LoadAuthenticatedUser(c.Auth),
|
||||||
|
middleware.ServeCachedPage(c.Cache),
|
||||||
|
echomw.CSRFWithConfig(echomw.CSRFConfig{
|
||||||
|
TokenLookup: "form:csrf",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
err := Error{Controller: controller.NewController(c)}
|
||||||
|
c.Web.HTTPErrorHandler = err.Page
|
||||||
|
|
||||||
|
// Initialize and register all handlers
|
||||||
|
for _, h := range GetHandlers() {
|
||||||
|
if err := h.Init(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Routes(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -30,7 +30,9 @@ func TestMain(m *testing.M) {
|
|||||||
c = services.NewContainer()
|
c = services.NewContainer()
|
||||||
|
|
||||||
// Start a test HTTP server
|
// Start a test HTTP server
|
||||||
BuildRouter(c)
|
if err := BuildRouter(c); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
srv = httptest.NewServer(c.Web)
|
srv = httptest.NewServer(c.Web)
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
@ -1,17 +1,19 @@
|
|||||||
package routes
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/controller"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const routeNameSearch = "search"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
search struct {
|
Search struct {
|
||||||
controller.Controller
|
controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +23,20 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *search) Get(ctx echo.Context) error {
|
func init() {
|
||||||
|
Register(new(Search))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Search) Init(ct *services.Container) error {
|
||||||
|
c.Controller = controller.NewController(ct)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Search) Routes(g *echo.Group) {
|
||||||
|
g.GET("/search", c.Page).Name = routeNameSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Search) Page(ctx echo.Context) error {
|
||||||
page := controller.NewPage(ctx)
|
page := controller.NewPage(ctx)
|
||||||
page.Layout = templates.LayoutMain
|
page.Layout = templates.LayoutMain
|
||||||
page.Name = templates.PageSearch
|
page.Name = templates.PageSearch
|
@ -1,101 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
|
||||||
"github.com/mikestefanello/pagoda/ent/user"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
forgotPassword struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
forgotPasswordForm struct {
|
|
||||||
Email string `form:"email" validate:"required,email"`
|
|
||||||
Submission controller.FormSubmission
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *forgotPassword) Get(ctx echo.Context) error {
|
|
||||||
page := controller.NewPage(ctx)
|
|
||||||
page.Layout = templates.LayoutAuth
|
|
||||||
page.Name = templates.PageForgotPassword
|
|
||||||
page.Title = "Forgot password"
|
|
||||||
page.Form = forgotPasswordForm{}
|
|
||||||
|
|
||||||
if form := ctx.Get(context.FormKey); form != nil {
|
|
||||||
page.Form = form.(*forgotPasswordForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.RenderPage(ctx, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *forgotPassword) Post(ctx echo.Context) error {
|
|
||||||
var form forgotPasswordForm
|
|
||||||
ctx.Set(context.FormKey, &form)
|
|
||||||
|
|
||||||
succeed := func() error {
|
|
||||||
ctx.Set(context.FormKey, nil)
|
|
||||||
msg.Success(ctx, "An email containing a link to reset your password will be sent to this address if it exists in our system.")
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the form values
|
|
||||||
if err := ctx.Bind(&form); err != nil {
|
|
||||||
return c.Fail(err, "unable to parse forgot password form")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := form.Submission.Process(ctx, form); err != nil {
|
|
||||||
return c.Fail(err, "unable to process form submission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Submission.HasErrors() {
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to load the user
|
|
||||||
u, err := c.Container.ORM.User.
|
|
||||||
Query().
|
|
||||||
Where(user.Email(strings.ToLower(form.Email))).
|
|
||||||
Only(ctx.Request().Context())
|
|
||||||
|
|
||||||
switch err.(type) {
|
|
||||||
case *ent.NotFoundError:
|
|
||||||
return succeed()
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
return c.Fail(err, "error querying user during forgot password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the token
|
|
||||||
token, pt, err := c.Container.Auth.GeneratePasswordResetToken(ctx, u.ID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "error generating password reset token")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
|
||||||
|
|
||||||
// Email the user
|
|
||||||
url := ctx.Echo().Reverse(routeNameResetPassword, u.ID, pt.ID, token)
|
|
||||||
err = c.Container.Mail.
|
|
||||||
Compose().
|
|
||||||
To(u.Email).
|
|
||||||
Subject("Reset your password").
|
|
||||||
Body(fmt.Sprintf("Go here to reset your password: %s", url)).
|
|
||||||
Send(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "error sending password reset email")
|
|
||||||
}
|
|
||||||
|
|
||||||
return succeed()
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
home struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
post struct {
|
|
||||||
Title string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *home) Get(ctx echo.Context) error {
|
|
||||||
page := controller.NewPage(ctx)
|
|
||||||
page.Layout = templates.LayoutMain
|
|
||||||
page.Name = templates.PageHome
|
|
||||||
page.Metatags.Description = "Welcome to the homepage."
|
|
||||||
page.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
|
|
||||||
page.Pager = controller.NewPager(ctx, 4)
|
|
||||||
page.Data = c.fetchPosts(&page.Pager)
|
|
||||||
|
|
||||||
return c.RenderPage(ctx, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchPosts is an mock example of fetching posts to illustrate how paging works
|
|
||||||
func (c *home) fetchPosts(pager *controller.Pager) []post {
|
|
||||||
pager.SetItems(20)
|
|
||||||
posts := make([]post, 20)
|
|
||||||
|
|
||||||
for k := range posts {
|
|
||||||
posts[k] = post{
|
|
||||||
Title: fmt.Sprintf("Post example #%d", k+1),
|
|
||||||
Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
|
||||||
"github.com/mikestefanello/pagoda/ent/user"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
login struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
loginForm struct {
|
|
||||||
Email string `form:"email" validate:"required,email"`
|
|
||||||
Password string `form:"password" validate:"required"`
|
|
||||||
Submission controller.FormSubmission
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *login) Get(ctx echo.Context) error {
|
|
||||||
page := controller.NewPage(ctx)
|
|
||||||
page.Layout = templates.LayoutAuth
|
|
||||||
page.Name = templates.PageLogin
|
|
||||||
page.Title = "Log in"
|
|
||||||
page.Form = loginForm{}
|
|
||||||
|
|
||||||
if form := ctx.Get(context.FormKey); form != nil {
|
|
||||||
page.Form = form.(*loginForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.RenderPage(ctx, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *login) Post(ctx echo.Context) error {
|
|
||||||
var form loginForm
|
|
||||||
ctx.Set(context.FormKey, &form)
|
|
||||||
|
|
||||||
authFailed := func() error {
|
|
||||||
form.Submission.SetFieldError("Email", "")
|
|
||||||
form.Submission.SetFieldError("Password", "")
|
|
||||||
msg.Danger(ctx, "Invalid credentials. Please try again.")
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the form values
|
|
||||||
if err := ctx.Bind(&form); err != nil {
|
|
||||||
return c.Fail(err, "unable to parse login form")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := form.Submission.Process(ctx, form); err != nil {
|
|
||||||
return c.Fail(err, "unable to process form submission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Submission.HasErrors() {
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to load the user
|
|
||||||
u, err := c.Container.ORM.User.
|
|
||||||
Query().
|
|
||||||
Where(user.Email(strings.ToLower(form.Email))).
|
|
||||||
Only(ctx.Request().Context())
|
|
||||||
|
|
||||||
switch err.(type) {
|
|
||||||
case *ent.NotFoundError:
|
|
||||||
return authFailed()
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
return c.Fail(err, "error querying user during login")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the password is correct
|
|
||||||
err = c.Container.Auth.CheckPassword(form.Password, u.Password)
|
|
||||||
if err != nil {
|
|
||||||
return authFailed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the user in
|
|
||||||
err = c.Container.Auth.Login(ctx, u.ID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "unable to log in user")
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Success(ctx, fmt.Sprintf("Welcome back, <strong>%s</strong>. You are now logged in.", u.Name))
|
|
||||||
return c.Redirect(ctx, routeNameHome)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logout struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logout) Get(c echo.Context) error {
|
|
||||||
if err := l.Container.Auth.Logout(c); err == nil {
|
|
||||||
msg.Success(c, "You have been logged out successfully.")
|
|
||||||
} else {
|
|
||||||
msg.Danger(c, "An error occurred. Please try again.")
|
|
||||||
}
|
|
||||||
return l.Redirect(c, routeNameHome)
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
register struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
registerForm struct {
|
|
||||||
Name string `form:"name" validate:"required"`
|
|
||||||
Email string `form:"email" validate:"required,email"`
|
|
||||||
Password string `form:"password" validate:"required"`
|
|
||||||
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
|
|
||||||
Submission controller.FormSubmission
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *register) Get(ctx echo.Context) error {
|
|
||||||
page := controller.NewPage(ctx)
|
|
||||||
page.Layout = templates.LayoutAuth
|
|
||||||
page.Name = templates.PageRegister
|
|
||||||
page.Title = "Register"
|
|
||||||
page.Form = registerForm{}
|
|
||||||
|
|
||||||
if form := ctx.Get(context.FormKey); form != nil {
|
|
||||||
page.Form = form.(*registerForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.RenderPage(ctx, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *register) Post(ctx echo.Context) error {
|
|
||||||
var form registerForm
|
|
||||||
ctx.Set(context.FormKey, &form)
|
|
||||||
|
|
||||||
// Parse the form values
|
|
||||||
if err := ctx.Bind(&form); err != nil {
|
|
||||||
return c.Fail(err, "unable to parse register form")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := form.Submission.Process(ctx, form); err != nil {
|
|
||||||
return c.Fail(err, "unable to process form submission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Submission.HasErrors() {
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password
|
|
||||||
pwHash, err := c.Container.Auth.HashPassword(form.Password)
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "unable to hash password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt creating the user
|
|
||||||
u, err := c.Container.ORM.User.
|
|
||||||
Create().
|
|
||||||
SetName(form.Name).
|
|
||||||
SetEmail(form.Email).
|
|
||||||
SetPassword(pwHash).
|
|
||||||
Save(ctx.Request().Context())
|
|
||||||
|
|
||||||
switch err.(type) {
|
|
||||||
case nil:
|
|
||||||
ctx.Logger().Infof("user created: %s", u.Name)
|
|
||||||
case *ent.ConstraintError:
|
|
||||||
msg.Warning(ctx, "A user with this email address already exists. Please log in.")
|
|
||||||
return c.Redirect(ctx, routeNameLogin)
|
|
||||||
default:
|
|
||||||
return c.Fail(err, "unable to create user")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the user in
|
|
||||||
err = c.Container.Auth.Login(ctx, u.ID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Errorf("unable to log in: %v", err)
|
|
||||||
msg.Info(ctx, "Your account has been created.")
|
|
||||||
return c.Redirect(ctx, routeNameLogin)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Success(ctx, "Your account has been created. You are now logged in.")
|
|
||||||
|
|
||||||
// Send the verification email
|
|
||||||
c.sendVerificationEmail(ctx, u)
|
|
||||||
|
|
||||||
return c.Redirect(ctx, routeNameHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
|
|
||||||
// Generate a token
|
|
||||||
token, err := c.Container.Auth.GenerateEmailVerificationToken(usr.Email)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Errorf("unable to generate email verification token: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the email
|
|
||||||
url := ctx.Echo().Reverse(routeNameVerifyEmail, token)
|
|
||||||
err = c.Container.Mail.
|
|
||||||
Compose().
|
|
||||||
To(usr.Email).
|
|
||||||
Subject("Confirm your email address").
|
|
||||||
Body(fmt.Sprintf("Click here to confirm your email address: %s", url)).
|
|
||||||
Send(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Errorf("unable to send email verification link: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Info(ctx, "An email was sent to you to verify your email address.")
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
resetPassword struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPasswordForm struct {
|
|
||||||
Password string `form:"password" validate:"required"`
|
|
||||||
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
|
|
||||||
Submission controller.FormSubmission
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *resetPassword) Get(ctx echo.Context) error {
|
|
||||||
page := controller.NewPage(ctx)
|
|
||||||
page.Layout = templates.LayoutAuth
|
|
||||||
page.Name = templates.PageResetPassword
|
|
||||||
page.Title = "Reset password"
|
|
||||||
page.Form = resetPasswordForm{}
|
|
||||||
|
|
||||||
if form := ctx.Get(context.FormKey); form != nil {
|
|
||||||
page.Form = form.(*resetPasswordForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.RenderPage(ctx, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *resetPassword) Post(ctx echo.Context) error {
|
|
||||||
var form resetPasswordForm
|
|
||||||
ctx.Set(context.FormKey, &form)
|
|
||||||
|
|
||||||
// Parse the form values
|
|
||||||
if err := ctx.Bind(&form); err != nil {
|
|
||||||
return c.Fail(err, "unable to parse password reset form")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := form.Submission.Process(ctx, form); err != nil {
|
|
||||||
return c.Fail(err, "unable to process form submission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Submission.HasErrors() {
|
|
||||||
return c.Get(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the new password
|
|
||||||
hash, err := c.Container.Auth.HashPassword(form.Password)
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "unable to hash password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the requesting user
|
|
||||||
usr := ctx.Get(context.UserKey).(*ent.User)
|
|
||||||
|
|
||||||
// Update the user
|
|
||||||
_, err = usr.
|
|
||||||
Update().
|
|
||||||
SetPassword(hash).
|
|
||||||
Save(ctx.Request().Context())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "unable to update password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all password tokens for this user
|
|
||||||
err = c.Container.Auth.DeletePasswordTokens(ctx, usr.ID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "unable to delete password tokens")
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Success(ctx, "Your password has been updated.")
|
|
||||||
return c.Redirect(ctx, routeNameLogin)
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/config"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/middleware"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/services"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/labstack/echo-contrib/session"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
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
|
|
||||||
func BuildRouter(c *services.Container) {
|
|
||||||
// Static files with proper cache control
|
|
||||||
// funcmap.File() should be used in templates to append a cache key to the URL in order to break cache
|
|
||||||
// after each server restart
|
|
||||||
c.Web.Group("", middleware.CacheControl(c.Config.Cache.Expiration.StaticFile)).
|
|
||||||
Static(config.StaticPrefix, config.StaticDir)
|
|
||||||
|
|
||||||
// Non static file route group
|
|
||||||
g := c.Web.Group("")
|
|
||||||
|
|
||||||
// Force HTTPS, if enabled
|
|
||||||
if c.Config.HTTP.TLS.Enabled {
|
|
||||||
g.Use(echomw.HTTPSRedirect())
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Use(
|
|
||||||
echomw.RemoveTrailingSlashWithConfig(echomw.TrailingSlashConfig{
|
|
||||||
RedirectCode: http.StatusMovedPermanently,
|
|
||||||
}),
|
|
||||||
echomw.Recover(),
|
|
||||||
echomw.Secure(),
|
|
||||||
echomw.RequestID(),
|
|
||||||
echomw.Gzip(),
|
|
||||||
echomw.Logger(),
|
|
||||||
middleware.LogRequestID(),
|
|
||||||
echomw.TimeoutWithConfig(echomw.TimeoutConfig{
|
|
||||||
Timeout: c.Config.App.Timeout,
|
|
||||||
}),
|
|
||||||
session.Middleware(sessions.NewCookieStore([]byte(c.Config.App.EncryptionKey))),
|
|
||||||
middleware.LoadAuthenticatedUser(c.Auth),
|
|
||||||
middleware.ServeCachedPage(c.Cache),
|
|
||||||
echomw.CSRFWithConfig(echomw.CSRFConfig{
|
|
||||||
TokenLookup: "form:csrf",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Base controller
|
|
||||||
ctr := controller.NewController(c)
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
err := errorHandler{Controller: ctr}
|
|
||||||
c.Web.HTTPErrorHandler = err.Get
|
|
||||||
|
|
||||||
// Example routes
|
|
||||||
navRoutes(c, g, ctr)
|
|
||||||
userRoutes(c, g, ctr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func navRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
|
||||||
home := home{Controller: ctr}
|
|
||||||
g.GET("/", home.Get).Name = routeNameHome
|
|
||||||
|
|
||||||
search := search{Controller: ctr}
|
|
||||||
g.GET("/search", search.Get).Name = routeNameSearch
|
|
||||||
|
|
||||||
about := about{Controller: ctr}
|
|
||||||
g.GET("/about", about.Get).Name = routeNameAbout
|
|
||||||
|
|
||||||
contact := contact{Controller: ctr}
|
|
||||||
g.GET("/contact", contact.Get).Name = routeNameContact
|
|
||||||
g.POST("/contact", contact.Post).Name = routeNameContactSubmit
|
|
||||||
}
|
|
||||||
|
|
||||||
func userRoutes(c *services.Container, g *echo.Group, ctr controller.Controller) {
|
|
||||||
logout := logout{Controller: ctr}
|
|
||||||
g.GET("/logout", logout.Get, middleware.RequireAuthentication()).Name = routeNameLogout
|
|
||||||
|
|
||||||
verifyEmail := verifyEmail{Controller: ctr}
|
|
||||||
g.GET("/email/verify/:token", verifyEmail.Get).Name = routeNameVerifyEmail
|
|
||||||
|
|
||||||
noAuth := g.Group("/user", middleware.RequireNoAuthentication())
|
|
||||||
login := login{Controller: ctr}
|
|
||||||
noAuth.GET("/login", login.Get).Name = routeNameLogin
|
|
||||||
noAuth.POST("/login", login.Post).Name = routeNameLoginSubmit
|
|
||||||
|
|
||||||
register := register{Controller: ctr}
|
|
||||||
noAuth.GET("/register", register.Get).Name = routeNameRegister
|
|
||||||
noAuth.POST("/register", register.Post).Name = routeNameRegisterSubmit
|
|
||||||
|
|
||||||
forgot := forgotPassword{Controller: ctr}
|
|
||||||
noAuth.GET("/password", forgot.Get).Name = routeNameForgotPassword
|
|
||||||
noAuth.POST("/password", forgot.Post).Name = routeNameForgotPasswordSubmit
|
|
||||||
|
|
||||||
resetGroup := noAuth.Group("/password/reset",
|
|
||||||
middleware.LoadUser(c.ORM),
|
|
||||||
middleware.LoadValidPasswordToken(c.Auth),
|
|
||||||
)
|
|
||||||
reset := resetPassword{Controller: ctr}
|
|
||||||
resetGroup.GET("/token/:user/:password_token/:token", reset.Get).Name = routeNameResetPassword
|
|
||||||
resetGroup.POST("/token/:user/:password_token/:token", reset.Post).Name = routeNameResetPasswordSubmit
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
|
||||||
"github.com/mikestefanello/pagoda/ent/user"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/controller"
|
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
|
||||||
)
|
|
||||||
|
|
||||||
type verifyEmail struct {
|
|
||||||
controller.Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *verifyEmail) Get(ctx echo.Context) error {
|
|
||||||
var usr *ent.User
|
|
||||||
|
|
||||||
// Validate the token
|
|
||||||
token := ctx.Param("token")
|
|
||||||
email, err := c.Container.Auth.ValidateEmailVerificationToken(token)
|
|
||||||
if err != nil {
|
|
||||||
msg.Warning(ctx, "The link is either invalid or has expired.")
|
|
||||||
return c.Redirect(ctx, routeNameHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it matches the authenticated user
|
|
||||||
if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
|
|
||||||
authUser := u.(*ent.User)
|
|
||||||
|
|
||||||
if authUser.Email == email {
|
|
||||||
usr = authUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query to find a matching user, if needed
|
|
||||||
if usr == nil {
|
|
||||||
usr, err = c.Container.ORM.User.
|
|
||||||
Query().
|
|
||||||
Where(user.Email(email)).
|
|
||||||
Only(ctx.Request().Context())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "query failed loading email verification token user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the user, if needed
|
|
||||||
if !usr.Verified {
|
|
||||||
usr, err = usr.
|
|
||||||
Update().
|
|
||||||
SetVerified(true).
|
|
||||||
Save(ctx.Request().Context())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return c.Fail(err, "failed to set user as verified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Success(ctx, "Your email has been successfully verified.")
|
|
||||||
return c.Redirect(ctx, routeNameHome)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user