satellite/console/consoleweb: send email when account already exists

When a someone tries to create an account with an email that is already
associated with a verified account, send them an email with options to
sign in, create an account on another satellite, or reset password.

Change-Id: I844144d88b7356bd7064c4840c9441347a5368b0
This commit is contained in:
Cameron 2022-07-01 13:31:14 -04:00 committed by Cameron
parent 0eb2cc4511
commit b4ea1bac42
6 changed files with 604 additions and 89 deletions

View File

@ -170,17 +170,6 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
return
}
var userID uuid.UUID
defer func() {
if err == nil {
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(userID)
if err != nil {
a.log.Error("registration handler could not encode userID", zap.Error(ErrAuthAPI.Wrap(err)))
}
}
}()
var registerData struct {
FullName string `json:"fullName"`
ShortName string `json:"shortName"`
@ -226,31 +215,21 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
}
if verified != nil {
recoveryToken, err := a.service.GeneratePasswordRecoveryToken(ctx, verified.ID)
if err != nil {
a.serveJSONError(w, err)
return
satelliteAddress := a.ExternalAddress
if !strings.HasSuffix(satelliteAddress, "/") {
satelliteAddress += "/"
}
userName := verified.ShortName
if verified.ShortName == "" {
userName = verified.FullName
}
a.mailService.SendRenderedAsync(
ctx,
[]post.Address{{Address: verified.Email, Name: userName}},
&consoleql.ForgotPasswordEmail{
Origin: a.ExternalAddress,
UserName: userName,
ResetLink: a.PasswordRecoveryURL + "?token=" + recoveryToken,
CancelPasswordRecoveryLink: a.CancelPasswordRecoveryURL + "?token=" + recoveryToken,
LetUsKnowURL: a.LetUsKnowURL,
ContactInfoURL: a.ContactInfoURL,
TermsAndConditionsURL: a.TermsAndConditionsURL,
[]post.Address{{Address: verified.Email}},
&consoleql.AccountAlreadyExistsEmail{
Origin: satelliteAddress,
SatelliteName: a.SatelliteName,
SignInLink: satelliteAddress + "login",
ResetPasswordLink: satelliteAddress + "forgot-password",
CreateAccountLink: satelliteAddress + "signup",
},
)
userID = verified.ID
return
}
@ -334,7 +313,6 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
}
a.analytics.TrackCreateUser(trackCreateUserFields)
}
userID = user.ID
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
if err != nil {

View File

@ -26,7 +26,6 @@ import (
"go.uber.org/zap"
"storj.io/common/testcontext"
"storj.io/common/uuid"
"storj.io/storj/private/post"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
@ -97,16 +96,9 @@ func TestAuth_Register(t *testing.T) {
}()
require.Equal(t, http.StatusOK, result.StatusCode)
body, err := ioutil.ReadAll(result.Body)
_, users, err := planet.Satellites[0].API.Console.Service.GetUserByEmailWithUnverified(ctx, registerData.Email)
require.NoError(t, err)
var userID uuid.UUID
err = json.Unmarshal(body, &userID)
require.NoError(t, err)
user, err := planet.Satellites[0].API.Console.Service.GetUser(ctx, userID)
require.NoError(t, err)
require.Equal(t, []byte(test.Partner), user.UserAgent)
require.Equal(t, []byte(test.Partner), users[0].UserAgent)
}()
}
})
@ -122,8 +114,9 @@ func TestAuth_Register_CORS(t *testing.T) {
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
jsonBody := []byte(`{"email":"user@test.com","fullName":"testuser","password":"abc123","shortName":"test"}`)
email := "user@test.com"
fullName := "testuser"
jsonBody := []byte(fmt.Sprintf(`{"email":"%s","fullName":"%s","password":"abc123","shortName":"test"}`, email, fullName))
url := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/register"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody))
require.NoError(t, err)
@ -198,15 +191,9 @@ func TestAuth_Register_CORS(t *testing.T) {
"Authorization",
})
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var userID uuid.UUID
err = json.Unmarshal(body, &userID)
require.NoError(t, err)
_, err = planet.Satellites[0].API.Console.Service.GetUser(ctx, userID)
_, users, err := planet.Satellites[0].API.Console.Service.GetUserByEmailWithUnverified(ctx, email)
require.NoError(t, err)
require.Equal(t, fullName, users[0].FullName)
})
}
@ -647,16 +634,16 @@ func TestRegistrationEmail(t *testing.T) {
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
email := "test@mail.test"
jsonBody, err := json.Marshal(map[string]interface{}{
"fullName": "Test User",
"shortName": "Test",
"email": "test@mail.test",
"email": email,
"password": "123a123",
})
require.NoError(t, err)
register := func() string {
register := func() {
url := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/register"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody))
require.NoError(t, err)
@ -665,46 +652,39 @@ func TestRegistrationEmail(t *testing.T) {
result, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, result.StatusCode)
var userID string
require.NoError(t, json.NewDecoder(result.Body).Decode(&userID))
require.NoError(t, result.Body.Close())
return userID
}
sender := &EmailVerifier{Context: ctx}
sat.API.Mail.Service.Sender = sender
// Registration attempts using new e-mail address should send activation e-mail.
userID := register()
register()
body, err := sender.Data.Get(ctx)
require.NoError(t, err)
require.Contains(t, body, "/activation")
// Registration attempts using existing but unverified e-mail address should send activation e-mail.
newUserID := register()
require.Equal(t, userID, newUserID)
register()
body, err = sender.Data.Get(ctx)
require.NoError(t, err)
require.Contains(t, body, "/activation")
// Registration attempts using existing and verified e-mail address should send password reset e-mail.
userUUID, err := uuid.FromString(userID)
require.NoError(t, err)
user, err := sat.DB.Console().Users().Get(ctx, userUUID)
// Registration attempts using existing and verified e-mail address should send account already exists e-mail.
_, users, err := sat.DB.Console().Users().GetByEmailWithUnverified(ctx, email)
require.NoError(t, err)
user.Status = console.Active
require.NoError(t, sat.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &user.Status,
users[0].Status = console.Active
require.NoError(t, sat.DB.Console().Users().Update(ctx, users[0].ID, console.UpdateUserRequest{
Status: &users[0].Status,
}))
newUserID = register()
require.Equal(t, userID, newUserID)
register()
body, err = sender.Data.Get(ctx)
require.NoError(t, err)
require.Contains(t, body, "/password-recovery")
require.Contains(t, body, "/login")
require.Contains(t, body, "/forgot-password")
require.Contains(t, body, "/signup")
})
}
@ -760,7 +740,7 @@ func TestAuth_Register_NameSpecialChars(t *testing.T) {
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
inputName := "The website has been changed to https://evil.com/login.html - Enter Login Details,"
filteredName := "The website has been changed to https---evil-com-login-html - Enter Login Details,"
email := "user@mail.test"
registerData := struct {
FullName string `json:"fullName"`
ShortName string `json:"shortName"`
@ -769,7 +749,7 @@ func TestAuth_Register_NameSpecialChars(t *testing.T) {
}{
FullName: inputName,
ShortName: inputName,
Email: "user@mail.test",
Email: email,
Password: "abc123",
}
@ -788,17 +768,10 @@ func TestAuth_Register_NameSpecialChars(t *testing.T) {
}()
require.Equal(t, http.StatusOK, result.StatusCode)
body, err := ioutil.ReadAll(result.Body)
_, users, err := planet.Satellites[0].API.Console.Service.GetUserByEmailWithUnverified(ctx, email)
require.NoError(t, err)
var userID uuid.UUID
err = json.Unmarshal(body, &userID)
require.NoError(t, err)
user, err := planet.Satellites[0].API.Console.Service.GetUser(ctx, userID)
require.NoError(t, err)
require.Equal(t, filteredName, user.FullName)
require.Equal(t, filteredName, user.ShortName)
require.Equal(t, filteredName, users[0].FullName)
require.Equal(t, filteredName, users[0].ShortName)
})
}

View File

@ -88,3 +88,20 @@ func (*UnknownResetPasswordEmail) Template() string { return "UnknownReset" }
func (*UnknownResetPasswordEmail) Subject() string {
return "You have requested to reset your password, but..."
}
// AccountAlreadyExistsEmail is mailservice template for email where user tries to create account, but one already exists.
type AccountAlreadyExistsEmail struct {
Origin string
SatelliteName string
SignInLink string
ResetPasswordLink string
CreateAccountLink string
}
// Template returns email template name.
func (*AccountAlreadyExistsEmail) Template() string { return "AccountAlreadyExists" }
// Subject gets email subject.
func (*AccountAlreadyExistsEmail) Subject() string {
return "Are you trying to sign in?"
}

View File

@ -872,7 +872,6 @@ func (test *test) registerUser(email, password string) registeredUser {
}))
require.Equal(test.t, http.StatusOK, resp.StatusCode)
require.NotEmpty(test.t, body)
time.Sleep(time.Second) // TODO: hack-fix, register activates account asynchronously

View File

@ -243,7 +243,7 @@ export class AuthHttpApi implements UsersApi {
* @returns id of created user
* @throws Error
*/
public async register(user: Partial<User>, secret: string, captchaResponse: string): Promise<string> {
public async register(user: Partial<User>, secret: string, captchaResponse: string): Promise<void> {
const path = `${this.ROOT_PATH}/register`;
const body = {
secret: secret,
@ -263,8 +263,8 @@ export class AuthHttpApi implements UsersApi {
};
const response = await this.http.post(path, JSON.stringify(body));
const result = await response.json();
if (!response.ok) {
const result = await response.json();
const errMsg = result.error || 'Cannot register user';
switch (response.status) {
case 400:
@ -277,7 +277,6 @@ export class AuthHttpApi implements UsersApi {
throw new Error(errMsg);
}
}
return result;
}
/**

View File

@ -0,0 +1,549 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!--[if IE]><html xmlns="http://www.w3.org/1999/xhtml" class="ie"><![endif]--><!--[if !IE]><!-->
<html style="margin: 0;padding: 0;" xmlns="http://www.w3.org/1999/xhtml"><!--<![endif]--><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge" /><!--<![endif]-->
<meta name="viewport" content="width=device-width" /><style type="text/css">
@media only screen and (min-width: 620px) {
.wrapper {
min-width: 600px !important
}
.wrapper h1 {}
.wrapper h1 {
font-size: 64px !important;
line-height: 63px !important
}
.wrapper h2 {}
.wrapper h2 {
font-size: 30px !important;
line-height: 38px !important
}
.wrapper h3 {}
.wrapper h3 {
font-size: 22px !important;
line-height: 31px !important
}
.wrapper .size-10 {
font-size: 10px !important;
line-height: 18px !important
}
.wrapper .size-12 {
font-size: 12px !important;
line-height: 19px !important
}
.wrapper .size-16 {
font-size: 16px !important;
line-height: 24px !important
}
.wrapper .size-20 {
font-size: 20px !important;
line-height: 28px !important
}
.wrapper .size-40 {
font-size: 40px !important;
line-height: 47px !important
}
}
</style>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
table-layout: fixed;
}
* {
line-height: inherit;
}
[x-apple-data-detectors],
[href^="tel"],
[href^="sms"] {
color: inherit !important;
text-decoration: none !important;
}
.wrapper .footer__share-button a:hover,
.wrapper .footer__share-button a:focus {
color: #ffffff !important;
}
.btn a:hover,
.btn a:focus,
.footer__share-button a:hover,
.footer__share-button a:focus,
.email-footer__links a:hover,
.email-footer__links a:focus {
opacity: 0.8;
}
.layout,
.column {
transition: width 0.25s ease-in-out, max-width 0.25s ease-in-out;
}
.preheader td {
padding-bottom: 8px;
}
.layout {
max-width: 400px !important;
-fallback-width: 95% !important;
width: calc(100% - 20px) !important;
}
.column {
max-width: 400px !important;
width: 100% !important;
}
.ie {
width: 100%;
}
[owa] .column div,
[owa] .column button {
display: block !important;
}
.ie .column,
[owa] .column,
.ie {
display: table-cell;
float: none !important;
vertical-align: top;
}
.ie .layout,
[owa] .layout,
.ie .one-col .column,
[owa] .one-col .column {
max-width: 600px !important;
width: 600px !important;
}
.ie .two-col .column,
[owa] .two-col .column {
max-width: 300px !important;
width: 300px !important;
}
.ie .three-col .column,
[owa] .three-col .column {
max-width: 200px !important;
width: 200px !important;
}
.ie .two-col.has-gutter .column,
[owa] .two-col.x_has-gutter .column {
max-width: 290px !important;
width: 290px !important;
}
.ie .three-col.has-gutter .column,
[owa] .three-col.x_has-gutter .column {
max-width: 188px !important;
width: 188px !important;
}
.ie .has-gutter .wide,
[owa] .has-gutter .wide {
max-width: 394px !important;
width: 394px !important;
}
.ie .two-col.has-gutter.has-border .column,
[owa] .two-col.x_has-gutter.x_has-border .column {
max-width: 292px !important;
width: 292px !important;
}
.ie .three-col.has-gutter.has-border .column,
[owa] .three-col.x_has-gutter.x_has-border .column,
.ie .has-gutter.has-border .narrow,
[owa] .has-gutter.x_has-border .narrow {
max-width: 190px !important;
width: 190px !important;
}
.ie .has-gutter.has-border .wide,
[owa] .has-gutter.x_has-border .wide {
max-width: 396px !important;
width: 396px !important;
}
.ie .fixed-width .layout__inner {
border-left: 0 none white !important;
border-right: 0 none white !important;
}
.layout-fixed-width,
.mso .layout-full-width {
background-color: #ffffff;
}
@media only screen and (min-width: 620px) {
.column {
display: table-cell;
Float: none !important;
vertical-align: top;
}
.layout,
.one-col .column {
max-width: 600px !important;
width: 600px !important;
}
.two-col .column {
max-width: 300px !important;
width: 300px !important;
}
.three-col .column,
.column.narrow {
max-width: 200px !important;
width: 200px !important;
}
.column.wide {
width: 400px !important;
}
.two-col.has-gutter .column,
.two-col.ecxhas-gutter .column {
max-width: 290px !important;
width: 290px !important;
}
.three-col.has-gutter .column,
.three-col.ecxhas-gutter .column,
.has-gutter .narrow {
max-width: 188px !important;
width: 188px !important;
}
.has-gutter .wide {
max-width: 394px !important;
width: 394px !important;
}
.two-col.has-gutter.has-border .column,
.two-col.ecxhas-gutter.ecxhas-border .column {
max-width: 292px !important;
width: 292px !important;
}
.three-col.has-gutter.has-border .column,
.three-col.ecxhas-gutter.ecxhas-border .column,
.has-gutter.has-border .narrow,
.has-gutter.ecxhas-border .narrow {
max-width: 190px !important;
width: 190px !important;
}
.has-gutter.has-border .wide,
.has-gutter.ecxhas-border .wide {
max-width: 396px !important;
width: 396px !important;
}
}
@media (max-width: 321px) {
.fixed-width.has-border .layout__inner {
border-width: 1px 0 !important;
}
.layout,
.column {
min-width: 320px !important;
width: 320px !important;
}
}
.mso div {
border: 0 none white !important;
}
.mso .w560 .divider {
Margin-left: 260px !important;
Margin-right: 260px !important;
}
.mso .w360 .divider {
Margin-left: 160px !important;
Margin-right: 160px !important;
}
.mso .w260 .divider {
Margin-left: 110px !important;
Margin-right: 110px !important;
}
.mso .w160 .divider {
Margin-left: 60px !important;
Margin-right: 60px !important;
}
.mso .w354 .divider {
Margin-left: 157px !important;
Margin-right: 157px !important;
}
.mso .w250 .divider {
Margin-left: 105px !important;
Margin-right: 105px !important;
}
.mso .w148 .divider {
Margin-left: 54px !important;
Margin-right: 54px !important;
}
.mso .size-10,
.ie .size-10 {
font-size: 10px !important;
line-height: 18px !important;
}
.mso .size-12,
.ie .size-12 {
font-size: 12px !important;
line-height: 19px !important;
}
.mso .size-16,
.ie .size-16 {
font-size: 16px !important;
line-height: 24px !important;
}
.mso .size-20,
.ie .size-20 {
font-size: 20px !important;
line-height: 28px !important;
}
.mso .size-40,
.ie .size-40 {
font-size: 40px !important;
line-height: 47px !important;
}
</style>
<!--[if !mso]><!-->
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700,400italic);
</style>
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700,400italic" rel="stylesheet" type="text/css" />
<!--<![endif]-->
<style type="text/css">
body {
background-color: #fff
}
.logo a:hover,
.logo a:focus {
color: #859bb1 !important
}
.mso h1,
.ie h1 {}
.mso h1,
.ie h1 {
font-size: 64px !important;
line-height: 63px !important
}
.mso h2,
.ie h2 {}
.mso h2,
.ie h2 {
font-size: 30px !important;
line-height: 38px !important
}
.mso h3,
.ie h3 {}
.mso h3,
.ie h3 {
font-size: 22px !important;
line-height: 31px !important
}
.mso .footer__share-button p {}
.mso .footer__share-button p {
font-family: sans-serif
}
</style>
<meta name="robots" content="noindex,nofollow" />
<meta property="og:title" content="My First Campaign" />
</head>
<!--[if mso]>
<body class="mso">
<![endif]-->
<!--[if !mso]><!-->
<body class="half-padding" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;">
<!--<![endif]-->
<table class="wrapper" style="border-collapse: collapse;table-layout: fixed;min-width: 320px;width: 100%;background-color: #fff;" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<div role="section">
<div class="layout one-col fixed-width"
style="margin: 0 auto;max-width: 600px;min-width: 320px; width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%;
background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);">
<div style="margin: 12px 20px">
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
<h1 class="size-40"
style="Margin-top: 0;Margin-bottom: 0;font-style: normal;font-weight: normal;
color: #000;font-size: 32px;line-height: 40px;
font-family: montserrat,dejavu sans,verdana,sans-serif;" lang="x-size-40">
<span class="font-montserrat">
<strong>Are you trying to sign in?</strong>
</span>
</h1>
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div class="layout one-col fixed-width"
style="Margin: 0 auto;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%; background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);">
<div style="margin: 12px 20px">
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
<p class="size-20"
style="Margin-top: 5px;Margin-bottom: 0;
font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 17px;
line-height: 26px;" lang="x-size-20">
<span class="font-montserrat">There was an attempt to create an account with this
email address on the {{ .SatelliteName }} Storj DCS satellite, but an account
was previously created with this email. If this was you, try signing in to the
existing account instead.
</span>
</p>
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div class="layout one-col fixed-width"
style="Margin: 0 auto;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%;background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);">
<div style="margin: 12px 20px">
<div class="btn btn--flat btn--large" style="text-align:left;">
<a style="border-radius: 8px;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: #0149FF;
font-family: Montserrat, DejaVu Sans, Verdana, sans-serif;"
href="{{ .SignInLink }}">Sign In
</a>
<!--[if mso]>
<p style="line-height:0;margin:0;"></p>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
href="{{ .SignInLink }}" style="width:191px" arcsize="9%" fillcolor="#2683FF"
stroke="f">
<v:textbox style="mso-fit-shape-to-text:t" inset="0px,11px,0px,11px">
<center style="font-size:14px;line-height:24px;color:#FFFFFF;
font-family:Montserrat,DejaVu Sans,Verdana,sans-serif;font-weight:bold;
mso-line-height-rule:exactly;mso-text-raise:4px">Sign In
</center>
</v:textbox>
</v:roundrect>
<![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div class="layout one-col fixed-width"
style="Margin: 0 auto;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%; background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);">
<div style="margin: 12px 20px">
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
<p class="size-20"
style="Margin-top: 5px;Margin-bottom: 0;
font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 17px;
line-height: 26px;" lang="x-size-20">
<span class="font-montserrat">If this wasn't you, we recommend <a href="{{ .ResetPasswordLink }}" target="_blank" rel="noopener noreferrer">resetting your password</a>.
If this activity looks suspicious to you, notify our <a href="https://supportdcs.storj.io/hc/en-us" target="_blank" rel="noopener noreferrer">Support team</a>.
<br/><br/>
If your intention was to create an account on a different satellite, please select the appropriate satellite abbreviation from the drop down at
the top right of the screen when <a href="{{ .CreateAccountLink }}" target="_blank" rel="noopener noreferrer">creating an account</a>.
</span>
</p>
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div class="layout one-col fixed-width"
style="Margin: 0 auto;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%;background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px; width: calc(28000% - 167400px);">
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;">
<div class="divider"
style="display: block;font-size: 2px;line-height: 1px;Margin-left: auto;
Margin-right: auto;width: 100%;background-color: #ccc;Margin-bottom: 20px;">&nbsp;
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div class="layout one-col fixed-width"
style="Margin: 0 auto;max-width: 600px;min-width: 320px; width: calc(28000% - 167400px);
overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner"
style="border-collapse: collapse;display: table;width: 100%;background-color: #fff;">
<!--[if (mso)|(IE)]>
<table align="center" cellpadding="0" cellspacing="0" role="presentation">
<tr class="layout-fixed-width" style="background-color: #fff;">
<td style="width: 600px" class="w560">
<![endif]-->
<div class="column"
style="text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;
font-family: sans-serif;max-width: 600px;min-width: 320px;width: calc(28000% - 167400px);">
<div style="margin: 12px 20px">
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
<p class="size-12" style="Margin-top: 0;Margin-bottom: 0;font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 12px;line-height: 19px;" lang="x-size-12">
<span class="font-montserrat">Please do not reply to this email.<br />
1450 W. Peachtree St. NW #200, PMB 75268, Atlanta, GA 30309-2955, United States
</span>
</p>
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>