Added user email verification support.

This commit is contained in:
mikestefanello 2022-01-08 15:32:18 -05:00
parent feb11bbe5b
commit ea46a38f68
31 changed files with 417 additions and 85 deletions

View file

@ -40,6 +40,7 @@
* [Registration](#registration) * [Registration](#registration)
* [Authenticated user](#authenticated-user) * [Authenticated user](#authenticated-user)
* [Middleware](#middleware) * [Middleware](#middleware)
* [Email verification](#email-verification)
* [Routes](#routes) * [Routes](#routes)
* [Custom middleware](#custom-middleware) * [Custom middleware](#custom-middleware)
* [Controller / Dependencies](#controller--dependencies) * [Controller / Dependencies](#controller--dependencies)
@ -342,6 +343,20 @@ Registered for all routes is middleware that will load the currently logged in u
If you wish to require either authentication or non-authentication for a given route, you can use either `middleware.RequireAuthentication()` or `middleware.RequireNoAuthentication()`. If you wish to require either authentication or non-authentication for a given route, you can use either `middleware.RequireAuthentication()` or `middleware.RequireNoAuthentication()`.
### Email verification
Most web applications require the user to verify their email address (or other form of contact information). The `User` entity has a field `Verified` to indicate if they have verified themself. When a user successfully registers, an email is sent to them containing a link with a token that will verify their account when visited. This route is currently accessible at `/email/verify/:token` and handled by `routes/VerifyEmail`.
There is currently no enforcement that a `User` must be verified in order to access the application. If that is something you desire, it will have to be added in yourself. It was not included because you may want partial access of certain features until the user verifies; or no access at all.
Verification tokens are [JSON Web Tokens](https://jwt.io/) generated and processed by the [jwt](https://github.com/golang-jwt/jwt) module. The tokens are _signed_ using the encryption key stored in [configuration](#configuration) (`Config.App.EncryptionKey`). **It is imperative** that you override this value from the default in any live environments otherwise the data can be comprimised. JWT was chosen because they are secure tokens that do not have to be stored in the database, since the tokens contain all of the data required, including built-in expirations. These were not chosen for password reset tokens because JWT cannot be withdrawn once they are issued which poses a security risk. Since these tokens do not grant access to an account, the ability to withdraw the tokens is not needed.
By default, verification tokens expire 12 hours after they are issued. This can be changed in configuration at `Config.App.EmailVerificationTokenExpiration`. There is currently not a route or form provided to request a new link.
Be sure to review the [email](#email) section since actual email sending is not fully implemented.
To generate a new verification token, the `AuthClient` has a method `GenerateEmailVerificationToken()` which creates a token for a given email address. To verify the token, pass it in to `ValidateEmailVerificationToken()` which will return the email address associated with the token and an error if the token is invalid.
## Routes ## Routes
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `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 `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.
@ -920,7 +935,6 @@ By default, Echo's [request ID middleware](https://echo.labstack.com/middleware/
Future work includes but is not limited to: Future work includes but is not limited to:
- Email verification
- Flexible pager templates - Flexible pager templates
- Expanded HTMX examples and integration - Expanded HTMX examples and integration
- Admin section - Admin section
@ -929,21 +943,22 @@ Future work includes but is not limited to:
Thank you to all of the following amazing projects for making this possible. Thank you to all of the following amazing projects for making this possible.
- [go](https://go.dev/)
- [echo](https://github.com/labstack/echo)
- [ent](https://github.com/ent/ent)
- [sprig](https://github.com/Masterminds/sprig)
- [goquery](https://github.com/PuerkitoBio/goquery)
- [validator](https://github.com/go-playground/validator)
- [go-redis](https://github.com/go-redis/redis)
- [gocache](https://github.com/eko/gocache)
- [sessions](https://github.com/gorilla/sessions)
- [pgx](https://github.com/jackc/pgx)
- [envdecode](https://github.com/joeshaw/envdecode)
- [testify](https://github.com/stretchr/testify)
- [htmx](https://github.com/bigskysoftware/htmx)
- [alpinejs](https://github.com/alpinejs/alpine) - [alpinejs](https://github.com/alpinejs/alpine)
- [bulma](https://github.com/jgthms/bulma) - [bulma](https://github.com/jgthms/bulma)
- [docker](https://www.docker.com/) - [docker](https://www.docker.com/)
- [echo](https://github.com/labstack/echo)
- [ent](https://github.com/ent/ent)
- [envdecode](https://github.com/joeshaw/envdecode)
- [go](https://go.dev/)
- [gocache](https://github.com/eko/gocache)
- [goquery](https://github.com/PuerkitoBio/goquery)
- [go-redis](https://github.com/go-redis/redis)
- [htmx](https://github.com/bigskysoftware/htmx)
- [jwt](https://github.com/golang-jwt/jwt)
- [pgx](https://github.com/jackc/pgx)
- [postgresql](https://www.postgresql.org/) - [postgresql](https://www.postgresql.org/)
- [redis](https://redis.io/) - [redis](https://redis.io/)
- [sprig](https://github.com/Masterminds/sprig)
- [sessions](https://github.com/gorilla/sessions)
- [testify](https://github.com/stretchr/testify)
- [validator](https://github.com/go-playground/validator)

View file

@ -67,14 +67,16 @@ type (
// AppConfig stores application configuration // AppConfig stores application configuration
AppConfig struct { AppConfig struct {
Name string `env:"APP_NAME,default=Pagoda"` Name string `env:"APP_NAME,default=Pagoda"`
Environment Environment `env:"APP_ENVIRONMENT,default=local"` Environment Environment `env:"APP_ENVIRONMENT,default=local"`
// THIS MUST BE OVERRIDDEN ON ANY LIVE ENVIRONMENTS
EncryptionKey string `env:"APP_ENCRYPTION_KEY,default=?E(G+KbPeShVmYq3t6w9z$C&F)J@McQf"` EncryptionKey string `env:"APP_ENCRYPTION_KEY,default=?E(G+KbPeShVmYq3t6w9z$C&F)J@McQf"`
Timeout time.Duration `env:"APP_TIMEOUT,default=20s"` Timeout time.Duration `env:"APP_TIMEOUT,default=20s"`
PasswordToken struct { PasswordToken struct {
Expiration time.Duration `env:"APP_PASSWORD_TOKEN_EXPIRATION,default=60m"` Expiration time.Duration `env:"APP_PASSWORD_TOKEN_EXPIRATION,default=60m"`
Length int `env:"APP_PASSWORD_TOKEN_LENGTH,default=64"` Length int `env:"APP_PASSWORD_TOKEN_LENGTH,default=64"`
} }
EmailVerificationTokenExpiration time.Duration `env:"APP_EMAIL_VERIFICATION_TOKEN_EXPIRATION,default=12h"`
} }
// CacheConfig stores the cache configuration // CacheConfig stores the cache configuration

View file

@ -6,11 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent" "entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
) )
// ent aliases to avoid import conflicts in user's code. // ent aliases to avoid import conflicts in user's code.

View file

@ -6,7 +6,6 @@ import (
"context" "context"
"github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent"
// required by schema hooks. // required by schema hooks.
_ "github.com/mikestefanello/pagoda/ent/runtime" _ "github.com/mikestefanello/pagoda/ent/runtime"

View file

@ -35,6 +35,7 @@ var (
{Name: "name", Type: field.TypeString}, {Name: "name", Type: field.TypeString},
{Name: "email", Type: field.TypeString, Unique: true}, {Name: "email", Type: field.TypeString, Unique: true},
{Name: "password", Type: field.TypeString}, {Name: "password", Type: field.TypeString},
{Name: "verified", Type: field.TypeBool, Default: false},
{Name: "created_at", Type: field.TypeTime}, {Name: "created_at", Type: field.TypeTime},
} }
// UsersTable holds the schema information for the "users" table. // UsersTable holds the schema information for the "users" table.

View file

@ -452,6 +452,7 @@ type UserMutation struct {
name *string name *string
email *string email *string
password *string password *string
verified *bool
created_at *time.Time created_at *time.Time
clearedFields map[string]struct{} clearedFields map[string]struct{}
owner map[int]struct{} owner map[int]struct{}
@ -649,6 +650,42 @@ func (m *UserMutation) ResetPassword() {
m.password = nil m.password = nil
} }
// SetVerified sets the "verified" field.
func (m *UserMutation) SetVerified(b bool) {
m.verified = &b
}
// Verified returns the value of the "verified" field in the mutation.
func (m *UserMutation) Verified() (r bool, exists bool) {
v := m.verified
if v == nil {
return
}
return *v, true
}
// OldVerified returns the old "verified" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldVerified(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldVerified is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldVerified requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldVerified: %w", err)
}
return oldValue.Verified, nil
}
// ResetVerified resets all changes to the "verified" field.
func (m *UserMutation) ResetVerified() {
m.verified = nil
}
// SetCreatedAt sets the "created_at" field. // SetCreatedAt sets the "created_at" field.
func (m *UserMutation) SetCreatedAt(t time.Time) { func (m *UserMutation) SetCreatedAt(t time.Time) {
m.created_at = &t m.created_at = &t
@ -758,7 +795,7 @@ func (m *UserMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call // order to get all numeric fields that were incremented/decremented, call
// AddedFields(). // AddedFields().
func (m *UserMutation) Fields() []string { func (m *UserMutation) Fields() []string {
fields := make([]string, 0, 4) fields := make([]string, 0, 5)
if m.name != nil { if m.name != nil {
fields = append(fields, user.FieldName) fields = append(fields, user.FieldName)
} }
@ -768,6 +805,9 @@ func (m *UserMutation) Fields() []string {
if m.password != nil { if m.password != nil {
fields = append(fields, user.FieldPassword) fields = append(fields, user.FieldPassword)
} }
if m.verified != nil {
fields = append(fields, user.FieldVerified)
}
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, user.FieldCreatedAt) fields = append(fields, user.FieldCreatedAt)
} }
@ -785,6 +825,8 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
return m.Email() return m.Email()
case user.FieldPassword: case user.FieldPassword:
return m.Password() return m.Password()
case user.FieldVerified:
return m.Verified()
case user.FieldCreatedAt: case user.FieldCreatedAt:
return m.CreatedAt() return m.CreatedAt()
} }
@ -802,6 +844,8 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldEmail(ctx) return m.OldEmail(ctx)
case user.FieldPassword: case user.FieldPassword:
return m.OldPassword(ctx) return m.OldPassword(ctx)
case user.FieldVerified:
return m.OldVerified(ctx)
case user.FieldCreatedAt: case user.FieldCreatedAt:
return m.OldCreatedAt(ctx) return m.OldCreatedAt(ctx)
} }
@ -834,6 +878,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
} }
m.SetPassword(v) m.SetPassword(v)
return nil return nil
case user.FieldVerified:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetVerified(v)
return nil
case user.FieldCreatedAt: case user.FieldCreatedAt:
v, ok := value.(time.Time) v, ok := value.(time.Time)
if !ok { if !ok {
@ -899,6 +950,9 @@ func (m *UserMutation) ResetField(name string) error {
case user.FieldPassword: case user.FieldPassword:
m.ResetPassword() m.ResetPassword()
return nil return nil
case user.FieldVerified:
m.ResetVerified()
return nil
case user.FieldCreatedAt: case user.FieldCreatedAt:
m.ResetCreatedAt() m.ResetCreatedAt()
return nil return nil

View file

@ -7,10 +7,9 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent/dialect/sql"
"github.com/mikestefanello/pagoda/ent/passwordtoken" "github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user" "github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql"
) )
// PasswordToken is the model entity for the PasswordToken schema. // PasswordToken is the model entity for the PasswordToken schema.

View file

@ -5,10 +5,9 @@ package passwordtoken
import ( import (
"time" "time"
"github.com/mikestefanello/pagoda/ent/predicate"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"github.com/mikestefanello/pagoda/ent/predicate"
) )
// ID filters vertices based on their ID field. // ID filters vertices based on their ID field.

View file

@ -8,11 +8,10 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
) )
// PasswordTokenCreate is the builder for creating a PasswordToken entity. // PasswordTokenCreate is the builder for creating a PasswordToken entity.

View file

@ -6,12 +6,11 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
) )
// PasswordTokenDelete is the builder for deleting a PasswordToken entity. // PasswordTokenDelete is the builder for deleting a PasswordToken entity.

View file

@ -8,13 +8,12 @@ import (
"fmt" "fmt"
"math" "math"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
) )
// PasswordTokenQuery is the builder for querying PasswordToken entities. // PasswordTokenQuery is the builder for querying PasswordToken entities.

View file

@ -8,13 +8,12 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
) )
// PasswordTokenUpdate is the builder for updating PasswordToken entities. // PasswordTokenUpdate is the builder for updating PasswordToken entities.

View file

@ -2,4 +2,4 @@
package ent package ent
// The schema-stitching logic is generated in goweb/ent/runtime/runtime.go // The schema-stitching logic is generated in github.com/mikestefanello/pagoda/ent/runtime/runtime.go

View file

@ -40,8 +40,12 @@ func init() {
userDescPassword := userFields[2].Descriptor() userDescPassword := userFields[2].Descriptor()
// user.PasswordValidator is a validator for the "password" field. It is called by the builders before save. // user.PasswordValidator is a validator for the "password" field. It is called by the builders before save.
user.PasswordValidator = userDescPassword.Validators[0].(func(string) error) user.PasswordValidator = userDescPassword.Validators[0].(func(string) error)
// userDescVerified is the schema descriptor for verified field.
userDescVerified := userFields[3].Descriptor()
// user.DefaultVerified holds the default value on creation for the verified field.
user.DefaultVerified = userDescVerified.Default.(bool)
// userDescCreatedAt is the schema descriptor for created_at field. // userDescCreatedAt is the schema descriptor for created_at field.
userDescCreatedAt := userFields[3].Descriptor() userDescCreatedAt := userFields[4].Descriptor()
// user.DefaultCreatedAt holds the default value on creation for the created_at field. // user.DefaultCreatedAt holds the default value on creation for the created_at field.
user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time) user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time)
} }

View file

@ -29,6 +29,8 @@ func (User) Fields() []ent.Field {
field.String("password"). field.String("password").
Sensitive(). Sensitive().
NotEmpty(), NotEmpty(),
field.Bool("verified").
Default(false),
field.Time("created_at"). field.Time("created_at").
Default(time.Now). Default(time.Now).
Immutable(), Immutable(),

View file

@ -7,9 +7,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/mikestefanello/pagoda/ent/user"
) )
// User is the model entity for the User schema. // User is the model entity for the User schema.
@ -23,6 +22,8 @@ type User struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
// Password holds the value of the "password" field. // Password holds the value of the "password" field.
Password string `json:"-"` Password string `json:"-"`
// Verified holds the value of the "verified" field.
Verified bool `json:"verified,omitempty"`
// CreatedAt holds the value of the "created_at" field. // CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
@ -53,6 +54,8 @@ func (*User) scanValues(columns []string) ([]interface{}, error) {
values := make([]interface{}, len(columns)) values := make([]interface{}, len(columns))
for i := range columns { for i := range columns {
switch columns[i] { switch columns[i] {
case user.FieldVerified:
values[i] = new(sql.NullBool)
case user.FieldID: case user.FieldID:
values[i] = new(sql.NullInt64) values[i] = new(sql.NullInt64)
case user.FieldName, user.FieldEmail, user.FieldPassword: case user.FieldName, user.FieldEmail, user.FieldPassword:
@ -98,6 +101,12 @@ func (u *User) assignValues(columns []string, values []interface{}) error {
} else if value.Valid { } else if value.Valid {
u.Password = value.String u.Password = value.String
} }
case user.FieldVerified:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field verified", values[i])
} else if value.Valid {
u.Verified = value.Bool
}
case user.FieldCreatedAt: case user.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok { if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i]) return fmt.Errorf("unexpected type %T for field created_at", values[i])
@ -142,6 +151,8 @@ func (u *User) String() string {
builder.WriteString(", email=") builder.WriteString(", email=")
builder.WriteString(u.Email) builder.WriteString(u.Email)
builder.WriteString(", password=<sensitive>") builder.WriteString(", password=<sensitive>")
builder.WriteString(", verified=")
builder.WriteString(fmt.Sprintf("%v", u.Verified))
builder.WriteString(", created_at=") builder.WriteString(", created_at=")
builder.WriteString(u.CreatedAt.Format(time.ANSIC)) builder.WriteString(u.CreatedAt.Format(time.ANSIC))
builder.WriteByte(')') builder.WriteByte(')')

View file

@ -19,6 +19,8 @@ const (
FieldEmail = "email" FieldEmail = "email"
// FieldPassword holds the string denoting the password field in the database. // FieldPassword holds the string denoting the password field in the database.
FieldPassword = "password" FieldPassword = "password"
// FieldVerified holds the string denoting the verified field in the database.
FieldVerified = "verified"
// FieldCreatedAt holds the string denoting the created_at field in the database. // FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at" FieldCreatedAt = "created_at"
// EdgeOwner holds the string denoting the owner edge name in mutations. // EdgeOwner holds the string denoting the owner edge name in mutations.
@ -40,6 +42,7 @@ var Columns = []string{
FieldName, FieldName,
FieldEmail, FieldEmail,
FieldPassword, FieldPassword,
FieldVerified,
FieldCreatedAt, FieldCreatedAt,
} }
@ -57,7 +60,7 @@ func ValidColumn(column string) bool {
// package on the initialization of the application. Therefore, // package on the initialization of the application. Therefore,
// it should be imported in the main as follows: // it should be imported in the main as follows:
// //
// import _ "goweb/ent/runtime" // import _ "github.com/mikestefanello/pagoda/ent/runtime"
// //
var ( var (
Hooks [1]ent.Hook Hooks [1]ent.Hook
@ -67,6 +70,8 @@ var (
EmailValidator func(string) error EmailValidator func(string) error
// PasswordValidator is a validator for the "password" field. It is called by the builders before save. // PasswordValidator is a validator for the "password" field. It is called by the builders before save.
PasswordValidator func(string) error PasswordValidator func(string) error
// DefaultVerified holds the default value on creation for the "verified" field.
DefaultVerified bool
// DefaultCreatedAt holds the default value on creation for the "created_at" field. // DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time DefaultCreatedAt func() time.Time
) )

View file

@ -5,10 +5,9 @@ package user
import ( import (
"time" "time"
"github.com/mikestefanello/pagoda/ent/predicate"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"github.com/mikestefanello/pagoda/ent/predicate"
) )
// ID filters vertices based on their ID field. // ID filters vertices based on their ID field.
@ -115,6 +114,13 @@ func Password(v string) predicate.User {
}) })
} }
// Verified applies equality check predicate on the "verified" field. It's identical to VerifiedEQ.
func Verified(v bool) predicate.User {
return predicate.User(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldVerified), v))
})
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.User { func CreatedAt(v time.Time) predicate.User {
return predicate.User(func(s *sql.Selector) { return predicate.User(func(s *sql.Selector) {
@ -455,6 +461,20 @@ func PasswordContainsFold(v string) predicate.User {
}) })
} }
// VerifiedEQ applies the EQ predicate on the "verified" field.
func VerifiedEQ(v bool) predicate.User {
return predicate.User(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldVerified), v))
})
}
// VerifiedNEQ applies the NEQ predicate on the "verified" field.
func VerifiedNEQ(v bool) predicate.User {
return predicate.User(func(s *sql.Selector) {
s.Where(sql.NEQ(s.C(FieldVerified), v))
})
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field. // CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.User { func CreatedAtEQ(v time.Time) predicate.User {
return predicate.User(func(s *sql.Selector) { return predicate.User(func(s *sql.Selector) {

View file

@ -8,11 +8,10 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
) )
// UserCreate is the builder for creating a User entity. // UserCreate is the builder for creating a User entity.
@ -40,6 +39,20 @@ func (uc *UserCreate) SetPassword(s string) *UserCreate {
return uc return uc
} }
// SetVerified sets the "verified" field.
func (uc *UserCreate) SetVerified(b bool) *UserCreate {
uc.mutation.SetVerified(b)
return uc
}
// SetNillableVerified sets the "verified" field if the given value is not nil.
func (uc *UserCreate) SetNillableVerified(b *bool) *UserCreate {
if b != nil {
uc.SetVerified(*b)
}
return uc
}
// SetCreatedAt sets the "created_at" field. // SetCreatedAt sets the "created_at" field.
func (uc *UserCreate) SetCreatedAt(t time.Time) *UserCreate { func (uc *UserCreate) SetCreatedAt(t time.Time) *UserCreate {
uc.mutation.SetCreatedAt(t) uc.mutation.SetCreatedAt(t)
@ -142,6 +155,10 @@ func (uc *UserCreate) ExecX(ctx context.Context) {
// defaults sets the default values of the builder before save. // defaults sets the default values of the builder before save.
func (uc *UserCreate) defaults() error { func (uc *UserCreate) defaults() error {
if _, ok := uc.mutation.Verified(); !ok {
v := user.DefaultVerified
uc.mutation.SetVerified(v)
}
if _, ok := uc.mutation.CreatedAt(); !ok { if _, ok := uc.mutation.CreatedAt(); !ok {
if user.DefaultCreatedAt == nil { if user.DefaultCreatedAt == nil {
return fmt.Errorf("ent: uninitialized user.DefaultCreatedAt (forgotten import ent/runtime?)") return fmt.Errorf("ent: uninitialized user.DefaultCreatedAt (forgotten import ent/runtime?)")
@ -178,6 +195,9 @@ func (uc *UserCreate) check() error {
return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "password": %w`, err)} return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "password": %w`, err)}
} }
} }
if _, ok := uc.mutation.Verified(); !ok {
return &ValidationError{Name: "verified", err: errors.New(`ent: missing required field "verified"`)}
}
if _, ok := uc.mutation.CreatedAt(); !ok { if _, ok := uc.mutation.CreatedAt(); !ok {
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "created_at"`)} return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "created_at"`)}
} }
@ -232,6 +252,14 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
}) })
_node.Password = value _node.Password = value
} }
if value, ok := uc.mutation.Verified(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: user.FieldVerified,
})
_node.Verified = value
}
if value, ok := uc.mutation.CreatedAt(); ok { if value, ok := uc.mutation.CreatedAt(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeTime, Type: field.TypeTime,

View file

@ -6,12 +6,11 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
) )
// UserDelete is the builder for deleting a User entity. // UserDelete is the builder for deleting a User entity.

View file

@ -9,13 +9,12 @@ import (
"fmt" "fmt"
"math" "math"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
) )
// UserQuery is the builder for querying User entities. // UserQuery is the builder for querying User entities.

View file

@ -6,13 +6,12 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/predicate"
"github.com/mikestefanello/pagoda/ent/user"
) )
// UserUpdate is the builder for updating User entities. // UserUpdate is the builder for updating User entities.
@ -46,6 +45,20 @@ func (uu *UserUpdate) SetPassword(s string) *UserUpdate {
return uu return uu
} }
// SetVerified sets the "verified" field.
func (uu *UserUpdate) SetVerified(b bool) *UserUpdate {
uu.mutation.SetVerified(b)
return uu
}
// SetNillableVerified sets the "verified" field if the given value is not nil.
func (uu *UserUpdate) SetNillableVerified(b *bool) *UserUpdate {
if b != nil {
uu.SetVerified(*b)
}
return uu
}
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs. // AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
func (uu *UserUpdate) AddOwnerIDs(ids ...int) *UserUpdate { func (uu *UserUpdate) AddOwnerIDs(ids ...int) *UserUpdate {
uu.mutation.AddOwnerIDs(ids...) uu.mutation.AddOwnerIDs(ids...)
@ -206,6 +219,13 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
Column: user.FieldPassword, Column: user.FieldPassword,
}) })
} }
if value, ok := uu.mutation.Verified(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: user.FieldVerified,
})
}
if uu.mutation.OwnerCleared() { if uu.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.O2M,
@ -297,6 +317,20 @@ func (uuo *UserUpdateOne) SetPassword(s string) *UserUpdateOne {
return uuo return uuo
} }
// SetVerified sets the "verified" field.
func (uuo *UserUpdateOne) SetVerified(b bool) *UserUpdateOne {
uuo.mutation.SetVerified(b)
return uuo
}
// SetNillableVerified sets the "verified" field if the given value is not nil.
func (uuo *UserUpdateOne) SetNillableVerified(b *bool) *UserUpdateOne {
if b != nil {
uuo.SetVerified(*b)
}
return uuo
}
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs. // AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
func (uuo *UserUpdateOne) AddOwnerIDs(ids ...int) *UserUpdateOne { func (uuo *UserUpdateOne) AddOwnerIDs(ids ...int) *UserUpdateOne {
uuo.mutation.AddOwnerIDs(ids...) uuo.mutation.AddOwnerIDs(ids...)
@ -481,6 +515,13 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
Column: user.FieldPassword, Column: user.FieldPassword,
}) })
} }
if value, ok := uuo.mutation.Verified(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: user.FieldVerified,
})
}
if uuo.mutation.OwnerCleared() { if uuo.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.O2M,

2
go.mod
View file

@ -10,6 +10,7 @@ require (
github.com/go-playground/assert/v2 v2.0.1 github.com/go-playground/assert/v2 v2.0.1
github.com/go-playground/validator/v10 v10.9.0 github.com/go-playground/validator/v10 v10.9.0
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.1
github.com/jackc/pgx/v4 v4.14.1 github.com/jackc/pgx/v4 v4.14.1
github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
@ -33,7 +34,6 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect

7
go.sum
View file

@ -150,7 +150,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
@ -404,7 +403,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@ -447,7 +445,6 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -565,13 +562,11 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@ -671,7 +666,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -818,7 +812,6 @@ golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,6 +1,8 @@
package routes package routes
import ( import (
"fmt"
"github.com/mikestefanello/pagoda/context" "github.com/mikestefanello/pagoda/context"
"github.com/mikestefanello/pagoda/controller" "github.com/mikestefanello/pagoda/controller"
"github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent"
@ -86,6 +88,31 @@ func (c *Register) Post(ctx echo.Context) error {
return c.Redirect(ctx, "login") return c.Redirect(ctx, "login")
} }
msg.Info(ctx, "Your account has been created. You are now logged in.") 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, "home") return c.Redirect(ctx, "home")
} }
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
err = c.Container.Mail.Send(ctx, usr.Email, fmt.Sprintf(
"Confirm your email address: %s",
ctx.Echo().Reverse("verify_email", token),
))
if err != nil {
ctx.Logger().Errorf("unable to send email verification token: %v", err)
return
}
msg.Info(ctx, "An email was sent to you to verify your email address.")
}

View file

@ -83,6 +83,9 @@ func userRoutes(c *services.Container, g *echo.Group, ctr controller.Controller)
logout := Logout{Controller: ctr} logout := Logout{Controller: ctr}
g.GET("/logout", logout.Get, middleware.RequireAuthentication()).Name = "logout" g.GET("/logout", logout.Get, middleware.RequireAuthentication()).Name = "logout"
verifyEmail := VerifyEmail{Controller: ctr}
g.GET("/email/verify/:token", verifyEmail.Get).Name = "verify_email"
noAuth := g.Group("/user", middleware.RequireNoAuthentication()) noAuth := g.Group("/user", middleware.RequireNoAuthentication())
login := Login{Controller: ctr} login := Login{Controller: ctr}
noAuth.GET("/login", login.Get).Name = "login" noAuth.GET("/login", login.Get).Name = "login"

70
routes/verify_email.go Normal file
View file

@ -0,0 +1,70 @@
package routes
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/context"
"github.com/mikestefanello/pagoda/controller"
"github.com/mikestefanello/pagoda/ent"
"github.com/mikestefanello/pagoda/ent/user"
"github.com/mikestefanello/pagoda/msg"
)
type VerifyEmail struct {
controller.Controller
}
func (c *VerifyEmail) Get(ctx echo.Context) error {
c.verifyToken(ctx)
return c.Redirect(ctx, "home")
}
func (c *VerifyEmail) verifyToken(ctx echo.Context) {
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
}
// 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 {
ctx.Logger().Errorf("error querying user during email verification: %v", err)
msg.Danger(ctx, "An error occurred. Please try again.")
return
}
}
// Verify the user
err = c.Container.ORM.User.
Update().
SetVerified(true).
Where(user.ID(usr.ID)).
Exec(ctx.Request().Context())
if err != nil {
ctx.Logger().Errorf("error setting user as verified: %v", err)
msg.Danger(ctx, "An error occurred. Please try again.")
return
}
msg.Success(ctx, "You email has been successfully verified.")
}

View file

@ -3,8 +3,11 @@ package services
import ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors"
"fmt"
"time" "time"
"github.com/golang-jwt/jwt"
"github.com/mikestefanello/pagoda/config" "github.com/mikestefanello/pagoda/config"
"github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent"
"github.com/mikestefanello/pagoda/ent/passwordtoken" "github.com/mikestefanello/pagoda/ent/passwordtoken"
@ -194,3 +197,36 @@ func (c *AuthClient) RandomToken(length int) (string, error) {
token := hex.EncodeToString(b) token := hex.EncodeToString(b)
return token[:length], nil return token[:length], nil
} }
// GenerateEmailVerificationToken generates an email verification token for a given email address using JWT which
// is set to expire based on the duration stored in configuration
func (c *AuthClient) GenerateEmailVerificationToken(email string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"email": email,
"exp": time.Now().Add(c.config.App.EmailVerificationTokenExpiration).Unix(),
})
return token.SignedString([]byte(c.config.App.EncryptionKey))
}
// ValidateEmailVerificationToken validates an email verification token and returns the associated email address if
// the token is valid and has not expired
func (c *AuthClient) ValidateEmailVerificationToken(token string) (string, error) {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(c.config.App.EncryptionKey), nil
})
if err != nil {
return "", err
}
if claims, ok := t.Claims.(jwt.MapClaims); ok && t.Valid {
return claims["email"].(string), nil
}
return "", errors.New("invalid or expired token")
}

View file

@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAuth(t *testing.T) { func TestAuthClient_Auth(t *testing.T) {
assertNoAuth := func() { assertNoAuth := func() {
_, err := c.Auth.GetAuthenticatedUserID(ctx) _, err := c.Auth.GetAuthenticatedUserID(ctx)
assert.True(t, errors.Is(err, NotAuthenticatedError{})) assert.True(t, errors.Is(err, NotAuthenticatedError{}))
@ -41,7 +41,7 @@ func TestAuth(t *testing.T) {
assertNoAuth() assertNoAuth()
} }
func TestPasswordHashing(t *testing.T) { func TestAuthClient_PasswordHashing(t *testing.T) {
pw := "testcheckpassword" pw := "testcheckpassword"
hash, err := c.Auth.HashPassword(pw) hash, err := c.Auth.HashPassword(pw)
assert.NoError(t, err) assert.NoError(t, err)
@ -50,14 +50,14 @@ func TestPasswordHashing(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestGeneratePasswordResetToken(t *testing.T) { func TestAuthClient_GeneratePasswordResetToken(t *testing.T) {
token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID) token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, token, c.Config.App.PasswordToken.Length) assert.Len(t, token, c.Config.App.PasswordToken.Length)
assert.NoError(t, c.Auth.CheckPassword(token, pt.Hash)) assert.NoError(t, c.Auth.CheckPassword(token, pt.Hash))
} }
func TestGetValidPasswordToken(t *testing.T) { func TestAuthClient_GetValidPasswordToken(t *testing.T) {
// Check that a fake token is not valid // Check that a fake token is not valid
_, err := c.Auth.GetValidPasswordToken(ctx, "faketoken", usr.ID) _, err := c.Auth.GetValidPasswordToken(ctx, "faketoken", usr.ID)
assert.Error(t, err) assert.Error(t, err)
@ -82,7 +82,7 @@ func TestGetValidPasswordToken(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestDeletePasswordTokens(t *testing.T) { func TestAuthClient_DeletePasswordTokens(t *testing.T) {
// Create three tokens for the user // Create three tokens for the user
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID) _, _, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
@ -103,7 +103,7 @@ func TestDeletePasswordTokens(t *testing.T) {
assert.Equal(t, 0, count) assert.Equal(t, 0, count)
} }
func TestRandomToken(t *testing.T) { func TestAuthClient_RandomToken(t *testing.T) {
length := c.Config.App.PasswordToken.Length length := c.Config.App.PasswordToken.Length
a, err := c.Auth.RandomToken(length) a, err := c.Auth.RandomToken(length)
require.NoError(t, err) require.NoError(t, err)
@ -113,3 +113,33 @@ func TestRandomToken(t *testing.T) {
assert.Len(t, b, length) assert.Len(t, b, length)
assert.NotEqual(t, a, b) assert.NotEqual(t, a, b)
} }
func TestAuthClient_EmailVerificationToken(t *testing.T) {
t.Run("valid token", func(t *testing.T) {
email := "test@localhost.com"
token, err := c.Auth.GenerateEmailVerificationToken(email)
require.NoError(t, err)
tokenEmail, err := c.Auth.ValidateEmailVerificationToken(token)
require.NoError(t, err)
assert.Equal(t, email, tokenEmail)
})
t.Run("invalid token", func(t *testing.T) {
badToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAbG9jYWxob3N0LmNvbSIsImV4cCI6MTkxNzg2NDAwMH0.ScJCpfEEzlilKfRs_aVouzwPNKI28M3AIm-hyImQHUQ"
_, err := c.Auth.ValidateEmailVerificationToken(badToken)
assert.Error(t, err)
})
t.Run("expired token", func(t *testing.T) {
c.Config.App.EmailVerificationTokenExpiration = -time.Hour
email := "test@localhost.com"
token, err := c.Auth.GenerateEmailVerificationToken(email)
require.NoError(t, err)
_, err = c.Auth.ValidateEmailVerificationToken(token)
assert.Error(t, err)
c.Config.App.EmailVerificationTokenExpiration = time.Hour * 12
})
}

View file

@ -26,7 +26,7 @@
<script> <script>
document.body.addEventListener('htmx:configRequest', function(evt) { document.body.addEventListener('htmx:configRequest', function(evt) {
if (evt.detail.verb !== "get") { if (evt.detail.verb !== "get") {
evt.detail.parameters['csrf'] = '{{ .CSRF }}'; evt.detail.parameters['csrf'] = '{{.CSRF}}';
} }
}) })
</script> </script>

View file

@ -70,14 +70,14 @@
<h2 class="subtitle">Search</h2> <h2 class="subtitle">Search</h2>
<p class="control"> <p class="control">
<input <input
hx-get="{{call .ToURL "search"}}" hx-get="{{call .ToURL "search"}}"
hx-trigger="keyup changed delay:500ms" hx-trigger="keyup changed delay:500ms"
hx-target="#results" hx-target="#results"
name="query" name="query"
class="input" class="input"
type="search" type="search"
placeholder="Search..." placeholder="Search..."
x-ref="input" x-ref="input"
/> />
</p> </p>
<div class="block"></div> <div class="block"></div>