From 9a19552f7236c724a9cf33e0e74c6845a0cfe7ee Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 08:53:48 +0530 Subject: [PATCH 1/4] feat: add resolver for inviting members --- app/src/Root.tsx | 4 + app/src/pages/setup-password.tsx | 12 +++ server/email/invite_email.go | 113 ++++++++++++++++++++++ server/graph/generated/generated.go | 118 +++++++++++++++++++++++ server/graph/model/models_gen.go | 5 + server/graph/schema.graphqls | 6 ++ server/graph/schema.resolvers.go | 4 + server/resolvers/forgot_password.go | 2 +- server/resolvers/invite_members.go | 134 +++++++++++++++++++++++++++ server/resolvers/magic_link_login.go | 2 +- server/resolvers/signup.go | 2 +- server/resolvers/update_profile.go | 2 +- server/resolvers/update_user.go | 2 +- server/utils/urls.go | 11 +++ 14 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 app/src/pages/setup-password.tsx create mode 100644 server/email/invite_email.go create mode 100644 server/resolvers/invite_members.go diff --git a/app/src/Root.tsx b/app/src/Root.tsx index d62ded8..8707a2b 100644 --- a/app/src/Root.tsx +++ b/app/src/Root.tsx @@ -1,6 +1,7 @@ import React, { useEffect, lazy, Suspense } from 'react'; import { Switch, Route } from 'react-router-dom'; import { useAuthorizer } from '@authorizerdev/authorizer-react'; +import SetupPassword from './pages/setup-password'; const ResetPassword = lazy(() => import('./pages/rest-password')); const Login = lazy(() => import('./pages/login')); @@ -60,6 +61,9 @@ export default function Root({ + + + ); diff --git a/app/src/pages/setup-password.tsx b/app/src/pages/setup-password.tsx new file mode 100644 index 0000000..1709797 --- /dev/null +++ b/app/src/pages/setup-password.tsx @@ -0,0 +1,12 @@ +import React, { Fragment } from 'react'; +import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react'; + +export default function SetupPassword() { + return ( + +

Setup new Password

+
+ +
+ ); +} diff --git a/server/email/invite_email.go b/server/email/invite_email.go new file mode 100644 index 0000000..23a5cf3 --- /dev/null +++ b/server/email/invite_email.go @@ -0,0 +1,113 @@ +package email + +import ( + "log" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/envstore" +) + +// InviteEmail to send invite email +func InviteEmail(toEmail, token, url string) error { + // The receiver needs to be in slice as the receive supports multiple receiver + Receiver := []string{toEmail} + + Subject := "Please accept the invitation" + message := ` + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+
+
+ + + ` + data := make(map[string]interface{}, 3) + data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) + data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) + data["verification_url"] = url + "?token=" + token + message = addEmailTemplate(message, data, "verify_email.tmpl") + // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) + + err := SendMail(Receiver, Subject, message) + if err != nil { + log.Println("=> error sending email:", err) + } + return err +} diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 817dd79..43d5c21 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -114,6 +114,7 @@ type ComplexityRoot struct { AdminSignup func(childComplexity int, params model.AdminSignupInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int + InviteMembers func(childComplexity int, params model.InviteMemberInput) int Login func(childComplexity int, params model.LoginInput) int Logout func(childComplexity int) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int @@ -208,6 +209,7 @@ type MutationResolver interface { AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) AdminLogout(ctx context.Context) (*model.Response, error) UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error) + InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -660,6 +662,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true + case "Mutation._invite_members": + if e.complexity.Mutation.InviteMembers == nil { + break + } + + args, err := ec.field_Mutation__invite_members_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.InviteMembers(childComplexity, args["params"].(model.InviteMemberInput)), true + case "Mutation.login": if e.complexity.Mutation.Login == nil { break @@ -1434,6 +1448,11 @@ input OAuthRevokeInput { refresh_token: String! } +input InviteMemberInput { + emails: [String!]! + redirect_uri: String +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -1452,6 +1471,7 @@ type Mutation { _admin_login(params: AdminLoginInput!): Response! _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! + _invite_members(params: InviteMemberInput!): Response! } type Query { @@ -1517,6 +1537,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.InviteMemberInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4182,6 +4217,48 @@ func (ec *executionContext) _Mutation__update_env(ctx context.Context, field gra return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__invite_members(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__invite_members_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().InviteMembers(rctx, args["params"].(model.InviteMemberInput)) + }) + 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ᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6914,6 +6991,37 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) { + var it model.InviteMemberInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "emails": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emails")) + it.Emails, err = ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + case "redirect_uri": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri")) + it.RedirectURI, err = ec.unmarshalOString2ᚖstring(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 asMap := map[string]interface{}{} @@ -8182,6 +8290,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_invite_members": + out.Values[i] = ec._Mutation__invite_members(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8911,6 +9024,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel return res } +func (ec *executionContext) unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx context.Context, v interface{}) (model.InviteMemberInput, error) { + res, err := ec.unmarshalInputInviteMemberInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) { res, err := ec.unmarshalInputLoginInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ea069e5..88be349 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -74,6 +74,11 @@ type ForgotPasswordInput struct { RedirectURI *string `json:"redirect_uri"` } +type InviteMemberInput struct { + Emails []string `json:"emails"` + RedirectURI *string `json:"redirect_uri"` +} + type LoginInput struct { Email string `json:"email"` Password string `json:"password"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 18a727c..fcd2db0 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -272,6 +272,11 @@ input OAuthRevokeInput { refresh_token: String! } +input InviteMemberInput { + emails: [String!]! + redirect_uri: String +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -290,6 +295,7 @@ type Mutation { _admin_login(params: AdminLoginInput!): Response! _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! + _invite_members(params: InviteMemberInput!): Response! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 245d7d8..e4f9275 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -75,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv return resolvers.UpdateEnvResolver(ctx, params) } +func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { + return resolvers.InviteMembersResolver(ctx, params) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.MetaResolver(ctx) } diff --git a/server/resolvers/forgot_password.go b/server/resolvers/forgot_password.go index 4be96b1..58edb08 100644 --- a/server/resolvers/forgot_password.go +++ b/server/resolvers/forgot_password.go @@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu if err != nil { return res, err } - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) + "/reset-password" if params.RedirectURI != nil { redirectURL = *params.RedirectURI } diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go new file mode 100644 index 0000000..649e36a --- /dev/null +++ b/server/resolvers/invite_members.go @@ -0,0 +1,134 @@ +package resolvers + +import ( + "context" + "errors" + "log" + "strings" + "time" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + emailservice "github.com/authorizerdev/authorizer/server/email" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// InviteMembersResolver resolver to invite members +func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, errors.New("unauthorized") + } + + // this feature is only allowed if email server is configured + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { + return res, errors.New("email sending is disabled") + } + + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { + return res, errors.New("either basic authentication or magic link login is required") + } + + // filter valid emails + emails := []string{} + for _, email := range params.Emails { + if utils.IsValidEmail(email) { + emails = append(emails, email) + } + } + + if len(emails) == 0 { + res.Message = "No valid emails found" + return res, errors.New("no valid emails found") + } + + // TODO: optimise to use like query instead of looping through emails and getting user individually + // for each emails check if emails exists in db + newEmails := []string{} + for _, email := range emails { + _, err := db.Provider.GetUserByEmail(email) + if err != nil { + log.Printf("%s user not found. inviting user.", email) + newEmails = append(newEmails, email) + } else { + log.Println("%s user already exists. skipping.", email) + } + } + + if len(newEmails) == 0 { + res.Message = "All emails already exist" + return res, errors.New("all emails already exist") + } + + // invite new emails + for _, email := range newEmails { + + user := models.User{ + Email: email, + Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","), + } + redirectURL := utils.GetAppURL(gc) + "/verify_email" + if params.RedirectURI != nil { + redirectURL = *params.RedirectURI + } + + _, nonceHash, err := utils.GenerateNonce() + if err != nil { + return res, err + } + + verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, redirectURL, nonceHash, redirectURL) + if err != nil { + log.Println(`error generating token`, err) + } + + verificationRequest := models.VerificationRequest{ + Token: verificationToken, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: email, + Nonce: nonceHash, + RedirectURI: redirectURL, + } + + // use magic link login if that option is on + if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { + user.SignupMethods = constants.SignupMethodMagicLinkLogin + verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin + } else { + // use basic authentication if that option is on + user.SignupMethods = constants.SignupMethodBasicAuth + verificationRequest.Identifier = constants.VerificationTypeForgotPassword + + redirectURL = utils.GetAppURL(gc) + "/setup-password" + if params.RedirectURI != nil { + redirectURL = *params.RedirectURI + } + + } + + user, err = db.Provider.AddUser(user) + if err != nil { + log.Printf("error inviting user: %s, err: %v", email, err) + return res, err + } + + _, err = db.Provider.AddVerificationRequest(verificationRequest) + if err != nil { + log.Printf("error inviting user: %s, err: %v", email, err) + return res, err + } + + go emailservice.InviteEmail(email, verificationToken, redirectURL) + } + + return res, nil +} diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index 7014ca1..05d0708 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -123,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu if params.Scope != nil && len(params.Scope) > 0 { redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ") } - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) if params.RedirectURI != nil { redirectURL = *params.RedirectURI } diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 308d284..478f8b7 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -128,7 +128,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR return res, err } verificationType := constants.VerificationTypeBasicAuthSignup - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL) if err != nil { return res, err diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index 5a3f328..73e87fe 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) return res, err } verificationType := constants.VerificationTypeUpdateEmail - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index f9438d4..16871cf 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod return res, err } verificationType := constants.VerificationTypeUpdateEmail - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) diff --git a/server/utils/urls.go b/server/utils/urls.go index 64b8406..e4e6c06 100644 --- a/server/utils/urls.go +++ b/server/utils/urls.go @@ -4,6 +4,8 @@ import ( "net/url" "strings" + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/envstore" "github.com/gin-gonic/gin" ) @@ -71,3 +73,12 @@ func GetDomainName(uri string) string { return host } + +// GetAppURL to get /app/ url if not configured by user +func GetAppURL(gc *gin.Context) string { + envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + if envAppURL == "" { + envAppURL = GetHost(gc) + "/app/" + } + return envAppURL +} From 3e7150f872555857740d052e6ed3b019e19adc6f Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 09:56:50 +0530 Subject: [PATCH 2/4] fix: redirect uri --- app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 1131b59..e81ebba 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -21,7 +21,7 @@ export default function App() { if (redirectURL) { urlProps.redirectURL = redirectURL; } else { - urlProps.redirectURL = window.location.origin; + urlProps.redirectURL = window.location.origin + '/app'; } const globalState: Record = { // @ts-ignore From 5e6ee8d9b0c3af61f714fcb636e2f60d503a2986 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 09:57:09 +0530 Subject: [PATCH 3/4] fix: setup-password flow --- server/email/invite_email.go | 2 +- server/resolvers/invite_members.go | 41 +++++++++++++++--------------- server/resolvers/session.go | 8 ++++-- server/test/invite_member_test.go | 1 + server/utils/urls.go | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 server/test/invite_member_test.go diff --git a/server/email/invite_email.go b/server/email/invite_email.go index 23a5cf3..7db1a0a 100644 --- a/server/email/invite_email.go +++ b/server/email/invite_email.go @@ -102,7 +102,7 @@ func InviteEmail(toEmail, token, url string) error { data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) data["verification_url"] = url + "?token=" + token - message = addEmailTemplate(message, data, "verify_email.tmpl") + message = addEmailTemplate(message, data, "invite_email.tmpl") // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) err := SendMail(Receiver, Subject, message) diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 649e36a..817cdd1 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "errors" + "fmt" "log" "strings" "time" @@ -20,22 +21,21 @@ import ( // InviteMembersResolver resolver to invite members func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { gc, err := utils.GinContextFromContext(ctx) - var res *model.Response if err != nil { - return res, err + return nil, err } if !token.IsSuperAdmin(gc) { - return res, errors.New("unauthorized") + return nil, errors.New("unauthorized") } // this feature is only allowed if email server is configured if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { - return res, errors.New("email sending is disabled") + return nil, errors.New("email sending is disabled") } if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { - return res, errors.New("either basic authentication or magic link login is required") + return nil, errors.New("either basic authentication or magic link login is required") } // filter valid emails @@ -47,8 +47,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } if len(emails) == 0 { - res.Message = "No valid emails found" - return res, errors.New("no valid emails found") + return nil, errors.New("no valid emails found") } // TODO: optimise to use like query instead of looping through emails and getting user individually @@ -65,8 +64,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } if len(newEmails) == 0 { - res.Message = "All emails already exist" - return res, errors.New("all emails already exist") + return nil, errors.New("all emails already exist") } // invite new emails @@ -76,17 +74,21 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) Email: email, Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","), } - redirectURL := utils.GetAppURL(gc) + "/verify_email" + hostname := utils.GetHost(gc) + verifyEmailURL := hostname + "/verify_email" + appURL := utils.GetAppURL(gc) + + redirectURL := appURL if params.RedirectURI != nil { redirectURL = *params.RedirectURI } _, nonceHash, err := utils.GenerateNonce() if err != nil { - return res, err + return nil, err } - verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, redirectURL, nonceHash, redirectURL) + verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) } @@ -108,27 +110,26 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) user.SignupMethods = constants.SignupMethodBasicAuth verificationRequest.Identifier = constants.VerificationTypeForgotPassword - redirectURL = utils.GetAppURL(gc) + "/setup-password" - if params.RedirectURI != nil { - redirectURL = *params.RedirectURI - } + verifyEmailURL = appURL + "/setup-password" } user, err = db.Provider.AddUser(user) if err != nil { log.Printf("error inviting user: %s, err: %v", email, err) - return res, err + return nil, err } _, err = db.Provider.AddVerificationRequest(verificationRequest) if err != nil { log.Printf("error inviting user: %s, err: %v", email, err) - return res, err + return nil, err } - go emailservice.InviteEmail(email, verificationToken, redirectURL) + go emailservice.InviteEmail(email, verificationToken, verifyEmailURL) } - return res, nil + return &model.Response{ + Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)), + }, nil } diff --git a/server/resolvers/session.go b/server/resolvers/session.go index 151321d..e68fe07 100644 --- a/server/resolvers/session.go +++ b/server/resolvers/session.go @@ -2,7 +2,9 @@ package resolvers import ( "context" + "errors" "fmt" + "log" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" @@ -24,13 +26,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod sessionToken, err := cookie.GetSession(gc) if err != nil { - return res, err + log.Println("error getting session token:", err) + return res, errors.New("unauthorized") } // get session from cookie claims, err := token.ValidateBrowserSession(gc, sessionToken) if err != nil { - return res, err + log.Println("session validation failed:", err) + return res, errors.New("unauthorized") } userID := claims.Subject user, err := db.Provider.GetUserByID(userID) diff --git a/server/test/invite_member_test.go b/server/test/invite_member_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/invite_member_test.go @@ -0,0 +1 @@ +package test diff --git a/server/utils/urls.go b/server/utils/urls.go index e4e6c06..390cfbd 100644 --- a/server/utils/urls.go +++ b/server/utils/urls.go @@ -78,7 +78,7 @@ func GetDomainName(uri string) string { func GetAppURL(gc *gin.Context) string { envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) if envAppURL == "" { - envAppURL = GetHost(gc) + "/app/" + envAppURL = GetHost(gc) + "/app" } return envAppURL } From 74a8024131c67d73b920b09ffac75d0f5513888a Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 12:09:54 +0530 Subject: [PATCH 4/4] feat: add integration test for invite_member --- server/test/invite_member_test.go | 57 +++++++++++++++++++++++++++++++ server/test/resolvers_test.go | 1 + 2 files changed, 58 insertions(+) diff --git a/server/test/invite_member_test.go b/server/test/invite_member_test.go index 56e5404..76cd389 100644 --- a/server/test/invite_member_test.go +++ b/server/test/invite_member_test.go @@ -1 +1,58 @@ package test + +import ( + "fmt" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/crypto" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func inviteUserTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should invite user successfully`, func(t *testing.T) { + req, ctx := createContext(s) + emails := []string{"invite_member1." + s.TestInfo.Email} + + // unauthorized error + res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + + assert.Error(t, err) + assert.Nil(t, res) + + h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)) + assert.Nil(t, err) + req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h)) + + // invalid emails test + invalidEmailsTest := []string{ + "test", + "test.com", + } + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: invalidEmailsTest, + }) + + // valid test + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + assert.Nil(t, err) + assert.NotNil(t, res) + + // duplicate error test + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + assert.Error(t, err) + assert.Nil(t, res) + + cleanData(emails[0]) + }) +} diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index bc8eedc..7e0c41d 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -62,6 +62,7 @@ func TestResolvers(t *testing.T) { magicLinkLoginTests(t, s) logoutTests(t, s) metaTests(t, s) + inviteUserTest(t, s) }) } }