diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index d18bf18..fe588c3 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -80,6 +80,7 @@ func OAuthCallbackHandler() gin.HandlerFunc { existingUser, err := db.Provider.GetUserByEmail(ctx, user.Email) log := log.WithField("user", user.Email) + isSignUp := false if err != nil { isSignupDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) @@ -121,6 +122,7 @@ func OAuthCallbackHandler() gin.HandlerFunc { now := time.Now().Unix() user.EmailVerifiedAt = &now user, _ = db.Provider.AddUser(ctx, user) + isSignUp = true } else { user = existingUser if user.RevokedTimestamp != nil { @@ -215,11 +217,18 @@ func OAuthCallbackHandler() gin.HandlerFunc { memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) } - go db.Provider.AddSession(ctx, models.Session{ - UserID: user.ID, - UserAgent: utils.GetUserAgent(ctx.Request), - IP: utils.GetIP(ctx.Request), - }) + go func() { + if isSignUp { + utils.RegisterEvent(ctx, constants.EnvKeyDisableSignUp, provider, user) + } else { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, provider, user) + } + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(ctx.Request), + IP: utils.GetIP(ctx.Request), + }) + }() if strings.Contains(redirectURL, "?") { redirectURL = redirectURL + "&" + params } else { diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index 6d900bd..10d4fad 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -66,10 +66,12 @@ func VerifyEmailHandler() gin.HandlerFunc { return } + isSignUp := false // update email_verified_at in users table if user.EmailVerifiedAt == nil { now := time.Now().Unix() user.EmailVerifiedAt = &now + isSignUp = true db.Provider.UpdateUser(c, user) } // delete from verification table @@ -131,11 +133,19 @@ func VerifyEmailHandler() gin.HandlerFunc { redirectURL = redirectURL + "?" + strings.TrimPrefix(params, "&") } - go db.Provider.AddSession(c, models.Session{ - UserID: user.ID, - UserAgent: utils.GetUserAgent(c.Request), - IP: utils.GetIP(c.Request), - }) + go func() { + if isSignUp { + utils.RegisterEvent(c, constants.UserSignUpWebhookEvent, loginMethod, user) + } else { + utils.RegisterEvent(c, constants.UserLoginWebhookEvent, loginMethod, user) + } + + db.Provider.AddSession(c, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(c.Request), + IP: utils.GetIP(c.Request), + }) + }() c.Redirect(http.StatusTemporaryRedirect, redirectURL) } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index a74755b..e55f984 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -126,11 +126,14 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) } - go db.Provider.AddSession(ctx, models.Session{ - UserID: user.ID, - UserAgent: utils.GetUserAgent(gc.Request), - IP: utils.GetIP(gc.Request), - }) + go func() { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() return res, nil } diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index f7f8949..456d5c0 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -100,6 +100,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu user.Roles = strings.Join(inputRoles, ",") user, _ = db.Provider.AddUser(ctx, user) + go utils.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodMagicLinkLogin, user) } else { user = existingUser // There multiple scenarios with roles here in magic link login diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 11653fb..dbd3652 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -207,7 +207,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR } // exec it as go routin so that we can reduce the api latency - go email.SendVerificationMail(params.Email, verificationToken, hostname) + go func() { + email.SendVerificationMail(params.Email, verificationToken, hostname) + utils.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) + }() res = &model.AuthResponse{ Message: `Verification email has been sent. Please check your inbox`, @@ -225,12 +228,6 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR return res, err } - go db.Provider.AddSession(ctx, models.Session{ - UserID: user.ID, - UserAgent: utils.GetUserAgent(gc.Request), - IP: utils.GetIP(gc.Request), - }) - expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() if expiresIn <= 0 { expiresIn = 1 @@ -252,6 +249,15 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR res.RefreshToken = &authToken.RefreshToken.Token memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) } + + go func() { + utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() } return res, nil diff --git a/server/resolvers/update_webhook.go b/server/resolvers/update_webhook.go index 5a7801c..4ce49d6 100644 --- a/server/resolvers/update_webhook.go +++ b/server/resolvers/update_webhook.go @@ -49,7 +49,9 @@ func UpdateWebhookResolver(ctx context.Context, params model.UpdateWebhookReques EndPoint: utils.StringValue(webhook.Endpoint), Enabled: utils.BoolValue(webhook.Enabled), Headers: headersString, + CreatedAt: *webhook.CreatedAt, } + if webhookDetails.EventName != utils.StringValue(params.EventName) { if isValid := validators.IsValidWebhookEventName(utils.StringValue(params.EventName)); !isValid { log.Debug("invalid event name: ", utils.StringValue(params.EventName)) diff --git a/server/resolvers/verify_email.go b/server/resolvers/verify_email.go index 6898932..624d08a 100644 --- a/server/resolvers/verify_email.go +++ b/server/resolvers/verify_email.go @@ -58,13 +58,17 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m return res, err } - // update email_verified_at in users table - now := time.Now().Unix() - user.EmailVerifiedAt = &now - user, err = db.Provider.UpdateUser(ctx, user) - if err != nil { - log.Debug("Failed to update user: ", err) - return res, err + isSignUp := false + if user.EmailVerifiedAt == nil { + isSignUp = true + // update email_verified_at in users table + now := time.Now().Unix() + user.EmailVerifiedAt = &now + user, err = db.Provider.UpdateUser(ctx, user) + if err != nil { + log.Debug("Failed to update user: ", err) + return res, err + } } // delete from verification table err = db.Provider.DeleteVerificationRequest(gc, verificationRequest) @@ -86,12 +90,19 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m return res, err } - go db.Provider.AddSession(ctx, models.Session{ - UserID: user.ID, - UserAgent: utils.GetUserAgent(gc.Request), - IP: utils.GetIP(gc.Request), - }) + go func() { + if isSignUp { + utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) + } else { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user) + } + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() if expiresIn <= 0 { expiresIn = 1 diff --git a/server/test/add_webhook_test.go b/server/test/add_webhook_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/add_webhook_test.go @@ -0,0 +1 @@ +package test diff --git a/server/test/delete_webhook_test.go b/server/test/delete_webhook_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/delete_webhook_test.go @@ -0,0 +1 @@ +package test diff --git a/server/test/test_endpoint_test.go b/server/test/test_endpoint_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/test_endpoint_test.go @@ -0,0 +1 @@ +package test diff --git a/server/test/update_webhook_test.go b/server/test/update_webhook_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/update_webhook_test.go @@ -0,0 +1 @@ +package test diff --git a/server/test/webhook_logs_test.go b/server/test/webhook_logs_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/webhook_logs_test.go @@ -0,0 +1 @@ +package test diff --git a/server/test/webhooks_test.go b/server/test/webhooks_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/webhooks_test.go @@ -0,0 +1 @@ +package test diff --git a/server/utils/webhook.go b/server/utils/webhook.go index d4b585b..cfd0b2c 100644 --- a/server/utils/webhook.go +++ b/server/utils/webhook.go @@ -1 +1,92 @@ package utils + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "time" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + log "github.com/sirupsen/logrus" +) + +func RegisterEvent(ctx context.Context, eventName string, authRecipe string, user models.User) error { + webhook, err := db.Provider.GetWebhookByEventName(ctx, eventName) + if err != nil { + return err + } + + userBytes, err := json.Marshal(user) + if err != nil { + log.Debug("error marshalling user obj: ", err) + return err + } + userMap := map[string]interface{}{} + err = json.Unmarshal(userBytes, &userMap) + if err != nil { + log.Debug("error un-marshalling user obj: ", err) + return err + } + + reqBody := map[string]interface{}{ + "event_name": eventName, + "user": userMap, + } + + if eventName == constants.UserLoginWebhookEvent || eventName == constants.UserSignUpWebhookEvent { + reqBody["auth_recipe"] = authRecipe + } + + requestBody, err := json.Marshal(reqBody) + if err != nil { + log.Debug("error marshalling requestBody obj: ", err) + return err + } + + requestBytesBuffer := bytes.NewBuffer(requestBody) + req, err := http.NewRequest("POST", StringValue(webhook.Endpoint), requestBytesBuffer) + if err != nil { + log.Debug("error creating webhook post request: ", err) + return err + } + req.Header.Set("Content-Type", "application/json") + + if webhook.Headers != nil { + for key, val := range webhook.Headers { + req.Header.Set(key, val.(string)) + } + } + + client := &http.Client{Timeout: time.Second * 30} + resp, err := client.Do(req) + if err != nil { + log.Debug("error making request: ", err) + return err + } + defer resp.Body.Close() + + responseBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Debug("error reading response: ", err) + return err + } + + statusCode := int64(resp.StatusCode) + _, err = db.Provider.AddWebhookLog(ctx, models.WebhookLog{ + HttpStatus: statusCode, + Request: string(requestBytesBuffer.Bytes()), + Response: string(responseBytes), + WebhookID: webhook.ID, + }) + + if err != nil { + log.Debug("failed to add webhook log: ", err) + return err + } + + return nil +}