Added user email verification support.
This commit is contained in:
parent
feb11bbe5b
commit
ea46a38f68
31 changed files with 417 additions and 85 deletions
43
README.md
43
README.md
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
15
ent/user.go
15
ent/user.go
|
|
@ -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(')')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
2
go.mod
|
|
@ -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
7
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
70
routes/verify_email.go
Normal 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.")
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue