From b3a52c2466194560eeb5e31cabd89dbac8dbc8e2 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Sep 2021 08:23:40 +0530 Subject: [PATCH] feat: allow admin to user profile (#51) Resolves #49 --- server/db/db.go | 1 + server/db/user.go | 13 +++ server/graph/generated/generated.go | 151 ++++++++++++++++++++++++++++ server/graph/model/models_gen.go | 9 ++ server/graph/schema.graphqls | 10 ++ server/graph/schema.resolvers.go | 10 +- server/resolvers/adminUpdateUser.go | 129 ++++++++++++++++++++++++ 7 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 server/resolvers/adminUpdateUser.go diff --git a/server/db/db.go b/server/db/db.go index dbd6350..5f35cc9 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -17,6 +17,7 @@ type Manager interface { UpdateUser(user User) (User, error) GetUsers() ([]User, error) GetUserByEmail(email string) (User, error) + GetUserByID(email string) (User, error) UpdateVerificationTime(verifiedAt int64, id uint) error AddVerification(verification VerificationRequest) (VerificationRequest, error) GetVerificationByToken(token string) (VerificationRequest, error) diff --git a/server/db/user.go b/server/db/user.go index ede8434..8af9409 100644 --- a/server/db/user.go +++ b/server/db/user.go @@ -2,6 +2,7 @@ package db import ( "log" + "time" "gorm.io/gorm/clause" ) @@ -37,6 +38,7 @@ func (mgr *manager) SaveUser(user User) (User, error) { // UpdateUser function to update user with ID conflict func (mgr *manager) UpdateUser(user User) (User, error) { + user.UpdatedAt = time.Now().Unix() result := mgr.db.Clauses( clause.OnConflict{ UpdateAll: true, @@ -72,6 +74,17 @@ func (mgr *manager) GetUserByEmail(email string) (User, error) { return user, nil } +func (mgr *manager) GetUserByID(id string) (User, error) { + var user User + result := mgr.db.Where("id = ?", id).First(&user) + + if result.Error != nil { + return user, result.Error + } + + return user, nil +} + func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uint) error { user := &User{ ID: id, diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 94b05ad..d1ea7c0 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -66,6 +66,7 @@ type ComplexityRoot struct { } Mutation struct { + AdminUpdateUser func(childComplexity int, params model.AdminUpdateUserInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int Login func(childComplexity int, params model.LoginInput) int @@ -118,6 +119,7 @@ type MutationResolver interface { Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, error) Logout(ctx context.Context) (*model.Response, error) UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error) + AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.AuthResponse, error) ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) @@ -238,6 +240,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.Version(childComplexity), true + case "Mutation.adminUpdateUser": + if e.complexity.Mutation.AdminUpdateUser == nil { + break + } + + args, err := ec.field_Mutation_adminUpdateUser_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AdminUpdateUser(childComplexity, args["params"].(model.AdminUpdateUserInput)), true + case "Mutation.deleteUser": if e.complexity.Mutation.DeleteUser == nil { break @@ -662,6 +676,15 @@ input UpdateProfileInput { # roles: [String] } +input AdminUpdateUserInput { + id: ID! + email: String + firstName: String + lastName: String + image: String + roles: [String] +} + input ForgotPasswordInput { email: String! } @@ -681,6 +704,7 @@ type Mutation { login(params: LoginInput!): AuthResponse! logout: Response! updateProfile(params: UpdateProfileInput!): Response! + adminUpdateUser(params: AdminUpdateUserInput!): User! verifyEmail(params: VerifyEmailInput!): AuthResponse! resendVerifyEmail(params: ResendVerifyEmailInput!): Response! forgotPassword(params: ForgotPasswordInput!): Response! @@ -703,6 +727,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_adminUpdateUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.AdminUpdateUserInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNAdminUpdateUserInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminUpdateUserInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1498,6 +1537,48 @@ func (ec *executionContext) _Mutation_updateProfile(ctx context.Context, field g return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_adminUpdateUser(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_adminUpdateUser_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().AdminUpdateUser(rctx, args["params"].(model.AdminUpdateUserInput)) + }) + 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.User) + fc.Result = res + return ec.marshalNUser2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_verifyEmail(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3639,6 +3720,66 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputAdminUpdateUserInput(ctx context.Context, obj interface{}) (model.AdminUpdateUserInput, error) { + var it model.AdminUpdateUserInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + it.ID, err = ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + case "email": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + it.Email, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "firstName": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("firstName")) + it.FirstName, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "lastName": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("lastName")) + it.LastName, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "image": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("image")) + it.Image, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "roles": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles")) + it.Roles, err = ec.unmarshalOString2ᚕᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteUserInput(ctx context.Context, obj interface{}) (model.DeleteUserInput, error) { var it model.DeleteUserInput var asMap = obj.(map[string]interface{}) @@ -4092,6 +4233,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "adminUpdateUser": + out.Values[i] = ec._Mutation_adminUpdateUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "verifyEmail": out.Values[i] = ec._Mutation_verifyEmail(ctx, field) if out.Values[i] == graphql.Null { @@ -4590,6 +4736,11 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) unmarshalNAdminUpdateUserInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminUpdateUserInput(ctx context.Context, v interface{}) (model.AdminUpdateUserInput, error) { + res, err := ec.unmarshalInputAdminUpdateUserInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNAuthResponse2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx context.Context, sel ast.SelectionSet, v model.AuthResponse) graphql.Marshaler { return ec._AuthResponse(ctx, sel, &v) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 3b8f59d..ae2c163 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -2,6 +2,15 @@ package model +type AdminUpdateUserInput struct { + ID string `json:"id"` + Email *string `json:"email"` + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` + Image *string `json:"image"` + Roles []*string `json:"roles"` +} + type AuthResponse struct { Message string `json:"message"` AccessToken *string `json:"accessToken"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 651720c..cb10e84 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -89,6 +89,15 @@ input UpdateProfileInput { # roles: [String] } +input AdminUpdateUserInput { + id: ID! + email: String + firstName: String + lastName: String + image: String + roles: [String] +} + input ForgotPasswordInput { email: String! } @@ -108,6 +117,7 @@ type Mutation { login(params: LoginInput!): AuthResponse! logout: Response! updateProfile(params: UpdateProfileInput!): Response! + adminUpdateUser(params: AdminUpdateUserInput!): User! verifyEmail(params: VerifyEmailInput!): AuthResponse! resendVerifyEmail(params: ResendVerifyEmailInput!): Response! forgotPassword(params: ForgotPasswordInput!): Response! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 448346c..7a55de6 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -27,6 +27,10 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, params model.Updat return resolvers.UpdateProfile(ctx, params) } +func (r *mutationResolver) AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error) { + return resolvers.AdminUpdateUser(ctx, params) +} + func (r *mutationResolver) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.AuthResponse, error) { return resolvers.VerifyEmail(ctx, params) } @@ -73,7 +77,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/server/resolvers/adminUpdateUser.go b/server/resolvers/adminUpdateUser.go new file mode 100644 index 0000000..fb491a7 --- /dev/null +++ b/server/resolvers/adminUpdateUser.go @@ -0,0 +1,129 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/enum" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/session" + "github.com/authorizerdev/authorizer/server/utils" +) + +func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.User + if err != nil { + return res, err + } + + if !utils.IsSuperAdmin(gc) { + return res, fmt.Errorf("unauthorized") + } + + if params.FirstName == nil && params.LastName == nil && params.Image == nil && params.Email == nil && params.Roles == nil { + return res, fmt.Errorf("please enter atleast one param to update") + } + + user, err := db.Mgr.GetUserByID(params.ID) + + if err != nil { + return res, fmt.Errorf(`User not found`) + } + + if params.FirstName != nil && user.FirstName != *params.FirstName { + user.FirstName = *params.FirstName + } + + if params.LastName != nil && user.LastName != *params.LastName { + user.LastName = *params.LastName + } + + if params.Image != nil && user.Image != *params.Image { + user.Image = *params.Image + } + + if params.Email != nil && user.Email != *params.Email { + // check if valid email + if !utils.IsValidEmail(*params.Email) { + return res, fmt.Errorf("invalid email address") + } + newEmail := strings.ToLower(*params.Email) + // check if user with new email exists + _, err = db.Mgr.GetUserByEmail(newEmail) + // err = nil means user exists + if err == nil { + return res, fmt.Errorf("user with this email address already exists") + } + + session.DeleteToken(fmt.Sprintf("%v", user.ID)) + utils.DeleteCookie(gc) + + user.Email = newEmail + user.EmailVerifiedAt = 0 + // insert verification request + verificationType := enum.UpdateEmail.String() + token, err := utils.CreateVerificationToken(newEmail, verificationType) + if err != nil { + log.Println(`Error generating token`, err) + } + db.Mgr.AddVerification(db.VerificationRequest{ + Token: token, + Identifier: verificationType, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: newEmail, + }) + + // exec it as go routin so that we can reduce the api latency + go func() { + utils.SendVerificationMail(newEmail, token) + }() + } + + rolesToSave := "" + if params.Roles != nil && len(params.Roles) > 0 { + currentRoles := strings.Split(user.Roles, ",") + inputRoles := []string{} + for _, item := range params.Roles { + inputRoles = append(inputRoles, *item) + } + + if !utils.IsValidRolesArray(inputRoles) { + return res, fmt.Errorf("invalid list of roles") + } + + if !utils.IsStringArrayEqual(inputRoles, currentRoles) { + rolesToSave = strings.Join(inputRoles, ",") + } + + session.DeleteToken(fmt.Sprintf("%v", user.ID)) + utils.DeleteCookie(gc) + } + + if rolesToSave != "" { + user.Roles = rolesToSave + } + + user, err = db.Mgr.UpdateUser(user) + if err != nil { + log.Println("Error updating user:", err) + return res, err + } + + userIdStr := fmt.Sprintf("%v", user.ID) + res = &model.User{ + ID: userIdStr, + Email: user.Email, + Image: &user.Image, + FirstName: &user.FirstName, + LastName: &user.LastName, + Roles: strings.Split(user.Roles, ","), + CreatedAt: &user.CreatedAt, + UpdatedAt: &user.UpdatedAt, + } + return res, nil +}