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
This commit is contained in:
parent
5a2e348b06
commit
ec72adb2a6
@ -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)})
|
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.Console.Service, err = console.NewService(
|
||||||
peer.Log.Named("console:service"),
|
peer.Log.Named("console:service"),
|
||||||
peer.DB.Console(),
|
peer.DB.Console(),
|
||||||
@ -576,6 +581,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
peer.Payments.DepositWallets,
|
peer.Payments.DepositWallets,
|
||||||
peer.Analytics.Service,
|
peer.Analytics.Service,
|
||||||
peer.Console.AuthTokens,
|
peer.Console.AuthTokens,
|
||||||
|
peer.Mail.Service,
|
||||||
|
externalAddress,
|
||||||
consoleConfig.Config,
|
consoleConfig.Config,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"storj.io/storj/private/web"
|
"storj.io/storj/private/web"
|
||||||
"storj.io/storj/satellite/analytics"
|
"storj.io/storj/satellite/analytics"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
|
||||||
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
||||||
"storj.io/storj/satellite/mailservice"
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/rewards"
|
"storj.io/storj/satellite/rewards"
|
||||||
@ -232,7 +231,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: verified.Email}},
|
[]post.Address{{Address: verified.Email}},
|
||||||
&consoleql.AccountAlreadyExistsEmail{
|
&console.AccountAlreadyExistsEmail{
|
||||||
Origin: satelliteAddress,
|
Origin: satelliteAddress,
|
||||||
SatelliteName: a.SatelliteName,
|
SatelliteName: a.SatelliteName,
|
||||||
SignInLink: satelliteAddress + "login",
|
SignInLink: satelliteAddress + "login",
|
||||||
@ -340,7 +339,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: user.Email, Name: userName}},
|
[]post.Address{{Address: user.Email, Name: userName}},
|
||||||
&consoleql.AccountActivationEmail{
|
&console.AccountActivationEmail{
|
||||||
ActivationLink: link,
|
ActivationLink: link,
|
||||||
Origin: a.ExternalAddress,
|
Origin: a.ExternalAddress,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
@ -527,7 +526,7 @@ func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: email, Name: ""}},
|
[]post.Address{{Address: email, Name: ""}},
|
||||||
&consoleql.UnknownResetPasswordEmail{
|
&console.UnknownResetPasswordEmail{
|
||||||
Satellite: a.SatelliteName,
|
Satellite: a.SatelliteName,
|
||||||
Email: email,
|
Email: email,
|
||||||
DoubleCheckLink: doubleCheckLink,
|
DoubleCheckLink: doubleCheckLink,
|
||||||
@ -559,7 +558,7 @@ func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: user.Email, Name: userName}},
|
[]post.Address{{Address: user.Email, Name: userName}},
|
||||||
&consoleql.ForgotPasswordEmail{
|
&console.ForgotPasswordEmail{
|
||||||
Origin: a.ExternalAddress,
|
Origin: a.ExternalAddress,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
ResetLink: passwordRecoveryLink,
|
ResetLink: passwordRecoveryLink,
|
||||||
@ -604,7 +603,7 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: verified.Email, Name: userName}},
|
[]post.Address{{Address: verified.Email, Name: userName}},
|
||||||
&consoleql.ForgotPasswordEmail{
|
&console.ForgotPasswordEmail{
|
||||||
Origin: a.ExternalAddress,
|
Origin: a.ExternalAddress,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
ResetLink: a.PasswordRecoveryURL + "?token=" + recoveryToken,
|
ResetLink: a.PasswordRecoveryURL + "?token=" + recoveryToken,
|
||||||
@ -637,7 +636,7 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mailService.SendRenderedAsync(
|
a.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: user.Email, Name: userName}},
|
[]post.Address{{Address: user.Email, Name: userName}},
|
||||||
&consoleql.AccountActivationEmail{
|
&console.AccountActivationEmail{
|
||||||
Origin: a.ExternalAddress,
|
Origin: a.ExternalAddress,
|
||||||
ActivationLink: link,
|
ActivationLink: link,
|
||||||
TermsAndConditionsURL: termsAndConditionsURL,
|
TermsAndConditionsURL: termsAndConditionsURL,
|
||||||
|
21
satellite/console/consoleweb/consoleql/keys.go
Normal file
21
satellite/console/consoleweb/consoleql/keys.go
Normal file
@ -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"
|
||||||
|
)
|
@ -179,7 +179,7 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
|
|||||||
mailService.SendRenderedAsync(
|
mailService.SendRenderedAsync(
|
||||||
p.Context,
|
p.Context,
|
||||||
[]post.Address{{Address: user.Email, Name: userName}},
|
[]post.Address{{Address: user.Email, Name: userName}},
|
||||||
&ProjectInvitationEmail{
|
&console.ProjectInvitationEmail{
|
||||||
Origin: origin,
|
Origin: origin,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
ProjectName: project.Name,
|
ProjectName: project.Name,
|
||||||
|
@ -112,6 +112,8 @@ func TestGraphqlMutation(t *testing.T) {
|
|||||||
consoleauth.NewService(consoleauth.Config{
|
consoleauth.NewService(consoleauth.Config{
|
||||||
TokenExpirationTime: 24 * time.Hour,
|
TokenExpirationTime: 24 * time.Hour,
|
||||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
console.Config{
|
console.Config{
|
||||||
PasswordCost: console.TestPasswordCost,
|
PasswordCost: console.TestPasswordCost,
|
||||||
DefaultProjectLimit: 5,
|
DefaultProjectLimit: 5,
|
||||||
|
@ -96,6 +96,8 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
consoleauth.NewService(consoleauth.Config{
|
consoleauth.NewService(consoleauth.Config{
|
||||||
TokenExpirationTime: 24 * time.Hour,
|
TokenExpirationTime: 24 * time.Hour,
|
||||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
console.Config{
|
console.Config{
|
||||||
PasswordCost: console.TestPasswordCost,
|
PasswordCost: console.TestPasswordCost,
|
||||||
DefaultProjectLimit: 5,
|
DefaultProjectLimit: 5,
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleauth"
|
"storj.io/storj/satellite/console/consoleauth"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleapi"
|
"storj.io/storj/satellite/console/consoleweb/consoleapi"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
|
||||||
"storj.io/storj/satellite/mailservice"
|
"storj.io/storj/satellite/mailservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
|
|||||||
err = chore.mailService.SendRendered(
|
err = chore.mailService.SendRendered(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: u.Email, Name: userName}},
|
[]post.Address{{Address: u.Email, Name: userName}},
|
||||||
&consoleql.AccountActivationEmail{
|
&console.AccountActivationEmail{
|
||||||
ActivationLink: link,
|
ActivationLink: link,
|
||||||
Origin: authController.ExternalAddress,
|
Origin: authController.ExternalAddress,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
@ -115,7 +114,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
|
|||||||
chore.mailService.SendRenderedAsync(
|
chore.mailService.SendRenderedAsync(
|
||||||
ctx,
|
ctx,
|
||||||
[]post.Address{{Address: u.Email, Name: userName}},
|
[]post.Address{{Address: u.Email, Name: userName}},
|
||||||
&consoleql.AccountActivationEmail{
|
&console.AccountActivationEmail{
|
||||||
ActivationLink: link,
|
ActivationLink: link,
|
||||||
Origin: authController.ExternalAddress,
|
Origin: authController.ExternalAddress,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
|
@ -1,24 +1,9 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information
|
// See LICENSE for copying information
|
||||||
|
|
||||||
package consoleql
|
package console
|
||||||
|
|
||||||
const (
|
import "time"
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccountActivationEmail is mailservice template with activation data.
|
// AccountActivationEmail is mailservice template with activation data.
|
||||||
type AccountActivationEmail struct {
|
type AccountActivationEmail struct {
|
||||||
@ -105,3 +90,16 @@ func (*AccountAlreadyExistsEmail) Template() string { return "AccountAlreadyExis
|
|||||||
func (*AccountAlreadyExistsEmail) Subject() string {
|
func (*AccountAlreadyExistsEmail) Subject() string {
|
||||||
return "Are you trying to sign in?"
|
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" }
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spacemonkeygo/monkit/v3"
|
"github.com/spacemonkeygo/monkit/v3"
|
||||||
@ -27,9 +28,11 @@ import (
|
|||||||
"storj.io/private/cfgstruct"
|
"storj.io/private/cfgstruct"
|
||||||
"storj.io/storj/private/api"
|
"storj.io/storj/private/api"
|
||||||
"storj.io/storj/private/blockchain"
|
"storj.io/storj/private/blockchain"
|
||||||
|
"storj.io/storj/private/post"
|
||||||
"storj.io/storj/satellite/accounting"
|
"storj.io/storj/satellite/accounting"
|
||||||
"storj.io/storj/satellite/analytics"
|
"storj.io/storj/satellite/analytics"
|
||||||
"storj.io/storj/satellite/console/consoleauth"
|
"storj.io/storj/satellite/console/consoleauth"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/payments"
|
"storj.io/storj/satellite/payments"
|
||||||
"storj.io/storj/satellite/payments/monetary"
|
"storj.io/storj/satellite/payments/monetary"
|
||||||
"storj.io/storj/satellite/rewards"
|
"storj.io/storj/satellite/rewards"
|
||||||
@ -132,6 +135,9 @@ type Service struct {
|
|||||||
loginCaptchaHandler CaptchaHandler
|
loginCaptchaHandler CaptchaHandler
|
||||||
analytics *analytics.Service
|
analytics *analytics.Service
|
||||||
tokens *consoleauth.Service
|
tokens *consoleauth.Service
|
||||||
|
mailService *mailservice.Service
|
||||||
|
|
||||||
|
satelliteAddress string
|
||||||
|
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
@ -186,7 +192,7 @@ type Payments struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns new instance of Service.
|
// 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 {
|
if store == nil {
|
||||||
return nil, errs.New("store can't be 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,
|
loginCaptchaHandler: loginCaptchaHandler,
|
||||||
analytics: analytics,
|
analytics: analytics,
|
||||||
tokens: tokens,
|
tokens: tokens,
|
||||||
|
mailService: mailService,
|
||||||
|
satelliteAddress: satelliteAddress,
|
||||||
config: config,
|
config: config,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -993,9 +1001,8 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut
|
|||||||
return consoleauth.Token{}, ErrLockedAccount.New(lockedAccountErrMsg)
|
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 {
|
handleLockAccount := func() error {
|
||||||
err = s.UpdateUsersFailedLoginState(ctx, user, &lockoutExpDate)
|
err = s.UpdateUsersFailedLoginState(ctx, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1062,7 +1069,7 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut
|
|||||||
return consoleauth.Token{}, err
|
return consoleauth.Token{}, err
|
||||||
}
|
}
|
||||||
} else if request.MFAPasscode != "" {
|
} else if request.MFAPasscode != "" {
|
||||||
valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, time.Now())
|
valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = handleLockAccount()
|
err = handleLockAccount()
|
||||||
if err != nil {
|
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.
|
// 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{}
|
updateRequest := UpdateUserRequest{}
|
||||||
if user.FailedLoginCount >= s.config.LoginAttemptsWithoutPenalty-1 {
|
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++
|
user.FailedLoginCount++
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -838,9 +837,7 @@ func TestLockAccount(t *testing.T) {
|
|||||||
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
|
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
|
||||||
|
|
||||||
// lock account once again and check if lockout expiration time increased.
|
// lock account once again and check if lockout expiration time increased.
|
||||||
expDuration := time.Duration(math.Pow(consoleConfig.FailedLoginPenalty, float64(lockedUser.FailedLoginCount-1))) * time.Minute
|
err = service.UpdateUsersFailedLoginState(userCtx, lockedUser)
|
||||||
lockoutExpDate := now.Add(expDuration)
|
|
||||||
err = service.UpdateUsersFailedLoginState(userCtx, lockedUser, &lockoutExpDate)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
lockedUser, err = service.GetUser(userCtx, user.ID)
|
lockedUser, err = service.GetUser(userCtx, user.ID)
|
||||||
|
@ -90,6 +90,8 @@ func TestSignupCouponCodes(t *testing.T) {
|
|||||||
consoleauth.NewService(consoleauth.Config{
|
consoleauth.NewService(consoleauth.Config{
|
||||||
TokenExpirationTime: 24 * time.Hour,
|
TokenExpirationTime: 24 * time.Hour,
|
||||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5},
|
console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
282
web/satellite/static/emails/LockAccount.html
Normal file
282
web/satellite/static/emails/LockAccount.html
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings></xml>
|
||||||
|
<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<title></title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Poppins:400,700&display=swap" rel="stylesheet">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td,
|
||||||
|
tr {
|
||||||
|
vertical-align: top;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[x-apple-data-detectors=true] {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im {
|
||||||
|
color: #56606D;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css" id="media-query">
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
|
||||||
|
.block-grid,
|
||||||
|
.col {
|
||||||
|
min-width: 320px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-grid {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col>div {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col {
|
||||||
|
min-width: 0 !important;
|
||||||
|
display: table-cell !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack.two-up .col {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num4 {
|
||||||
|
width: 33% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num8 {
|
||||||
|
width: 66% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num4 {
|
||||||
|
width: 33% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num3 {
|
||||||
|
width: 25% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num6 {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-stack .col.num9 {
|
||||||
|
width: 75% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Poppins:400,500,700,900|Roboto:100,300,500,700&display=swap');
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;">
|
||||||
|
<!--[if IE]><div class="ie-browser"><![endif]-->
|
||||||
|
<table class="nl-container"
|
||||||
|
style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0;
|
||||||
|
border-collapse: collapse; mso-table-lspace: 0; mso-table-rspace: 0; background-color: #FFFFFF; width: 100%;"
|
||||||
|
cellpadding="0" cellspacing="0" role="presentation" width="100%" bgcolor="#FFFFFF" valign="top">
|
||||||
|
<tbody>
|
||||||
|
<tr style="vertical-align: top;" valign="top">
|
||||||
|
<td style="word-break: break-word; vertical-align: top;" valign="top">
|
||||||
|
<!--[if (mso)|(IE)]>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td align="center" style="background-color:#FFFFFF">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="background-color:#FFFFFF;">
|
||||||
|
<div class="block-grid "
|
||||||
|
style="Margin: 0 auto; min-width: 320px; max-width: 520px; overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word; word-break: break-word; background-color: #FFFFFF;">
|
||||||
|
<div style="border-collapse: collapse;display: table;width: 100%;background-color:#FFFFFF;">
|
||||||
|
<!--[if (mso)|(IE)]>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#FFFFFF;">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" style="width:520px">
|
||||||
|
<tr class="layout-full-width" style="background-color:#FFFFFF">
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]>
|
||||||
|
<td align="center" width="520" style="background-color:#FFFFFF;width:520px;
|
||||||
|
border-top: 0px solid #000000; border-left: 0px solid #000000;
|
||||||
|
border-bottom: 0px solid #000000; border-right: 0px solid #000000;" valign="top">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td style="padding:10px 15px 0 15px;background-color:#FFFFFF;">
|
||||||
|
<![endif]-->
|
||||||
|
<div class="col num12"
|
||||||
|
style="min-width: 320px; max-width: 520px; display: table-cell; vertical-align: top; width: 520px;">
|
||||||
|
<div style="background-color:#FFFFFF;width:100% !important;">
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
<div style="border-top:0px solid #000000; border-left:0px solid #000000;
|
||||||
|
border-bottom:0px solid #000000; border-right:0px solid #000000; padding: 10px">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<div>
|
||||||
|
<h1 style="font-family: sans-serif; text-align: left;
|
||||||
|
color: #000; font-weight: bold; font-size: 36px; line-height: 47px;">
|
||||||
|
Your account was locked...
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td style="padding: 10px 10px 0 10px;font-family: Tahoma, Verdana, sans-serif">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="color:#000000;font-family:sans-serif;
|
||||||
|
line-height:1.2;">
|
||||||
|
<div style="font-family: sans-serif; line-height: 1.2; font-size: 12px; color: #000000; mso-line-height-alt: 14px;">
|
||||||
|
<p style="color: #56606D; font-size: 16px; line-height: 24px; margin: 0 0 15px 0;">
|
||||||
|
Hi {{ .Name }},<br/>
|
||||||
|
Your account was locked due to too many failed login attempts. <br/><br/>
|
||||||
|
If this was you, try again in {{ .LockoutDuration }}, or
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<a
|
||||||
|
href="{{ .ResetPasswordLink }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style="border-radius: 4px; display: inline-block; font-size: 14px; font-weight: bold;
|
||||||
|
line-height: 24px;padding: 12px 24px; text-align: center;
|
||||||
|
text-decoration: none !important; transition: opacity 0.1s ease-in;
|
||||||
|
color: #ffffff !important; background-color: #2683ff;
|
||||||
|
font-family: Montserrat, DejaVu Sans, Verdana, sans-serif;"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
<p style="color: #56606D; font-size: 16px; line-height: 24px; margin: 25px 0 0 0;">
|
||||||
|
If this login activity doesn't look familiar, please consider updating
|
||||||
|
your password and enabling multi-factor authentication for your account
|
||||||
|
if you haven't already.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
</div>
|
||||||
|
<!--<![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="background-color:transparent;">
|
||||||
|
<div class="block-grid " style="Margin: 0 auto; min-width: 320px; max-width: 520px; overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word; word-break: break-word; background-color: transparent;">
|
||||||
|
<div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
|
||||||
|
<!--[if (mso)|(IE)]>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0"
|
||||||
|
style="background-color:transparent;">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" style="width:520px">
|
||||||
|
<tr class="layout-full-width" style="background-color:transparent">
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]>
|
||||||
|
<td align="center"
|
||||||
|
style="background-color:transparent;width:520px; border-top: 0px solid transparent;
|
||||||
|
border-left: 0px solid transparent; border-bottom: 0px solid transparent;
|
||||||
|
border-right: 0px solid transparent;" valign="top">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td style="padding:20px 0 5px 0">
|
||||||
|
<![endif]-->
|
||||||
|
<div class="col num12" style="min-width: 320px; max-width: 520px; display: table-cell;
|
||||||
|
vertical-align: top; width: 520px;padding: 10px">
|
||||||
|
<div style="width:100% !important;">
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
<div style="border-top:0px solid transparent; border-left:0px solid transparent;
|
||||||
|
border-bottom:0px solid transparent; border-right:0px solid transparent;
|
||||||
|
padding:0 0 5px 0">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<table class="divider" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||||
|
style="table-layout: fixed; vertical-align: top; border-spacing: 0;
|
||||||
|
border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt;
|
||||||
|
min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
|
||||||
|
role="presentation" valign="top">
|
||||||
|
<tbody>
|
||||||
|
<tr style="vertical-align: top;" valign="top">
|
||||||
|
<td class="divider_inner" style="word-break: break-word; vertical-align: top;
|
||||||
|
min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
|
||||||
|
padding: 10px 0 40px 0;" valign="top">
|
||||||
|
<table class="divider_content" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
width="100%" style="table-layout: fixed; vertical-align: top;
|
||||||
|
border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt; border-top: 1px solid #BBBBBB; height: 0px;
|
||||||
|
width: 100%;" align="center" role="presentation" height="0"
|
||||||
|
valign="top">
|
||||||
|
<tbody>
|
||||||
|
<tr style="vertical-align: top;" valign="top">
|
||||||
|
<td style="word-break: break-word; vertical-align: top;
|
||||||
|
-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
|
||||||
|
height="0" valign="top">
|
||||||
|
<span></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="size-12" style="margin: 0; color: #56606D;
|
||||||
|
font-family: sans-serif;font-size: 12px;
|
||||||
|
line-height: 19px;" lang="x-size-12">
|
||||||
|
<span>Please do not reply to this email.<br />
|
||||||
|
1450 W. Peachtree St. NW #200, PMB 75268, Atlanta, GA 30309-2955, United States
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<!--[if mso]>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td style="padding:10px; font-family: Arial, sans-serif">
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
</div>
|
||||||
|
<!--<![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if (IE)]></div><![endif]-->
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user