From 367d02f86ec81f48a0c41742da296421a7f7a58b Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sun, 18 Jul 2021 15:26:29 +0530 Subject: [PATCH] Add forgot password resolver Resolves #10 --- server/constants/env.go | 5 +- server/enum/verification.go | 2 + server/graph/generated/generated.go | 242 +++++++++++++++++++++- server/graph/model/models_gen.go | 10 + server/graph/schema.graphqls | 12 ++ server/graph/schema.resolvers.go | 8 + server/resolvers/forgotPassword.go | 47 +++++ server/resolvers/forgotPasswordRequest.go | 50 +++++ server/utils/email.go | 27 ++- 9 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 server/resolvers/forgotPassword.go create mode 100644 server/resolvers/forgotPasswordRequest.go diff --git a/server/constants/env.go b/server/constants/env.go index 9b93418..0fe3b26 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -32,6 +32,8 @@ var ( GITHUB_CLIENT_SECRET = "" // FACEBOOK_CLIENT_ID = "" // FACEBOOK_CLIENT_SECRET = "" + FORGOT_PASSWORD_URI = "" + VERIFY_EMAIL_URI = "" ) func init() { @@ -60,7 +62,8 @@ func init() { GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET") // FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID") // FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET") - + FORGOT_PASSWORD_URI = strings.TrimPrefix(os.Getenv("FORGOT_PASSWORD_URI"), "/") + VERIFY_EMAIL_URI = strings.TrimPrefix(os.Getenv("VERIFY_EMAIL_URI"), "/") if YAUTH_ADMIN_SECRET == "" { panic("Yauth admin secret is required") } diff --git a/server/enum/verification.go b/server/enum/verification.go index af90671..fb409f9 100644 --- a/server/enum/verification.go +++ b/server/enum/verification.go @@ -5,11 +5,13 @@ type VerificationType int const ( BasicAuthSignup VerificationType = iota UpdateEmail + ForgotPassword ) func (d VerificationType) String() string { return [...]string{ "basic_auth_signup", "update_email", + "forgot_password", }[d] } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 7d59191..93b07be 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -56,12 +56,14 @@ type ComplexityRoot struct { } Mutation struct { - Login func(childComplexity int, params model.LoginInput) int - Logout func(childComplexity int) int - ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int - Signup func(childComplexity int, params model.SignUpInput) int - UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int - VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int + ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int + ForgotPasswordRequest func(childComplexity int, params model.ForgotPasswordRequestInput) int + Login func(childComplexity int, params model.LoginInput) int + Logout func(childComplexity int) int + ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int + Signup func(childComplexity int, params model.SignUpInput) int + UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int + VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int } Query struct { @@ -106,6 +108,8 @@ type MutationResolver interface { UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.LoginResponse, error) ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) + ForgotPasswordRequest(ctx context.Context, params model.ForgotPasswordRequestInput) (*model.Response, error) + ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) } type QueryResolver interface { Users(ctx context.Context) ([]*model.User, error) @@ -171,6 +175,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.LoginResponse.User(childComplexity), true + case "Mutation.forgotPassword": + if e.complexity.Mutation.ForgotPassword == nil { + break + } + + args, err := ec.field_Mutation_forgotPassword_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true + + case "Mutation.forgotPasswordRequest": + if e.complexity.Mutation.ForgotPasswordRequest == nil { + break + } + + args, err := ec.field_Mutation_forgotPasswordRequest_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ForgotPasswordRequest(childComplexity, args["params"].(model.ForgotPasswordRequestInput)), true + case "Mutation.login": if e.complexity.Mutation.Login == nil { break @@ -532,6 +560,16 @@ input UpdateProfileInput { email: String } +input ForgotPasswordRequestInput { + email: String! +} + +input ForgotPasswordInput { + token: String! + newPassword: String! + confirmPassword: String! +} + type Mutation { signup(params: SignUpInput!): Response! login(params: LoginInput!): LoginResponse! @@ -539,6 +577,8 @@ type Mutation { updateProfile(params: UpdateProfileInput!): Response! verifyEmail(params: VerifyEmailInput!): LoginResponse! resendVerifyEmail(params: ResendVerifyEmailInput!): Response! + forgotPasswordRequest(params: ForgotPasswordRequestInput!): Response! + forgotPassword(params: ForgotPasswordInput!): Response! } type Query { @@ -555,6 +595,36 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_forgotPasswordRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.ForgotPasswordRequestInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNForgotPasswordRequestInput2githubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐForgotPasswordRequestInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_forgotPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.ForgotPasswordInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNForgotPasswordInput2githubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐForgotPasswordInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_login_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1129,6 +1199,90 @@ func (ec *executionContext) _Mutation_resendVerifyEmail(ctx context.Context, fie return ec.marshalNResponse2ᚖgithubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_forgotPasswordRequest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_forgotPasswordRequest_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ForgotPasswordRequest(rctx, args["params"].(model.ForgotPasswordRequestInput)) + }) + 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.(*model.Response) + fc.Result = res + return ec.marshalNResponse2ᚖgithubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_forgotPassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_forgotPassword_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ForgotPassword(rctx, args["params"].(model.ForgotPasswordInput)) + }) + 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.(*model.Response) + fc.Result = res + return ec.marshalNResponse2ᚖgithubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_users(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3015,6 +3169,62 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Context, obj interface{}) (model.ForgotPasswordInput, error) { + var it model.ForgotPasswordInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "token": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + it.Token, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "newPassword": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("newPassword")) + it.NewPassword, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "confirmPassword": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("confirmPassword")) + it.ConfirmPassword, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputForgotPasswordRequestInput(ctx context.Context, obj interface{}) (model.ForgotPasswordRequestInput, error) { + var it model.ForgotPasswordRequestInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "email": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + it.Email, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) { var it model.LoginInput var asMap = obj.(map[string]interface{}) @@ -3329,6 +3539,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "forgotPasswordRequest": + out.Values[i] = ec._Mutation_forgotPasswordRequest(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "forgotPassword": + out.Values[i] = ec._Mutation_forgotPassword(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3800,6 +4020,16 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) unmarshalNForgotPasswordInput2githubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐForgotPasswordInput(ctx context.Context, v interface{}) (model.ForgotPasswordInput, error) { + res, err := ec.unmarshalInputForgotPasswordInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNForgotPasswordRequestInput2githubᚗcomᚋyauthdevᚋyauthᚋserverᚋgraphᚋmodelᚐForgotPasswordRequestInput(ctx context.Context, v interface{}) (model.ForgotPasswordRequestInput, error) { + res, err := ec.unmarshalInputForgotPasswordRequestInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 1f1d2bf..ad21f94 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -7,6 +7,16 @@ type Error struct { Reason string `json:"reason"` } +type ForgotPasswordInput struct { + Token string `json:"token"` + NewPassword string `json:"newPassword"` + ConfirmPassword string `json:"confirmPassword"` +} + +type ForgotPasswordRequestInput struct { + Email string `json:"email"` +} + type LoginInput struct { Email string `json:"email"` Password string `json:"password"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 42bf6a0..bb5362e 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -74,6 +74,16 @@ input UpdateProfileInput { email: String } +input ForgotPasswordRequestInput { + email: String! +} + +input ForgotPasswordInput { + token: String! + newPassword: String! + confirmPassword: String! +} + type Mutation { signup(params: SignUpInput!): Response! login(params: LoginInput!): LoginResponse! @@ -81,6 +91,8 @@ type Mutation { updateProfile(params: UpdateProfileInput!): Response! verifyEmail(params: VerifyEmailInput!): LoginResponse! resendVerifyEmail(params: ResendVerifyEmailInput!): Response! + forgotPasswordRequest(params: ForgotPasswordRequestInput!): Response! + forgotPassword(params: ForgotPasswordInput!): Response! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 5e623e3..6a7bf95 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -35,6 +35,14 @@ func (r *mutationResolver) ResendVerifyEmail(ctx context.Context, params model.R return resolvers.ResendVerifyEmail(ctx, params) } +func (r *mutationResolver) ForgotPasswordRequest(ctx context.Context, params model.ForgotPasswordRequestInput) (*model.Response, error) { + return resolvers.ForgotPasswordRequest(ctx, params) +} + +func (r *mutationResolver) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) { + return resolvers.ForgotPassword(ctx, params) +} + func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { return resolvers.Users(ctx) } diff --git a/server/resolvers/forgotPassword.go b/server/resolvers/forgotPassword.go new file mode 100644 index 0000000..c17b1f4 --- /dev/null +++ b/server/resolvers/forgotPassword.go @@ -0,0 +1,47 @@ +package resolvers + +import ( + "context" + "fmt" + + "github.com/yauthdev/yauth/server/db" + "github.com/yauthdev/yauth/server/graph/model" + "github.com/yauthdev/yauth/server/utils" +) + +func ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) { + var res *model.Response + + if params.NewPassword != params.ConfirmPassword { + return res, fmt.Errorf(`passwords don't match`) + } + + _, err := db.Mgr.GetVerificationByToken(params.Token) + if err != nil { + return res, fmt.Errorf(`invalid token`) + } + + // verify if token exists in db + claim, err := utils.VerifyVerificationToken(params.Token) + if err != nil { + return res, fmt.Errorf(`invalid token`) + } + + user, err := db.Mgr.GetUserByEmail(claim.Email) + if err != nil { + return res, err + } + + password, _ := utils.HashPassword(params.NewPassword) + user.Password = password + + // delete from verification table + db.Mgr.DeleteToken(claim.Email) + db.Mgr.UpdateUser(user) + + res = &model.Response{ + Message: `Password updated successfully.`, + } + + return res, nil +} diff --git a/server/resolvers/forgotPasswordRequest.go b/server/resolvers/forgotPasswordRequest.go new file mode 100644 index 0000000..4e38537 --- /dev/null +++ b/server/resolvers/forgotPasswordRequest.go @@ -0,0 +1,50 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/yauthdev/yauth/server/db" + "github.com/yauthdev/yauth/server/enum" + "github.com/yauthdev/yauth/server/graph/model" + "github.com/yauthdev/yauth/server/utils" +) + +func ForgotPasswordRequest(ctx context.Context, params model.ForgotPasswordRequestInput) (*model.Response, error) { + var res *model.Response + params.Email = strings.ToLower(params.Email) + + if !utils.IsValidEmail(params.Email) { + return res, fmt.Errorf("invalid email") + } + + _, err := db.Mgr.GetUserByEmail(params.Email) + if err != nil { + return res, fmt.Errorf(`user with this email not found`) + } + + token, err := utils.CreateVerificationToken(params.Email, enum.ForgotPassword.String()) + if err != nil { + log.Println(`Error generating token`, err) + } + db.Mgr.AddVerification(db.VerificationRequest{ + Token: token, + Identifier: enum.ForgotPassword.String(), + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: params.Email, + }) + + // exec it as go routin so that we can reduce the api latency + go func() { + utils.SendForgotPasswordMail(params.Email, token) + }() + + res = &model.Response{ + Message: `Please check your inbox! We have sent a password reset link.`, + } + + return res, nil +} diff --git a/server/utils/email.go b/server/utils/email.go index d1462cc..a5f8ea5 100644 --- a/server/utils/email.go +++ b/server/utils/email.go @@ -26,7 +26,32 @@ func SendVerificationMail(toEmail, token string) error { Click here to verify - `, constants.FRONTEND_URL+"/verify?token="+token) + `, constants.FRONTEND_URL+"/"+constants.VERIFY_EMAIL_URI+"?token="+token) + bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) + + return sender.SendMail(Receiver, Subject, bodyMessage) +} + +// SendForgotPasswordMail to send verification email +func SendForgotPasswordMail(toEmail, token string) error { + sender := email.NewSender() + + // The receiver needs to be in slice as the receive supports multiple receiver + Receiver := []string{toEmail} + + Subject := "Reset Password" + message := fmt.Sprintf(` + + + + + + +

Please use the link below to reset password


+ Reset Password + + + `, constants.FRONTEND_URL+"/"+constants.FORGOT_PASSWORD_URI+"?token="+token) bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) return sender.SendMail(Receiver, Subject, bodyMessage)