satellite/console: send unverified user activation link in project invite
When an unverified user is sent a project invitation it contains a registration link currently. Instead, send an activation link. github issue: https://github.com/storj/storj/issues/6033 Change-Id: I54b88de8347a2532f7a85372c0c5e4df4bf4eb38
This commit is contained in:
parent
d6e0987dd9
commit
a5b1c0432f
@ -70,6 +70,21 @@ func (email *ExistingUserProjectInvitationEmail) Subject() string {
|
||||
return "You were invited to join a project on Storj"
|
||||
}
|
||||
|
||||
// UnverifiedUserProjectInvitationEmail is mailservice template for project invitation email for unverified users.
|
||||
type UnverifiedUserProjectInvitationEmail struct {
|
||||
InviterEmail string
|
||||
Region string
|
||||
ActivationLink string
|
||||
}
|
||||
|
||||
// Template returns email template name.
|
||||
func (*UnverifiedUserProjectInvitationEmail) Template() string { return "UnverifiedUserInvite" }
|
||||
|
||||
// Subject gets email subject.
|
||||
func (email *UnverifiedUserProjectInvitationEmail) Subject() string {
|
||||
return "You were invited to join a project on Storj"
|
||||
}
|
||||
|
||||
// NewUserProjectInvitationEmail is mailservice template for project invitation email for new users.
|
||||
type NewUserProjectInvitationEmail struct {
|
||||
InviterEmail string
|
||||
|
@ -3653,6 +3653,7 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
|
||||
|
||||
var users []*User
|
||||
var newUserEmails []string
|
||||
var unverifiedUsers []User
|
||||
for _, email := range emails {
|
||||
invite, err := s.store.ProjectInvitations().Get(ctx, projectID, email)
|
||||
if err != nil && !errs.Is(err, sql.ErrNoRows) {
|
||||
@ -3662,15 +3663,25 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
|
||||
return nil, ErrAlreadyInvited.New(projInviteExistsErrMsg, email)
|
||||
}
|
||||
|
||||
invitedUser, err := s.store.Users().GetByEmail(ctx, email)
|
||||
invitedUser, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, email)
|
||||
if err == nil {
|
||||
_, err = s.isProjectMember(ctx, invitedUser.ID, projectID)
|
||||
if err != nil && !ErrNoMembership.Has(err) {
|
||||
return nil, Error.Wrap(err)
|
||||
} else if err == nil {
|
||||
return nil, ErrAlreadyMember.New("%s is already a member", email)
|
||||
if invitedUser != nil {
|
||||
_, err = s.isProjectMember(ctx, invitedUser.ID, projectID)
|
||||
if err != nil && !ErrNoMembership.Has(err) {
|
||||
return nil, Error.Wrap(err)
|
||||
} else if err == nil {
|
||||
return nil, ErrAlreadyMember.New("%s is already a member", email)
|
||||
}
|
||||
users = append(users, invitedUser)
|
||||
} else {
|
||||
oldest := User{}
|
||||
for _, u := range unverified {
|
||||
if u.CreatedAt.Before(oldest.CreatedAt) {
|
||||
oldest = u
|
||||
}
|
||||
}
|
||||
unverifiedUsers = append(unverifiedUsers, oldest)
|
||||
}
|
||||
users = append(users, invitedUser)
|
||||
} else if errs.Is(err, sql.ErrNoRows) {
|
||||
newUserEmails = append(newUserEmails, email)
|
||||
} else {
|
||||
@ -3690,6 +3701,19 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var isUnverified bool
|
||||
for _, u := range unverifiedUsers {
|
||||
if email == u.Email {
|
||||
isUnverified = true
|
||||
invites = append(invites, *invite)
|
||||
break
|
||||
}
|
||||
}
|
||||
if isUnverified {
|
||||
continue
|
||||
}
|
||||
|
||||
token, err := s.CreateInviteToken(ctx, isMember.project.PublicID, email, invite.CreatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -3721,6 +3745,23 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
|
||||
},
|
||||
)
|
||||
}
|
||||
for _, u := range unverifiedUsers {
|
||||
token, err := s.GenerateActivationToken(ctx, u.ID, u.Email)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
activationLink := fmt.Sprintf("%s/activation?token=%s", s.satelliteAddress, token)
|
||||
s.mailService.SendRenderedAsync(
|
||||
ctx,
|
||||
[]post.Address{{Address: u.Email}},
|
||||
&UnverifiedUserProjectInvitationEmail{
|
||||
InviterEmail: user.Email,
|
||||
Region: s.satelliteName,
|
||||
ActivationLink: activationLink,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, email := range newUserEmails {
|
||||
inviteLink := fmt.Sprintf("%s?invite=%s", baseLink, inviteTokens[email])
|
||||
s.mailService.SendRenderedAsync(
|
||||
|
@ -29,10 +29,12 @@ import (
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/blockchain"
|
||||
"storj.io/storj/private/post"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/buckets"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleweb/consoleapi"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/billing"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
@ -2099,6 +2101,23 @@ func TestServiceGenMethods(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type EmailVerifier struct {
|
||||
Data consoleapi.ContextChannel
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func (v *EmailVerifier) SendEmail(ctx context.Context, msg *post.Message) error {
|
||||
body := ""
|
||||
for _, part := range msg.Parts {
|
||||
body += part.Content
|
||||
}
|
||||
return v.Data.Send(v.Context, body)
|
||||
}
|
||||
|
||||
func (v *EmailVerifier) FromAddress() post.Address {
|
||||
return post.Address{}
|
||||
}
|
||||
|
||||
func TestProjectInvitations(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
@ -2212,6 +2231,29 @@ func TestProjectInvitations(t *testing.T) {
|
||||
// inviting a project member should fail.
|
||||
_, err = service.InviteProjectMembers(ctx, project.ID, []string{user2.Email})
|
||||
require.Error(t, err)
|
||||
|
||||
// test inviting unverified user.
|
||||
sender := &EmailVerifier{Context: ctx}
|
||||
sat.API.Mail.Service.Sender = sender
|
||||
|
||||
regToken, err := service.CreateRegToken(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
unverified, err := service.CreateUser(ctx, console.CreateUser{
|
||||
FullName: "test user",
|
||||
Email: "test-unverified-email@test",
|
||||
Password: "password",
|
||||
}, regToken.Secret)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, unverified.Status)
|
||||
|
||||
invites, err = service.InviteProjectMembers(ctx, project.ID, []string{unverified.Email})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, unverified.Email, strings.ToLower(invites[0].Email))
|
||||
|
||||
body, err := sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "/activation")
|
||||
})
|
||||
|
||||
t.Run("get invitation", func(t *testing.T) {
|
||||
|
222
web/satellite/static/emails/UnverifiedUserInvite.html
Normal file
222
web/satellite/static/emails/UnverifiedUserInvite.html
Normal file
@ -0,0 +1,222 @@
|
||||
<!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:o="urn:schemas-microsoft-com:office:office"
|
||||
style="width:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta content="telephone=no" name="format-detection">
|
||||
<title>Project Invitation</title><!--[if (mso 16)]>
|
||||
<style type="text/css">
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<![endif]--><!--[if gte mso 9]>
|
||||
<style>sup {
|
||||
font-size: 100% !important;
|
||||
}</style><![endif]--><!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG></o:AllowPNG>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.es-button {
|
||||
mso-style-priority: 100 !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
||||
p,
|
||||
ul li,
|
||||
ol li,
|
||||
a {
|
||||
line-height: 150% !important
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a {
|
||||
line-height: 120% !important
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px !important;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px !important;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px !important;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.es-content-body h1 a {
|
||||
font-size: 30px !important
|
||||
}
|
||||
|
||||
.es-content-body h2 a {
|
||||
font-size: 26px !important
|
||||
}
|
||||
|
||||
.es-content-body h3 a {
|
||||
font-size: 20px !important
|
||||
}
|
||||
|
||||
.es-content-body p,
|
||||
.es-content-body ul li,
|
||||
.es-content-body ol li,
|
||||
.es-content-body a {
|
||||
font-size: 16px !important
|
||||
}
|
||||
|
||||
.es-button-border {
|
||||
display: block !important
|
||||
}
|
||||
|
||||
a.es-button,
|
||||
button.es-button {
|
||||
font-size: 20px !important;
|
||||
display: block !important;
|
||||
padding: 15px 25px 15px 25px !important
|
||||
}
|
||||
|
||||
.es-content table,
|
||||
.es-content {
|
||||
width: 100% !important;
|
||||
max-width: 600px !important
|
||||
}
|
||||
|
||||
.adapt-img {
|
||||
width: 100% !important;
|
||||
height: auto !important
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
style="width:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:'helvetica neue', helvetica, arial, sans-serif;padding:0;Margin:0">
|
||||
<div class="es-wrapper-color" style="background-color:#F6F6F6"><!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" color="#f6f6f6"></v:fill>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table class="es-wrapper" width="100%" cellspacing="0" cellpadding="0"
|
||||
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;padding:0;Margin:0;width:100%;height:100%;background-repeat:repeat;background-position:center top;background-color:#F6F6F6">
|
||||
<tr style="border-collapse:collapse">
|
||||
<td valign="top" style="padding:0;Margin:0">
|
||||
<table class="es-content" cellspacing="0" cellpadding="0" align="center"
|
||||
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
|
||||
<tr style="border-collapse:collapse">
|
||||
<td style="padding:0;Margin:0;background-color:#fafafb;background-size:cover"
|
||||
bgcolor="#FAFAFB" align="center">
|
||||
<table class="es-content-body"
|
||||
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:transparent;width:640px"
|
||||
cellspacing="0" cellpadding="0" bgcolor="#f6f6f6" align="center">
|
||||
<tr style="border-collapse:collapse">
|
||||
<td align="left"
|
||||
style="padding:0;Margin:0;padding-top:20px;padding-left:20px;padding-right:20px">
|
||||
<table width="100%" cellspacing="0" cellpadding="0"
|
||||
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||
<tr style="border-collapse:collapse">
|
||||
<td valign="top" align="center"
|
||||
style="padding:0;Margin:0;width:600px">
|
||||
<table width="100%" cellspacing="0" cellpadding="0"
|
||||
role="presentation"
|
||||
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||
<tr style="border-collapse:collapse">
|
||||
<td align="center"
|
||||
style="padding:0;Margin:0;padding-top:25px;padding-bottom:30px">
|
||||
<h1
|
||||
style="Margin:0;line-height:67px;mso-line-height-rule:exactly;font-family:'helvetica neue', helvetica, arial, sans-serif;font-size:56px;font-style:normal;font-weight:bold;color:#091c45">
|
||||
You were invited<br>to a project on Storj</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td esdev-links-color="#b7bdc9" align="center"
|
||||
style="padding:0;Margin:0">
|
||||
<p
|
||||
style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:'helvetica neue', helvetica, arial, sans-serif;line-height:21px;color:#091c45;font-size:14px">
|
||||
{{ .InviterEmail }} has invited you to
|
||||
a project on Storj DCS on the {{ .Region }}
|
||||
region. Click the link to activate your account,
|
||||
then sign in to join the project. Do not share
|
||||
this link with anyone.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td align="center"
|
||||
style="margin:0;padding: 25px 0 50px;">
|
||||
<!--[if mso]>
|
||||
<a href="{{ .ActivationLink }}" target="_blank"
|
||||
hidden>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
esdevVmlButton
|
||||
href="{{ .ActivationLink }}"
|
||||
style="height:46px; v-text-anchor:middle; width:195px"
|
||||
arcsize="22%" stroke="f"
|
||||
fillcolor="#00ac26">
|
||||
<w:anchorlock></w:anchorlock>
|
||||
<center style='color:#ffffff; font-family:"helvetica neue", helvetica, arial, sans-serif; font-size:14px; font-weight:400; line-height:14px; mso-text-raise:1px'>
|
||||
Accept Invitation →
|
||||
</center>
|
||||
</v:roundrect>
|
||||
</a>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<span class="msohide es-button-border"
|
||||
style="box-shadow: 0px 20px 30px rgba(10, 27, 44, 0.2);border-style:solid;border-color:#75B6C9;background:#00ac26;border-width:0px;display:inline-block;border-radius:10px;width:auto;mso-hide:all">
|
||||
<a href="{{ .ActivationLink }}"
|
||||
class="es-button msohide" target="_blank"
|
||||
style="mso-style-priority:100 !important;text-decoration:none;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;color:#ffffff;font-size:14px;display:inline-block;background:#00ac26;border-radius:10px;font-family:'helvetica neue', helvetica, arial, sans-serif;font-weight:700;font-style:normal;line-height:17px;width:auto;text-align:center;padding:15px 25px 15px 25px;mso-padding-alt:0;mso-border-alt:10px solid #75B6C9;mso-hide:all">Activate
|
||||
Account →</a>
|
||||
</span>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user