From ec72adb2a6cd9f71bb91261f2975c3103237fe4c Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 14 Jul 2022 16:44:06 +0300 Subject: [PATCH] satellite/console: send email when user's account gets locked We send an email when user's account gets locked. Issue: https://github.com/storj/storj/issues/4967 Change-Id: I68beceda0ac09128755c0333dfa014bd5a186317 --- satellite/api.go | 7 + .../console/consoleweb/consoleapi/auth.go | 13 +- .../console/consoleweb/consoleql/keys.go | 21 ++ .../console/consoleweb/consoleql/mutation.go | 2 +- .../consoleweb/consoleql/mutation_test.go | 2 + .../consoleweb/consoleql/query_test.go | 2 + satellite/console/emailreminders/chore.go | 5 +- .../{consoleweb/consoleql => }/mail.go | 34 +-- satellite/console/service.go | 38 ++- satellite/console/service_test.go | 5 +- .../stripecoinpayments/accounts_test.go | 2 + web/satellite/static/emails/LockAccount.html | 282 ++++++++++++++++++ 12 files changed, 374 insertions(+), 39 deletions(-) create mode 100644 satellite/console/consoleweb/consoleql/keys.go rename satellite/console/{consoleweb/consoleql => }/mail.go (77%) create mode 100644 web/satellite/static/emails/LockAccount.html diff --git a/satellite/api.go b/satellite/api.go index fbf47ab31..207779844 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -564,6 +564,11 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, peer.Console.AuthTokens = consoleauth.NewService(config.ConsoleAuth, &consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)}) + externalAddress := consoleConfig.ExternalAddress + if externalAddress == "" { + externalAddress = "http://" + peer.Console.Listener.Addr().String() + } + peer.Console.Service, err = console.NewService( peer.Log.Named("console:service"), peer.DB.Console(), @@ -576,6 +581,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, peer.Payments.DepositWallets, peer.Analytics.Service, peer.Console.AuthTokens, + peer.Mail.Service, + externalAddress, consoleConfig.Config, ) if err != nil { diff --git a/satellite/console/consoleweb/consoleapi/auth.go b/satellite/console/consoleweb/consoleapi/auth.go index 2e89d085c..bef2dd314 100644 --- a/satellite/console/consoleweb/consoleapi/auth.go +++ b/satellite/console/consoleweb/consoleapi/auth.go @@ -20,7 +20,6 @@ import ( "storj.io/storj/private/web" "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console" - "storj.io/storj/satellite/console/consoleweb/consoleql" "storj.io/storj/satellite/console/consoleweb/consolewebauth" "storj.io/storj/satellite/mailservice" "storj.io/storj/satellite/rewards" @@ -232,7 +231,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: verified.Email}}, - &consoleql.AccountAlreadyExistsEmail{ + &console.AccountAlreadyExistsEmail{ Origin: satelliteAddress, SatelliteName: a.SatelliteName, SignInLink: satelliteAddress + "login", @@ -340,7 +339,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: user.Email, Name: userName}}, - &consoleql.AccountActivationEmail{ + &console.AccountActivationEmail{ ActivationLink: link, Origin: a.ExternalAddress, UserName: userName, @@ -527,7 +526,7 @@ func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: email, Name: ""}}, - &consoleql.UnknownResetPasswordEmail{ + &console.UnknownResetPasswordEmail{ Satellite: a.SatelliteName, Email: email, DoubleCheckLink: doubleCheckLink, @@ -559,7 +558,7 @@ func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: user.Email, Name: userName}}, - &consoleql.ForgotPasswordEmail{ + &console.ForgotPasswordEmail{ Origin: a.ExternalAddress, UserName: userName, ResetLink: passwordRecoveryLink, @@ -604,7 +603,7 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: verified.Email, Name: userName}}, - &consoleql.ForgotPasswordEmail{ + &console.ForgotPasswordEmail{ Origin: a.ExternalAddress, UserName: userName, ResetLink: a.PasswordRecoveryURL + "?token=" + recoveryToken, @@ -637,7 +636,7 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) { a.mailService.SendRenderedAsync( ctx, []post.Address{{Address: user.Email, Name: userName}}, - &consoleql.AccountActivationEmail{ + &console.AccountActivationEmail{ Origin: a.ExternalAddress, ActivationLink: link, TermsAndConditionsURL: termsAndConditionsURL, diff --git a/satellite/console/consoleweb/consoleql/keys.go b/satellite/console/consoleweb/consoleql/keys.go new file mode 100644 index 000000000..68b4fa880 --- /dev/null +++ b/satellite/console/consoleweb/consoleql/keys.go @@ -0,0 +1,21 @@ +// Copyright (C) 2022 Storj Labs, Inc. +// See LICENSE for copying information. + +package consoleql + +const ( + // ActivationPath is key for path which handles account activation. + ActivationPath = "activationPath" + // PasswordRecoveryPath is key for path which handles password recovery. + PasswordRecoveryPath = "passwordRecoveryPath" + // CancelPasswordRecoveryPath is key for path which handles let us know sequence. + CancelPasswordRecoveryPath = "cancelPasswordRecoveryPath" + // SignInPath is key for sign in server route. + SignInPath = "signInPath" + // LetUsKnowURL is key to store let us know URL. + LetUsKnowURL = "letUsKnowURL" + // ContactInfoURL is a key to store contact info URL. + ContactInfoURL = "contactInfoURL" + // TermsAndConditionsURL is a key to store terms and conditions URL. + TermsAndConditionsURL = "termsAndConditionsURL" +) diff --git a/satellite/console/consoleweb/consoleql/mutation.go b/satellite/console/consoleweb/consoleql/mutation.go index 4308ddd41..e334c689d 100644 --- a/satellite/console/consoleweb/consoleql/mutation.go +++ b/satellite/console/consoleweb/consoleql/mutation.go @@ -179,7 +179,7 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse mailService.SendRenderedAsync( p.Context, []post.Address{{Address: user.Email, Name: userName}}, - &ProjectInvitationEmail{ + &console.ProjectInvitationEmail{ Origin: origin, UserName: userName, ProjectName: project.Name, diff --git a/satellite/console/consoleweb/consoleql/mutation_test.go b/satellite/console/consoleweb/consoleql/mutation_test.go index dfa7da737..863556447 100644 --- a/satellite/console/consoleweb/consoleql/mutation_test.go +++ b/satellite/console/consoleweb/consoleql/mutation_test.go @@ -112,6 +112,8 @@ func TestGraphqlMutation(t *testing.T) { consoleauth.NewService(consoleauth.Config{ TokenExpirationTime: 24 * time.Hour, }, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}), + nil, + "", console.Config{ PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5, diff --git a/satellite/console/consoleweb/consoleql/query_test.go b/satellite/console/consoleweb/consoleql/query_test.go index 32d51a1ee..2fc2ceccf 100644 --- a/satellite/console/consoleweb/consoleql/query_test.go +++ b/satellite/console/consoleweb/consoleql/query_test.go @@ -96,6 +96,8 @@ func TestGraphqlQuery(t *testing.T) { consoleauth.NewService(consoleauth.Config{ TokenExpirationTime: 24 * time.Hour, }, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}), + nil, + "", console.Config{ PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5, diff --git a/satellite/console/emailreminders/chore.go b/satellite/console/emailreminders/chore.go index 5d9cef9e5..0d151ba67 100644 --- a/satellite/console/emailreminders/chore.go +++ b/satellite/console/emailreminders/chore.go @@ -16,7 +16,6 @@ import ( "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleauth" "storj.io/storj/satellite/console/consoleweb/consoleapi" - "storj.io/storj/satellite/console/consoleweb/consoleql" "storj.io/storj/satellite/mailservice" ) @@ -101,7 +100,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) { err = chore.mailService.SendRendered( ctx, []post.Address{{Address: u.Email, Name: userName}}, - &consoleql.AccountActivationEmail{ + &console.AccountActivationEmail{ ActivationLink: link, Origin: authController.ExternalAddress, UserName: userName, @@ -115,7 +114,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) { chore.mailService.SendRenderedAsync( ctx, []post.Address{{Address: u.Email, Name: userName}}, - &consoleql.AccountActivationEmail{ + &console.AccountActivationEmail{ ActivationLink: link, Origin: authController.ExternalAddress, UserName: userName, diff --git a/satellite/console/consoleweb/consoleql/mail.go b/satellite/console/mail.go similarity index 77% rename from satellite/console/consoleweb/consoleql/mail.go rename to satellite/console/mail.go index d1658e3fe..7ae4712dd 100644 --- a/satellite/console/consoleweb/consoleql/mail.go +++ b/satellite/console/mail.go @@ -1,24 +1,9 @@ -// Copyright (C) 2019 Storj Labs, Inc. +// Copyright (C) 2022 Storj Labs, Inc. // See LICENSE for copying information -package consoleql +package console -const ( - // ActivationPath is key for path which handles account activation. - ActivationPath = "activationPath" - // PasswordRecoveryPath is key for path which handles password recovery. - PasswordRecoveryPath = "passwordRecoveryPath" - // CancelPasswordRecoveryPath is key for path which handles let us know sequence. - CancelPasswordRecoveryPath = "cancelPasswordRecoveryPath" - // SignInPath is key for sign in server route. - SignInPath = "signInPath" - // LetUsKnowURL is key to store let us know URL. - LetUsKnowURL = "letUsKnowURL" - // ContactInfoURL is a key to store contact info URL. - ContactInfoURL = "contactInfoURL" - // TermsAndConditionsURL is a key to store terms and conditions URL. - TermsAndConditionsURL = "termsAndConditionsURL" -) +import "time" // AccountActivationEmail is mailservice template with activation data. type AccountActivationEmail struct { @@ -105,3 +90,16 @@ func (*AccountAlreadyExistsEmail) Template() string { return "AccountAlreadyExis func (*AccountAlreadyExistsEmail) Subject() string { return "Are you trying to sign in?" } + +// LockAccountEmail is mailservice template with lock account data. +type LockAccountEmail struct { + Name string + LockoutDuration time.Duration + ResetPasswordLink string +} + +// Template returns email template name. +func (*LockAccountEmail) Template() string { return "LockAccount" } + +// Subject gets email subject. +func (*LockAccountEmail) Subject() string { return "Account Lock" } diff --git a/satellite/console/service.go b/satellite/console/service.go index af7698ddc..416c0ee77 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -11,6 +11,7 @@ import ( "net/http" "net/mail" "sort" + "strings" "time" "github.com/spacemonkeygo/monkit/v3" @@ -27,9 +28,11 @@ import ( "storj.io/private/cfgstruct" "storj.io/storj/private/api" "storj.io/storj/private/blockchain" + "storj.io/storj/private/post" "storj.io/storj/satellite/accounting" "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console/consoleauth" + "storj.io/storj/satellite/mailservice" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments/monetary" "storj.io/storj/satellite/rewards" @@ -132,6 +135,9 @@ type Service struct { loginCaptchaHandler CaptchaHandler analytics *analytics.Service tokens *consoleauth.Service + mailService *mailservice.Service + + satelliteAddress string config Config } @@ -186,7 +192,7 @@ type Payments struct { } // NewService returns new instance of Service. -func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, depositWallets payments.DepositWallets, analytics *analytics.Service, tokens *consoleauth.Service, config Config) (*Service, error) { +func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, depositWallets payments.DepositWallets, analytics *analytics.Service, tokens *consoleauth.Service, mailService *mailservice.Service, satelliteAddress string, config Config) (*Service, error) { if store == nil { return nil, errs.New("store can't be nil") } @@ -229,6 +235,8 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting loginCaptchaHandler: loginCaptchaHandler, analytics: analytics, tokens: tokens, + mailService: mailService, + satelliteAddress: satelliteAddress, config: config, }, nil } @@ -993,9 +1001,8 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut return consoleauth.Token{}, ErrLockedAccount.New(lockedAccountErrMsg) } - lockoutExpDate := now.Add(time.Duration(math.Pow(s.config.FailedLoginPenalty, float64(user.FailedLoginCount-1))) * time.Minute) handleLockAccount := func() error { - err = s.UpdateUsersFailedLoginState(ctx, user, &lockoutExpDate) + err = s.UpdateUsersFailedLoginState(ctx, user) if err != nil { return err } @@ -1062,7 +1069,7 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut return consoleauth.Token{}, err } } else if request.MFAPasscode != "" { - valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, time.Now()) + valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, now) if err != nil { err = handleLockAccount() if err != nil { @@ -1109,10 +1116,29 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut } // UpdateUsersFailedLoginState updates User's failed login state. -func (s *Service) UpdateUsersFailedLoginState(ctx context.Context, user *User, lockoutExpDate *time.Time) error { +func (s *Service) UpdateUsersFailedLoginState(ctx context.Context, user *User) error { updateRequest := UpdateUserRequest{} if user.FailedLoginCount >= s.config.LoginAttemptsWithoutPenalty-1 { - updateRequest.LoginLockoutExpiration = &lockoutExpDate + lockoutDuration := time.Duration(math.Pow(s.config.FailedLoginPenalty, float64(user.FailedLoginCount-1))) * time.Minute + lockoutExpTime := time.Now().Add(lockoutDuration) + lockoutExpTimePtr := &lockoutExpTime + + updateRequest.LoginLockoutExpiration = &lockoutExpTimePtr + + address := s.satelliteAddress + if !strings.HasSuffix(address, "/") { + address += "/" + } + + s.mailService.SendRenderedAsync( + ctx, + []post.Address{{Address: user.Email, Name: user.FullName}}, + &LockAccountEmail{ + Name: user.FullName, + LockoutDuration: lockoutDuration, + ResetPasswordLink: address + "forgot-password", + }, + ) } user.FailedLoginCount++ diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index e45a834ef..7f19c9bc0 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -7,7 +7,6 @@ import ( "context" "database/sql" "encoding/json" - "math" "math/big" "testing" "time" @@ -838,9 +837,7 @@ func TestLockAccount(t *testing.T) { require.True(t, lockedUser.LoginLockoutExpiration.After(now)) // lock account once again and check if lockout expiration time increased. - expDuration := time.Duration(math.Pow(consoleConfig.FailedLoginPenalty, float64(lockedUser.FailedLoginCount-1))) * time.Minute - lockoutExpDate := now.Add(expDuration) - err = service.UpdateUsersFailedLoginState(userCtx, lockedUser, &lockoutExpDate) + err = service.UpdateUsersFailedLoginState(userCtx, lockedUser) require.NoError(t, err) lockedUser, err = service.GetUser(userCtx, user.ID) diff --git a/satellite/payments/stripecoinpayments/accounts_test.go b/satellite/payments/stripecoinpayments/accounts_test.go index 5b8d995bc..fb4cac909 100644 --- a/satellite/payments/stripecoinpayments/accounts_test.go +++ b/satellite/payments/stripecoinpayments/accounts_test.go @@ -90,6 +90,8 @@ func TestSignupCouponCodes(t *testing.T) { consoleauth.NewService(consoleauth.Config{ TokenExpirationTime: 24 * time.Hour, }, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}), + nil, + "", console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5}, ) diff --git a/web/satellite/static/emails/LockAccount.html b/web/satellite/static/emails/LockAccount.html new file mode 100644 index 000000000..bc922520f --- /dev/null +++ b/web/satellite/static/emails/LockAccount.html @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +