From 3d31dbcfddda20c2825dfb356433640a7821f902 Mon Sep 17 00:00:00 2001 From: ayaanqui Date: Mon, 22 Apr 2024 13:15:26 -0500 Subject: [PATCH] feat: `resendEmailVerificationCode` mutation --- graph/generated.go | 99 ++++++++++++++++++- graph/resolver/user.resolvers.go | 14 +++ graph/user.graphql | 3 +- services/auth_service.go | 160 ++++++++++++++++++------------- 4 files changed, 205 insertions(+), 71 deletions(-) diff --git a/graph/generated.go b/graph/generated.go index 6974226..03d7731 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -177,10 +177,11 @@ type ComplexityRoot struct { } Mutation struct { - CreateAccount func(childComplexity int, input gmodel.CreateAccountInput) int - CreateEvent func(childComplexity int, input gmodel.CreateEvent) int - CreateLocation func(childComplexity int, input gmodel.CreateLocation) int - VerifyEmail func(childComplexity int, verificationCode string) int + CreateAccount func(childComplexity int, input gmodel.CreateAccountInput) int + CreateEvent func(childComplexity int, input gmodel.CreateEvent) int + CreateLocation func(childComplexity int, input gmodel.CreateLocation) int + ResendEmailVerificationCode func(childComplexity int, email string) int + VerifyEmail func(childComplexity int, verificationCode string) int } Owner struct { @@ -239,6 +240,7 @@ type MutationResolver interface { CreateLocation(ctx context.Context, input gmodel.CreateLocation) (*gmodel.Location, error) CreateAccount(ctx context.Context, input gmodel.CreateAccountInput) (*gmodel.User, error) VerifyEmail(ctx context.Context, verificationCode string) (*gmodel.User, error) + ResendEmailVerificationCode(ctx context.Context, email string) (bool, error) } type QueryResolver interface { GetAllCountries(ctx context.Context) ([]*gmodel.Country, error) @@ -961,6 +963,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateLocation(childComplexity, args["input"].(gmodel.CreateLocation)), true + case "Mutation.resendEmailVerificationCode": + if e.complexity.Mutation.ResendEmailVerificationCode == nil { + break + } + + args, err := ec.field_Mutation_resendEmailVerificationCode_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ResendEmailVerificationCode(childComplexity, args["email"].(string)), true + case "Mutation.verifyEmail": if e.complexity.Mutation.VerifyEmail == nil { break @@ -1416,6 +1430,21 @@ func (ec *executionContext) field_Mutation_createLocation_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Mutation_resendEmailVerificationCode_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["email"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["email"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_verifyEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -6209,6 +6238,61 @@ func (ec *executionContext) fieldContext_Mutation_verifyEmail(ctx context.Contex return fc, nil } +func (ec *executionContext) _Mutation_resendEmailVerificationCode(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_resendEmailVerificationCode(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ResendEmailVerificationCode(rctx, fc.Args["email"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_resendEmailVerificationCode(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_resendEmailVerificationCode_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Owner_id(ctx context.Context, field graphql.CollectedField, obj *gmodel.Owner) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Owner_id(ctx, field) if err != nil { @@ -10970,6 +11054,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "resendEmailVerificationCode": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_resendEmailVerificationCode(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/graph/resolver/user.resolvers.go b/graph/resolver/user.resolvers.go index 07bb0dd..0e39854 100644 --- a/graph/resolver/user.resolvers.go +++ b/graph/resolver/user.resolvers.go @@ -34,6 +34,20 @@ func (r *mutationResolver) VerifyEmail(ctx context.Context, verificationCode str return &user, nil } +// ResendEmailVerificationCode is the resolver for the resendEmailVerificationCode field. +func (r *mutationResolver) ResendEmailVerificationCode(ctx context.Context, email string) (bool, error) { + user, err := r.Service.FindUserByEmail(ctx, email) + if err != nil { + return false, fmt.Errorf("no user found with the provided email address") + } + email_verification, err := r.Service.ResendEmailVerification(ctx, user) + if err != nil { + return false, err + } + // TODO: Send email with new verification code + return email_verification.ID > 0, nil +} + // Login is the resolver for the login field. func (r *queryResolver) Login(ctx context.Context, email string, password string) (*gmodel.Auth, error) { auth, err := r.Service.LoginInternal(ctx, email, password) diff --git a/graph/user.graphql b/graph/user.graphql index 0f37118..fd0b04b 100644 --- a/graph/user.graphql +++ b/graph/user.graphql @@ -1,6 +1,7 @@ extend type Mutation { createAccount(input: CreateAccountInput!): User! - verifyEmail(verificationCode: String!): User! + verifyEmail(verificationCode: String!): User! + resendEmailVerificationCode(email: String!): Boolean! } extend type Query { diff --git a/services/auth_service.go b/services/auth_service.go index 88ec705..783c703 100644 --- a/services/auth_service.go +++ b/services/auth_service.go @@ -50,72 +50,100 @@ func (service Service) CreateEmailVerification(ctx context.Context, user gmodel. err := query.QueryContext(ctx, service.DbOrTxQueryable(), &email_verification) return email_verification, err } - -func (service Service) FindEmailVerificationByCode(ctx context.Context, verification_code string) (model.EmailVerification, error) { - qb := table.EmailVerification. - SELECT(table.EmailVerification.AllColumns). - WHERE(table.EmailVerification.Code.EQ(postgres.String(verification_code))). - LIMIT(1) - var email_verification model.EmailVerification - if err := qb.QueryContext(ctx, service.DbOrTxQueryable(), &email_verification); err != nil { - return model.EmailVerification{}, fmt.Errorf("invalid email verification code") - } - return email_verification, nil -} - -func (service Service) VerifyUserEmail(ctx context.Context, verification_code string) (gmodel.User, error) { - var err error - service.TX, err = service.DB.BeginTx(ctx, nil) - if err != nil { - service.TX.Rollback() - return gmodel.User{}, err - } - - email_verification, err := service.FindEmailVerificationByCode(ctx, verification_code) - if err != nil { - service.TX.Rollback() - return gmodel.User{}, err - } - - if time.Until(email_verification.CreatedAt).Abs() > time.Hour { - service.TX.Rollback() - // Delete verification entry since it's expired - del_query := table.EmailVerification. - DELETE(). - WHERE(table.EmailVerification.ID.EQ(postgres.Int(email_verification.ID))) - if _, err := del_query.ExecContext(ctx, service.DB); err != nil { - return gmodel.User{}, err - } - return gmodel.User{}, fmt.Errorf("verification code has expired") - } - - update := table.User. - UPDATE(table.User.Active, table.User.UpdatedAt). - SET(postgres.Bool(true), postgres.DateT(time.Now())). - WHERE(table.User.ID.EQ(postgres.Int(email_verification.UserID))) - if _, err := update.ExecContext(ctx, service.TX); err != nil { - service.TX.Rollback() - return gmodel.User{}, fmt.Errorf("could not update user email verification status to verified") - } - - // Remove email_verification row - delete := table.EmailVerification. - DELETE(). - WHERE(postgres.AND( - table.EmailVerification.ID.EQ(postgres.Int(email_verification.ID)), - table.EmailVerification.Code.EQ(postgres.String(verification_code)), - )) - if _, err := delete.ExecContext(ctx, service.TX); err != nil { - service.TX.Rollback() - return gmodel.User{}, fmt.Errorf("could not delete email verification entry") - } - - if err := service.TX.Commit(); err != nil { - return gmodel.User{}, fmt.Errorf("could not commit changes") - } - service.TX = nil - return service.FindUserById(ctx, email_verification.UserID) -} + +func (service Service) ResendEmailVerification(ctx context.Context, user gmodel.User) (email_verification model.EmailVerification, err error) { + if user.Active { + return model.EmailVerification{}, fmt.Errorf("user already has a verified email address") + } + service.TX, err = service.DB.BeginTx(ctx, nil) + if err != nil { + return model.EmailVerification{}, err + } + + _, err = table.EmailVerification.DELETE(). + WHERE(table.EmailVerification.UserID.EQ(postgres.Int(user.ID))). + ExecContext(ctx, service.TX) + if err != nil { + service.TX.Rollback() + return model.EmailVerification{}, fmt.Errorf("user email verification entry deletion failed") + } + + email_verification, err = service.CreateEmailVerification(ctx, user) + if err != nil { + service.TX.Rollback() + return model.EmailVerification{}, err + } + if err := service.TX.Commit(); err != nil { + return model.EmailVerification{}, fmt.Errorf("could not commit changes") + } + return email_verification, nil +} + +func (service Service) FindEmailVerificationByCode(ctx context.Context, verification_code string) (model.EmailVerification, error) { + qb := table.EmailVerification. + SELECT(table.EmailVerification.AllColumns). + WHERE(table.EmailVerification.Code.EQ(postgres.String(verification_code))). + LIMIT(1) + var email_verification model.EmailVerification + if err := qb.QueryContext(ctx, service.DbOrTxQueryable(), &email_verification); err != nil { + return model.EmailVerification{}, fmt.Errorf("invalid email verification code") + } + return email_verification, nil +} + +func (service Service) VerifyUserEmail(ctx context.Context, verification_code string) (gmodel.User, error) { + var err error + service.TX, err = service.DB.BeginTx(ctx, nil) + if err != nil { + service.TX.Rollback() + return gmodel.User{}, err + } + + email_verification, err := service.FindEmailVerificationByCode(ctx, verification_code) + if err != nil { + service.TX.Rollback() + return gmodel.User{}, err + } + + if time.Until(email_verification.CreatedAt).Abs() > time.Hour { + service.TX.Rollback() + // Delete verification entry since it's expired + del_query := table.EmailVerification. + DELETE(). + WHERE(table.EmailVerification.ID.EQ(postgres.Int(email_verification.ID))) + if _, err := del_query.ExecContext(ctx, service.DB); err != nil { + return gmodel.User{}, err + } + return gmodel.User{}, fmt.Errorf("verification code has expired") + } + + update := table.User. + UPDATE(table.User.Active, table.User.UpdatedAt). + SET(postgres.Bool(true), postgres.DateT(time.Now())). + WHERE(table.User.ID.EQ(postgres.Int(email_verification.UserID))) + if _, err := update.ExecContext(ctx, service.TX); err != nil { + service.TX.Rollback() + return gmodel.User{}, fmt.Errorf("could not update user email verification status to verified") + } + + // Remove email_verification row + delete := table.EmailVerification. + DELETE(). + WHERE(postgres.AND( + table.EmailVerification.ID.EQ(postgres.Int(email_verification.ID)), + table.EmailVerification.Code.EQ(postgres.String(verification_code)), + )) + if _, err := delete.ExecContext(ctx, service.TX); err != nil { + service.TX.Rollback() + return gmodel.User{}, fmt.Errorf("could not delete email verification entry") + } + + if err := service.TX.Commit(); err != nil { + return gmodel.User{}, fmt.Errorf("could not commit changes") + } + service.TX = nil + return service.FindUserById(ctx, email_verification.UserID) +} func (Service) HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)