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 = "" GITHUB_CLIENT_SECRET = ""
// FACEBOOK_CLIENT_ID = "" // FACEBOOK_CLIENT_ID = ""
// FACEBOOK_CLIENT_SECRET = "" // FACEBOOK_CLIENT_SECRET = ""
FORGOT_PASSWORD_URI = ""
VERIFY_EMAIL_URI = ""
) )
func init() { func init() {
@ -60,7 +62,8 @@ func init() {
GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET") GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET")
// FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID") // FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID")
// FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET") // 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 == "" { if YAUTH_ADMIN_SECRET == "" {
panic("Yauth admin secret is required") panic("Yauth admin secret is required")
} }

View File

@ -5,11 +5,13 @@ type VerificationType int
const ( const (
BasicAuthSignup VerificationType = iota BasicAuthSignup VerificationType = iota
UpdateEmail UpdateEmail
ForgotPassword
) )
func (d VerificationType) String() string { func (d VerificationType) String() string {
return [...]string{ return [...]string{
"basic_auth_signup", "basic_auth_signup",
"update_email", "update_email",
"forgot_password",
}[d] }[d]
} }

View File

@ -56,12 +56,14 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
Login func(childComplexity int, params model.LoginInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
Logout func(childComplexity int) int ForgotPasswordRequest func(childComplexity int, params model.ForgotPasswordRequestInput) int
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int Login func(childComplexity int, params model.LoginInput) int
Signup func(childComplexity int, params model.SignUpInput) int Logout func(childComplexity int) int
UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
VerifyEmail func(childComplexity int, params model.VerifyEmailInput) 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 { Query struct {
@ -106,6 +108,8 @@ type MutationResolver interface {
UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error) UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error)
VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.LoginResponse, error) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.LoginResponse, error)
ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, 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 { type QueryResolver interface {
Users(ctx context.Context) ([]*model.User, error) 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 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": case "Mutation.login":
if e.complexity.Mutation.Login == nil { if e.complexity.Mutation.Login == nil {
break break
@ -532,6 +560,16 @@ input UpdateProfileInput {
email: String email: String
} }
input ForgotPasswordRequestInput {
email: String!
}
input ForgotPasswordInput {
token: String!
newPassword: String!
confirmPassword: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): Response! signup(params: SignUpInput!): Response!
login(params: LoginInput!): LoginResponse! login(params: LoginInput!): LoginResponse!
@ -539,6 +577,8 @@ type Mutation {
updateProfile(params: UpdateProfileInput!): Response! updateProfile(params: UpdateProfileInput!): Response!
verifyEmail(params: VerifyEmailInput!): LoginResponse! verifyEmail(params: VerifyEmailInput!): LoginResponse!
resendVerifyEmail(params: ResendVerifyEmailInput!): Response! resendVerifyEmail(params: ResendVerifyEmailInput!): Response!
forgotPasswordRequest(params: ForgotPasswordRequestInput!): Response!
forgotPassword(params: ForgotPasswordInput!): Response!
} }
type Query { type Query {
@ -555,6 +595,36 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...)
// region ***************************** args.gotpl ***************************** // 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) { func (ec *executionContext) field_Mutation_login_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} 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) 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) { func (ec *executionContext) _Query_users(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -3015,6 +3169,62 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co
// region **************************** input.gotpl ***************************** // 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) { func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) {
var it model.LoginInput var it model.LoginInput
var asMap = obj.(map[string]interface{}) 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 { if out.Values[i] == graphql.Null {
invalids++ 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: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -3800,6 +4020,16 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res 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) { func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) {
res, err := graphql.UnmarshalID(v) res, err := graphql.UnmarshalID(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@ -7,6 +7,16 @@ type Error struct {
Reason string `json:"reason"` 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 { type LoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`

View File

@ -74,6 +74,16 @@ input UpdateProfileInput {
email: String email: String
} }
input ForgotPasswordRequestInput {
email: String!
}
input ForgotPasswordInput {
token: String!
newPassword: String!
confirmPassword: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): Response! signup(params: SignUpInput!): Response!
login(params: LoginInput!): LoginResponse! login(params: LoginInput!): LoginResponse!
@ -81,6 +91,8 @@ type Mutation {
updateProfile(params: UpdateProfileInput!): Response! updateProfile(params: UpdateProfileInput!): Response!
verifyEmail(params: VerifyEmailInput!): LoginResponse! verifyEmail(params: VerifyEmailInput!): LoginResponse!
resendVerifyEmail(params: ResendVerifyEmailInput!): Response! resendVerifyEmail(params: ResendVerifyEmailInput!): Response!
forgotPasswordRequest(params: ForgotPasswordRequestInput!): Response!
forgotPassword(params: ForgotPasswordInput!): Response!
} }
type Query { type Query {

View File

@ -35,6 +35,14 @@ func (r *mutationResolver) ResendVerifyEmail(ctx context.Context, params model.R
return resolvers.ResendVerifyEmail(ctx, params) 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) { func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
return resolvers.Users(ctx) 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> <a href="%s">Click here to verify</a>
</body> </body>
</html> </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) bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return sender.SendMail(Receiver, Subject, bodyMessage) return sender.SendMail(Receiver, Subject, bodyMessage)