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)})
|
||||
|
||||
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 {
|
||||
|
@ -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,
|
||||
|
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(
|
||||
p.Context,
|
||||
[]post.Address{{Address: user.Email, Name: userName}},
|
||||
&ProjectInvitationEmail{
|
||||
&console.ProjectInvitationEmail{
|
||||
Origin: origin,
|
||||
UserName: userName,
|
||||
ProjectName: project.Name,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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" }
|
@ -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++
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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},
|
||||
)
|
||||
|
||||
|
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