web/satellite: create chore that will resend verification emails to unverified users
We want to remind unverified users to verify their emails: once after 24 hours has passed and again after 5 days has passed. Add mailservice.Service to satellite core because it is needed by the chore for sending emails. To add the mailservice.Service to the core, we create a helper function in satellite/peer.go to avoid duplicating the code in both api.go and core.go. In addition to the chore, this change adds methods to users.DB to get unverified users in need of reminder. Change-Id: I4e515bdf43f922788b4f965b2efb34fa32288bd1
This commit is contained in:
parent
836fa188fa
commit
087e57d037
@ -548,6 +548,8 @@ func (planet *Planet) newSatellite(ctx context.Context, prefix string, index int
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peer.Mail.EmailReminders.TestSetLinkAddress("http://" + api.Console.Listener.Addr().String() + "/")
|
||||
|
||||
return createNewSystem(prefix, log, config, peer, api, repairerPeer, adminPeer, gcPeer), nil
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
@ -27,8 +25,6 @@ import (
|
||||
"storj.io/private/debug"
|
||||
"storj.io/private/version"
|
||||
"storj.io/storj/private/lifecycle"
|
||||
"storj.io/storj/private/post"
|
||||
"storj.io/storj/private/post/oauth2"
|
||||
"storj.io/storj/private/server"
|
||||
"storj.io/storj/private/version/checker"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
@ -43,7 +39,6 @@ import (
|
||||
"storj.io/storj/satellite/inspector"
|
||||
"storj.io/storj/satellite/internalpb"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/mailservice/simulate"
|
||||
"storj.io/storj/satellite/metabase"
|
||||
"storj.io/storj/satellite/metainfo"
|
||||
"storj.io/storj/satellite/metainfo/piecedeletion"
|
||||
@ -464,66 +459,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
}
|
||||
|
||||
{ // setup mailservice
|
||||
// TODO(yar): test multiple satellites using same OAUTH credentials
|
||||
mailConfig := config.Mail
|
||||
|
||||
// validate from mail address
|
||||
from, err := mail.ParseAddress(mailConfig.From)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
// validate smtp server address
|
||||
host, _, err := net.SplitHostPort(mailConfig.SMTPServerAddress)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
var sender mailservice.Sender
|
||||
switch mailConfig.AuthType {
|
||||
case "oauth2":
|
||||
creds := oauth2.Credentials{
|
||||
ClientID: mailConfig.ClientID,
|
||||
ClientSecret: mailConfig.ClientSecret,
|
||||
TokenURI: mailConfig.TokenURI,
|
||||
}
|
||||
token, err := oauth2.RefreshToken(context.TODO(), creds, mailConfig.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: &oauth2.Auth{
|
||||
UserEmail: from.Address,
|
||||
Storage: oauth2.NewTokenStore(creds, *token),
|
||||
},
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
case "plain":
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: smtp.PlainAuth("", mailConfig.Login, mailConfig.Password, host),
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
case "login":
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: post.LoginAuth{
|
||||
Username: mailConfig.Login,
|
||||
Password: mailConfig.Password,
|
||||
},
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
default:
|
||||
sender = simulate.NewDefaultLinkClicker(peer.Log.Named("mail:linkclicker"))
|
||||
}
|
||||
|
||||
peer.Mail.Service, err = mailservice.New(
|
||||
peer.Log.Named("mail:service"),
|
||||
sender,
|
||||
mailConfig.TemplatePath,
|
||||
)
|
||||
peer.Mail.Service, err = setupMailService(peer.Log, *config)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
@ -334,10 +334,6 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
||||
UserName: userName,
|
||||
},
|
||||
)
|
||||
|
||||
if err = a.service.UpdateEmailVerificationReminder(ctx, time.Now().UTC()); err != nil {
|
||||
a.serveJSONError(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateEmail validates email to have correct form and syntax.
|
||||
@ -616,10 +612,6 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) {
|
||||
UserName: userName,
|
||||
},
|
||||
)
|
||||
|
||||
if err = a.service.UpdateEmailVerificationReminder(ctx, time.Now().UTC()); err != nil {
|
||||
a.serveJSONError(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// EnableUserMFA enables multi-factor authentication for the user.
|
||||
|
150
satellite/console/emailreminders/chore.go
Normal file
150
satellite/console/emailreminders/chore.go
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package emailreminders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/sync2"
|
||||
"storj.io/storj/private/post"
|
||||
"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"
|
||||
)
|
||||
|
||||
var mon = monkit.Package()
|
||||
|
||||
// Config contains configurations for email reminders.
|
||||
type Config struct {
|
||||
FirstVerificationReminder time.Duration `help:"amount of time before sending first reminder to users who need to verify their email" default:"24h"`
|
||||
SecondVerificationReminder time.Duration `help:"amount of time before sending second reminder to users who need to verify their email" default:"120h"`
|
||||
ChoreInterval time.Duration `help:"how often to send reminders to users who need to verify their email" default:"24h"`
|
||||
Enable bool `help:"enable sending emails reminding users to verify their email" default:"true"`
|
||||
}
|
||||
|
||||
// Chore checks whether any emails need to be re-sent.
|
||||
//
|
||||
// architecture: Chore
|
||||
type Chore struct {
|
||||
log *zap.Logger
|
||||
Loop *sync2.Cycle
|
||||
|
||||
tokens *consoleauth.Service
|
||||
usersDB console.Users
|
||||
mailService *mailservice.Service
|
||||
config Config
|
||||
address string
|
||||
useBlockingSend bool
|
||||
}
|
||||
|
||||
// NewChore instantiates Chore.
|
||||
func NewChore(log *zap.Logger, tokens *consoleauth.Service, usersDB console.Users, mailservice *mailservice.Service, config Config, address string) *Chore {
|
||||
if !strings.HasSuffix(address, "/") {
|
||||
address += "/"
|
||||
}
|
||||
return &Chore{
|
||||
log: log,
|
||||
Loop: sync2.NewCycle(config.ChoreInterval),
|
||||
tokens: tokens,
|
||||
usersDB: usersDB,
|
||||
config: config,
|
||||
mailService: mailservice,
|
||||
address: address,
|
||||
useBlockingSend: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the chore.
|
||||
func (chore *Chore) Run(ctx context.Context) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
return chore.Loop.Run(ctx, func(ctx context.Context) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
now := time.Now()
|
||||
users, err := chore.usersDB.GetUnverifiedNeedingReminder(ctx, now.Add(-chore.config.FirstVerificationReminder), now.Add(-chore.config.SecondVerificationReminder))
|
||||
if err != nil {
|
||||
chore.log.Error("error getting users in need of reminder", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
mon.IntVal("unverified_needing_reminder").Observe(int64(len(users)))
|
||||
|
||||
for _, u := range users {
|
||||
|
||||
token, err := chore.tokens.CreateToken(ctx, u.ID, u.Email)
|
||||
|
||||
if err != nil {
|
||||
chore.log.Error("error generating activation token", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
authController := consoleapi.NewAuth(zap.L(), nil, nil, nil, nil, nil, chore.address, "", "", "")
|
||||
|
||||
link := authController.ActivateAccountURL + "?token=" + token
|
||||
userName := u.ShortName
|
||||
if u.ShortName == "" {
|
||||
userName = u.FullName
|
||||
}
|
||||
|
||||
// blocking send allows us to verify that links are clicked in tests.
|
||||
if chore.useBlockingSend {
|
||||
err = chore.mailService.SendRendered(
|
||||
ctx,
|
||||
[]post.Address{{Address: u.Email, Name: userName}},
|
||||
&consoleql.AccountActivationEmail{
|
||||
ActivationLink: link,
|
||||
Origin: authController.ExternalAddress,
|
||||
UserName: userName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
chore.log.Error("error sending email reminder", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
chore.mailService.SendRenderedAsync(
|
||||
ctx,
|
||||
[]post.Address{{Address: u.Email, Name: userName}},
|
||||
&consoleql.AccountActivationEmail{
|
||||
ActivationLink: link,
|
||||
Origin: authController.ExternalAddress,
|
||||
UserName: userName,
|
||||
},
|
||||
)
|
||||
}
|
||||
if err = chore.usersDB.UpdateVerificationReminders(ctx, u.ID); err != nil {
|
||||
chore.log.Error("error updating user's last email verifcation reminder", zap.Error(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Close closes chore.
|
||||
func (chore *Chore) Close() error {
|
||||
chore.Loop.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestSetLinkAddress allows the email link address to be reconfigured.
|
||||
// The address points to the satellite web server's external address.
|
||||
// In the test environment the external address is not set by a config.
|
||||
// It is an internal address, and we don't know what the port is until after it
|
||||
// has been assigned. With this method, we get the address from the api in testplanet
|
||||
// and assign it here.
|
||||
func (chore *Chore) TestSetLinkAddress(address string) {
|
||||
chore.address = address
|
||||
}
|
||||
|
||||
// TestUseBlockingSend allows us to set the chore to use a blocking send method.
|
||||
// Using a blocking send method allows us to test that links are clicked without
|
||||
// potential race conditions.
|
||||
func (chore *Chore) TestUseBlockingSend() {
|
||||
chore.useBlockingSend = true
|
||||
}
|
167
satellite/console/emailreminders/chore_test.go
Normal file
167
satellite/console/emailreminders/chore_test.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package emailreminders_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
func TestEmailChoreUpdatesVerificationReminders(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.EmailReminders.FirstVerificationReminder = 0
|
||||
config.EmailReminders.SecondVerificationReminder = 0
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
users := planet.Satellites[0].DB.Console().Users()
|
||||
chore := planet.Satellites[0].Core.Mail.EmailReminders
|
||||
chore.Loop.Pause()
|
||||
|
||||
// Overwrite link address in chore so the links don't work
|
||||
// and we can test that the correct number of reminders are sent.
|
||||
chore.TestSetLinkAddress("")
|
||||
|
||||
id1 := testrand.UUID()
|
||||
_, err := users.Insert(ctx, &console.User{
|
||||
ID: id1,
|
||||
FullName: "test",
|
||||
Email: "userone@mail.test",
|
||||
PasswordHash: []byte("123a123"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
id2 := testrand.UUID()
|
||||
_, err = users.Insert(ctx, &console.User{
|
||||
ID: id2,
|
||||
FullName: "test",
|
||||
Email: "usertwo@mail.test",
|
||||
PasswordHash: []byte("123a123"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
id3 := testrand.UUID()
|
||||
_, err = users.Insert(ctx, &console.User{
|
||||
ID: id3,
|
||||
FullName: "test",
|
||||
Email: "userthree@mail.test",
|
||||
PasswordHash: []byte("123a123"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// This user will verify immediately and should not get reminders.
|
||||
user1, err := users.Get(ctx, id1)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user1.VerificationReminders)
|
||||
|
||||
// This user will get one reminder and then verify and should not get a second.
|
||||
user2, err := users.Get(ctx, id2)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user2.VerificationReminders)
|
||||
|
||||
// This user will not verify at all and should get 2 reminders and no more.
|
||||
user3, err := users.Get(ctx, id3)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user3.VerificationReminders)
|
||||
|
||||
user1.Status = 1
|
||||
err = users.Update(ctx, user1)
|
||||
require.NoError(t, err)
|
||||
|
||||
chore.Loop.TriggerWait()
|
||||
|
||||
user1, err = users.Get(ctx, id1)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user1.VerificationReminders)
|
||||
|
||||
user2, err = users.Get(ctx, id2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, user2.VerificationReminders)
|
||||
|
||||
user3, err = users.Get(ctx, id3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, user3.VerificationReminders)
|
||||
|
||||
user2.Status = 1
|
||||
err = users.Update(ctx, user2)
|
||||
require.NoError(t, err)
|
||||
|
||||
chore.Loop.TriggerWait()
|
||||
|
||||
user1, err = users.Get(ctx, id1)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user1.VerificationReminders)
|
||||
|
||||
user2, err = users.Get(ctx, id2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, user2.VerificationReminders)
|
||||
|
||||
user3, err = users.Get(ctx, id3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, user3.VerificationReminders)
|
||||
|
||||
// Check user is not reminded again after 2
|
||||
chore.Loop.TriggerWait()
|
||||
|
||||
user1, err = users.Get(ctx, id1)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, user1.VerificationReminders)
|
||||
|
||||
user2, err = users.Get(ctx, id2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, user2.VerificationReminders)
|
||||
|
||||
user3, err = users.Get(ctx, id3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, user3.VerificationReminders)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmailChoreLinkActivatesAccount(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.EmailReminders.FirstVerificationReminder = 0
|
||||
config.EmailReminders.SecondVerificationReminder = 0
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
users := planet.Satellites[0].DB.Console().Users()
|
||||
chore := planet.Satellites[0].Core.Mail.EmailReminders
|
||||
chore.Loop.Pause()
|
||||
chore.TestUseBlockingSend()
|
||||
|
||||
id := testrand.UUID()
|
||||
_, err := users.Insert(ctx, &console.User{
|
||||
ID: id,
|
||||
FullName: "test",
|
||||
Email: "userone@mail.test",
|
||||
PasswordHash: []byte("123a123"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
u, err := users.Get(ctx, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, console.UserStatus(0), u.Status)
|
||||
|
||||
chore.Loop.TriggerWait()
|
||||
|
||||
u, err = users.Get(ctx, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, console.UserStatus(1), u.Status)
|
||||
})
|
||||
}
|
@ -1064,21 +1064,6 @@ func (s *Service) UpdateAccount(ctx context.Context, fullName string, shortName
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEmailVerificationReminder updates the last time a user was sent a verification email.
|
||||
func (s *Service) UpdateEmailVerificationReminder(ctx context.Context, t time.Time) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
err = s.store.Users().Update(ctx, &User{
|
||||
LastVerificationReminder: t,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeEmail updates email for a given user.
|
||||
func (s *Service) ChangeEmail(ctx context.Context, newEmail string) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -20,6 +20,10 @@ import (
|
||||
type Users interface {
|
||||
// Get is a method for querying user from the database by id.
|
||||
Get(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
// GetUnverifiedNeedingReminder gets unverified users needing a reminder to verify their email.
|
||||
GetUnverifiedNeedingReminder(ctx context.Context, firstReminder, secondReminder time.Time) ([]*User, error)
|
||||
// UpdateVerificationReminders increments verification_reminders.
|
||||
UpdateVerificationReminders(ctx context.Context, id uuid.UUID) error
|
||||
// GetByEmailWithUnverified is a method for querying users by email from the database.
|
||||
GetByEmailWithUnverified(ctx context.Context, email string) (*User, []User, error)
|
||||
// GetByEmail is a method for querying user by verified email from the database.
|
||||
|
@ -11,10 +11,12 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
@ -339,3 +341,64 @@ func TestGetUserByEmail(t *testing.T) {
|
||||
require.Equal(t, activeUser.ID, dbUser.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUnverifiedNeedingReminder(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.EmailReminders.FirstVerificationReminder = 24 * time.Hour
|
||||
config.EmailReminders.SecondVerificationReminder = 120 * time.Hour
|
||||
},
|
||||
},
|
||||
SatelliteCount: 1,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
var sentFirstReminder bool
|
||||
var sentSecondReminder bool
|
||||
|
||||
config := planet.Satellites[0].Config.EmailReminders
|
||||
db := planet.Satellites[0].DB.Console().Users()
|
||||
|
||||
id := testrand.UUID()
|
||||
_, err := db.Insert(ctx, &console.User{
|
||||
ID: id,
|
||||
FullName: "unverified user one",
|
||||
Email: "userone@mail.test",
|
||||
PasswordHash: []byte("123a123"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// We expect two reminders in total - one after a day of account creation,
|
||||
// and one after five. This test will check to ensure that both reminders occur.
|
||||
// Each iteration advances time by i*24 hours from `now`.
|
||||
for i := 0; i <= 6; i++ {
|
||||
u, err := db.Get(ctx, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Intuitively it would be better to test this by setting `created_at` to some point in the past.
|
||||
// Since we have no control over `created_at` (it's autoinserted) we will instead pass in a future time
|
||||
// as the `now` argument to `GetUnverifiedNeedingReminder`
|
||||
futureTime := now.Add(time.Duration(i*24) * time.Hour)
|
||||
needReminder, err := db.GetUnverifiedNeedingReminder(ctx, futureTime.Add(-config.FirstVerificationReminder), futureTime.Add(-config.SecondVerificationReminder))
|
||||
require.NoError(t, err)
|
||||
|
||||
// These are the conditions in the SQL query which selects users needing reminder
|
||||
if u.VerificationReminders == 0 && u.CreatedAt.Before(futureTime.Add(-config.FirstVerificationReminder)) {
|
||||
require.NotEmpty(t, needReminder)
|
||||
require.Equal(t, u.ID, needReminder[0].ID)
|
||||
require.NoError(t, db.UpdateVerificationReminders(ctx, u.ID))
|
||||
sentFirstReminder = true
|
||||
} else if u.VerificationReminders == 1 && u.CreatedAt.Before(futureTime.Add(-config.SecondVerificationReminder)) {
|
||||
require.NotEmpty(t, needReminder)
|
||||
require.Equal(t, u.ID, needReminder[0].ID)
|
||||
require.NoError(t, db.UpdateVerificationReminders(ctx, u.ID))
|
||||
sentSecondReminder = true
|
||||
} else {
|
||||
require.Empty(t, needReminder)
|
||||
}
|
||||
}
|
||||
require.True(t, sentFirstReminder)
|
||||
require.True(t, sentSecondReminder)
|
||||
})
|
||||
}
|
||||
|
@ -32,7 +32,10 @@ import (
|
||||
"storj.io/storj/satellite/accounting/tally"
|
||||
"storj.io/storj/satellite/audit"
|
||||
"storj.io/storj/satellite/buckets"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"storj.io/storj/satellite/console/emailreminders"
|
||||
"storj.io/storj/satellite/gracefulexit"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/metabase"
|
||||
"storj.io/storj/satellite/metabase/segmentloop"
|
||||
"storj.io/storj/satellite/metabase/zombiedeletion"
|
||||
@ -66,6 +69,11 @@ type Core struct {
|
||||
Service *version_checker.Service
|
||||
}
|
||||
|
||||
Mail struct {
|
||||
Service *mailservice.Service
|
||||
EmailReminders *emailreminders.Chore
|
||||
}
|
||||
|
||||
Debug struct {
|
||||
Listener net.Listener
|
||||
Server *debug.Server
|
||||
@ -209,6 +217,40 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
peer.Dialer = rpc.NewDefaultDialer(tlsOptions)
|
||||
}
|
||||
|
||||
{ // setup mailservice
|
||||
peer.Mail.Service, err = setupMailService(peer.Log, *config)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
peer.Services.Add(lifecycle.Item{
|
||||
Name: "mail:service",
|
||||
Close: peer.Mail.Service.Close,
|
||||
})
|
||||
}
|
||||
|
||||
{ // setup email reminders
|
||||
authTokens := consoleauth.NewService(config.ConsoleAuth, &consoleauth.Hmac{Secret: []byte(config.Console.AuthTokenSecret)})
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
peer.Mail.EmailReminders = emailreminders.NewChore(
|
||||
peer.Log.Named("console:chore"),
|
||||
authTokens,
|
||||
peer.DB.Console().Users(),
|
||||
peer.Mail.Service,
|
||||
config.EmailReminders,
|
||||
config.Console.ExternalAddress,
|
||||
)
|
||||
|
||||
peer.Services.Add(lifecycle.Item{
|
||||
Name: "mail:email-reminders",
|
||||
Run: peer.Mail.EmailReminders.Run,
|
||||
Close: peer.Mail.EmailReminders.Close,
|
||||
})
|
||||
}
|
||||
|
||||
{ // setup overlay
|
||||
peer.Overlay.DB = peer.DB.OverlayCache()
|
||||
peer.Overlay.Service, err = overlay.NewService(peer.Log.Named("overlay"), peer.Overlay.DB, config.Overlay)
|
||||
|
@ -5,12 +5,18 @@ package satellite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
|
||||
hw "github.com/jtolds/monkit-hw/v2"
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/identity"
|
||||
"storj.io/private/debug"
|
||||
"storj.io/storj/private/post"
|
||||
"storj.io/storj/private/post/oauth2"
|
||||
"storj.io/storj/private/server"
|
||||
version_checker "storj.io/storj/private/version/checker"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
@ -28,11 +34,13 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"storj.io/storj/satellite/console/consoleweb"
|
||||
"storj.io/storj/satellite/console/emailreminders"
|
||||
"storj.io/storj/satellite/console/restkeys"
|
||||
"storj.io/storj/satellite/contact"
|
||||
"storj.io/storj/satellite/gc"
|
||||
"storj.io/storj/satellite/gracefulexit"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/mailservice/simulate"
|
||||
"storj.io/storj/satellite/metabase/zombiedeletion"
|
||||
"storj.io/storj/satellite/metainfo"
|
||||
"storj.io/storj/satellite/metainfo/expireddeletion"
|
||||
@ -146,9 +154,10 @@ type Config struct {
|
||||
|
||||
Payments paymentsconfig.Config
|
||||
|
||||
RESTKeys restkeys.Config
|
||||
Console consoleweb.Config
|
||||
ConsoleAuth consoleauth.Config
|
||||
RESTKeys restkeys.Config
|
||||
Console consoleweb.Config
|
||||
ConsoleAuth consoleauth.Config
|
||||
EmailReminders emailreminders.Config
|
||||
|
||||
Version version_checker.Config
|
||||
|
||||
@ -162,3 +171,66 @@ type Config struct {
|
||||
|
||||
Analytics analytics.Config
|
||||
}
|
||||
|
||||
func setupMailService(log *zap.Logger, config Config) (*mailservice.Service, error) {
|
||||
// TODO(yar): test multiple satellites using same OAUTH credentials
|
||||
mailConfig := config.Mail
|
||||
|
||||
// validate from mail address
|
||||
from, err := mail.ParseAddress(mailConfig.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate smtp server address
|
||||
host, _, err := net.SplitHostPort(mailConfig.SMTPServerAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sender mailservice.Sender
|
||||
switch mailConfig.AuthType {
|
||||
case "oauth2":
|
||||
creds := oauth2.Credentials{
|
||||
ClientID: mailConfig.ClientID,
|
||||
ClientSecret: mailConfig.ClientSecret,
|
||||
TokenURI: mailConfig.TokenURI,
|
||||
}
|
||||
token, err := oauth2.RefreshToken(context.TODO(), creds, mailConfig.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: &oauth2.Auth{
|
||||
UserEmail: from.Address,
|
||||
Storage: oauth2.NewTokenStore(creds, *token),
|
||||
},
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
case "plain":
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: smtp.PlainAuth("", mailConfig.Login, mailConfig.Password, host),
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
case "login":
|
||||
sender = &post.SMTPSender{
|
||||
From: *from,
|
||||
Auth: post.LoginAuth{
|
||||
Username: mailConfig.Login,
|
||||
Password: mailConfig.Password,
|
||||
},
|
||||
ServerAddress: mailConfig.SMTPServerAddress,
|
||||
}
|
||||
default:
|
||||
sender = simulate.NewDefaultLinkClicker(log.Named("mail:linkclicker"))
|
||||
}
|
||||
|
||||
return mailservice.New(
|
||||
log.Named("mail:service"),
|
||||
sender,
|
||||
mailConfig.TemplatePath,
|
||||
)
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ type ConsoleDB struct {
|
||||
|
||||
// Users is getter a for Users repository.
|
||||
func (db *ConsoleDB) Users() console.Users {
|
||||
return &users{db.methods}
|
||||
return &users{db.db}
|
||||
}
|
||||
|
||||
// Projects is a getter for Projects repository.
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
@ -21,7 +22,7 @@ var _ console.Users = (*users)(nil)
|
||||
|
||||
// implementation of Users interface repository using spacemonkeygo/dbx orm.
|
||||
type users struct {
|
||||
db dbx.Methods
|
||||
db *satelliteDB
|
||||
}
|
||||
|
||||
// Get is a method for querying user from the database by id.
|
||||
@ -75,6 +76,46 @@ func (users *users) GetByEmail(ctx context.Context, email string) (_ *console.Us
|
||||
return userFromDBX(ctx, user)
|
||||
}
|
||||
|
||||
// GetUnverifiedNeedingReminder returns users in need of a reminder to verify their email.
|
||||
func (users *users) GetUnverifiedNeedingReminder(ctx context.Context, firstReminder, secondReminder time.Time) (usersNeedingReminder []*console.User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
rows, err := users.db.Query(ctx, `
|
||||
SELECT id, email, full_name, short_name
|
||||
FROM users
|
||||
WHERE status = 0
|
||||
AND (
|
||||
(verification_reminders = 0 AND created_at < $1)
|
||||
OR (verification_reminders = 1 AND created_at < $2)
|
||||
)
|
||||
`, firstReminder, secondReminder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, rows.Close()) }()
|
||||
|
||||
for rows.Next() {
|
||||
var user console.User
|
||||
err = rows.Scan(&user.ID, &user.Email, &user.FullName, &user.ShortName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersNeedingReminder = append(usersNeedingReminder, &user)
|
||||
}
|
||||
|
||||
return usersNeedingReminder, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateVerificationReminders increments verification_reminders.
|
||||
func (users *users) UpdateVerificationReminders(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := users.db.ExecContext(ctx, `
|
||||
UPDATE users
|
||||
SET verification_reminders = verification_reminders + 1
|
||||
WHERE id = $1
|
||||
`, id.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert is a method for inserting user into the database.
|
||||
func (users *users) Insert(ctx context.Context, user *console.User) (_ *console.User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
12
scripts/testdata/satellite-config.yaml.lock
vendored
12
scripts/testdata/satellite-config.yaml.lock
vendored
@ -385,6 +385,18 @@ contact.external-address: ""
|
||||
# If set, a path to write a process trace SVG to
|
||||
# debug.trace-out: ""
|
||||
|
||||
# how often to send reminders to users who need to verify their email
|
||||
# email-reminders.chore-interval: 24h0m0s
|
||||
|
||||
# enable sending emails reminding users to verify their email
|
||||
# email-reminders.enable: true
|
||||
|
||||
# amount of time before sending first reminder to users who need to verify their email
|
||||
# email-reminders.first-verification-reminder: 24h0m0s
|
||||
|
||||
# amount of time before sending second reminder to users who need to verify their email
|
||||
# email-reminders.second-verification-reminder: 120h0m0s
|
||||
|
||||
# set if expired segment cleanup is enabled or not
|
||||
# expired-deletion.enabled: true
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user