Add forgot password resolver

Resolves #10
This commit is contained in:
Lakhan Samani 2021-07-18 15:26:29 +05:30
parent 2840a085ca
commit 367d02f86e
9 changed files with 395 additions and 8 deletions

View File

@ -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")
}

View File

@ -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]
}

View File

@ -56,6 +56,8 @@ type ComplexityRoot struct {
}
Mutation struct {
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
@ -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)

View File

@ -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"`

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -26,7 +26,32 @@ func SendVerificationMail(toEmail, token string) error {
<a href="%s">Click here to verify</a>
</body>
</html>
`, 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(`
<!DOCTYPE HTML PULBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html"; charset=ISO-8859-1">
</head>
<body>
<h1>Please use the link below to reset password </h1><br/>
<a href="%s">Reset Password</a>
</body>
</html>
`, constants.FRONTEND_URL+"/"+constants.FORGOT_PASSWORD_URI+"?token="+token)
bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return sender.SendMail(Receiver, Subject, bodyMessage)