Add mail service to the satellite (#1302)
This commit is contained in:
parent
54f68347ad
commit
a30ba4eca8
@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -159,10 +160,18 @@ func newNetwork(flags *Flags) (*Processes, error) {
|
|||||||
// satellite must wait for bootstrap to start
|
// satellite must wait for bootstrap to start
|
||||||
process.WaitForStart(bootstrap)
|
process.WaitForStart(bootstrap)
|
||||||
|
|
||||||
|
// TODO: find source file, to set static path
|
||||||
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.Combine(processes.Close(), errs.New("no caller information"))
|
||||||
|
}
|
||||||
|
storjRoot := strings.TrimSuffix(filename, "/cmd/storj-sim/network.go")
|
||||||
|
|
||||||
process.Arguments = withCommon(Arguments{
|
process.Arguments = withCommon(Arguments{
|
||||||
"setup": {
|
"setup": {
|
||||||
"--identity-dir", process.Directory,
|
"--identity-dir", process.Directory,
|
||||||
"--console.address", net.JoinHostPort(host, strconv.Itoa(consolePort+i)),
|
"--console.address", net.JoinHostPort(host, strconv.Itoa(consolePort+i)),
|
||||||
|
"--console.static-dir", filepath.Join(storjRoot, "web/satellite/"),
|
||||||
"--server.address", process.Address,
|
"--server.address", process.Address,
|
||||||
|
|
||||||
"--kademlia.bootstrap-addr", bootstrap.Address,
|
"--kademlia.bootstrap-addr", bootstrap.Address,
|
||||||
@ -171,6 +180,10 @@ func newNetwork(flags *Flags) (*Processes, error) {
|
|||||||
|
|
||||||
"--server.extensions.revocation=false",
|
"--server.extensions.revocation=false",
|
||||||
"--server.use-peer-ca-whitelist=false",
|
"--server.use-peer-ca-whitelist=false",
|
||||||
|
|
||||||
|
"--mail.smtp-server-address", "smtp.gmail.com:587",
|
||||||
|
"--mail.from", "Storj <yaroslav-satellite-test@storj.io>",
|
||||||
|
"--mail.template-path", filepath.Join(storjRoot, "web/satellite/static/emails"),
|
||||||
},
|
},
|
||||||
"run": {},
|
"run": {},
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information
|
// See LICENSE for copying information
|
||||||
|
|
||||||
package mail
|
package post
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -10,15 +10,14 @@ import (
|
|||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
"net/mail"
|
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message is RFC compliant email message
|
// Message is RFC compliant email message
|
||||||
type Message struct {
|
type Message struct {
|
||||||
From mail.Address
|
From Address
|
||||||
To []mail.Address
|
To []Address
|
||||||
Subject string
|
Subject string
|
||||||
ID string
|
ID string
|
||||||
Date time.Time
|
Date time.Time
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information
|
// See LICENSE for copying information
|
||||||
|
|
||||||
package gmail
|
package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auth is XOAUTH2 implementation of smtp.Auth interface for gmail
|
// Auth is XOAUTH2 implementation of smtp.Auth interface
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
UserEmail string
|
UserEmail string
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ func (auth *Auth) Next(fromServer []byte, more bool) (toServer []byte, err error
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token represents OAUTH2 token
|
// Token represents OAuth2 token
|
||||||
type Token struct {
|
type Token struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
@ -51,14 +51,14 @@ type Token struct {
|
|||||||
Expiry time.Time `json:"expiry"`
|
Expiry time.Time `json:"expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credentials represents OAUTH2 credentials
|
// Credentials represents OAuth2 credentials
|
||||||
type Credentials struct {
|
type Credentials struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
TokenURI string `json:"token_uri"`
|
TokenURI string `json:"token_uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenStore is a thread safe storage for OAUTH2 token and credentials
|
// TokenStore is a thread safe storage for OAuth2 token and credentials
|
||||||
type TokenStore struct {
|
type TokenStore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
token Token
|
token Token
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information
|
// See LICENSE for copying information
|
||||||
|
|
||||||
package mail
|
package post
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
@ -12,20 +12,28 @@ import (
|
|||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Address is alias of net/mail.Address
|
||||||
|
type Address = mail.Address
|
||||||
|
|
||||||
// SMTPSender is smtp sender
|
// SMTPSender is smtp sender
|
||||||
type SMTPSender struct {
|
type SMTPSender struct {
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
|
|
||||||
From mail.Address
|
From Address
|
||||||
Auth smtp.Auth
|
Auth smtp.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromAddress implements satellite/mail.SMTPSender
|
||||||
|
func (sender *SMTPSender) FromAddress() Address {
|
||||||
|
return sender.From
|
||||||
|
}
|
||||||
|
|
||||||
// SendEmail sends email message to the given recipient
|
// SendEmail sends email message to the given recipient
|
||||||
func (sender *SMTPSender) SendEmail(msg *Message) error {
|
func (sender *SMTPSender) SendEmail(msg *Message) error {
|
||||||
host, _, err := net.SplitHostPort(sender.ServerAddress)
|
// TODO: validate address before initializing SMTPSender
|
||||||
if err != nil {
|
// suppress error because address should be validated
|
||||||
return err
|
// before creating SMTPSender
|
||||||
}
|
host, _, _ := net.SplitHostPort(sender.ServerAddress)
|
||||||
|
|
||||||
client, err := smtp.Dial(sender.ServerAddress)
|
client, err := smtp.Dial(sender.ServerAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,5 +81,8 @@ func (sender *SMTPSender) SendEmail(msg *Message) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Quit()
|
// send quit msg to stop gracefully returns err on
|
||||||
|
// success but we don't really care about the result
|
||||||
|
_ = client.Quit()
|
||||||
|
return nil
|
||||||
}
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -45,6 +46,7 @@ import (
|
|||||||
"storj.io/storj/satellite"
|
"storj.io/storj/satellite"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleweb"
|
"storj.io/storj/satellite/console/consoleweb"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/satellitedb"
|
"storj.io/storj/satellite/satellitedb"
|
||||||
"storj.io/storj/storagenode"
|
"storj.io/storj/storagenode"
|
||||||
"storj.io/storj/storagenode/storagenodedb"
|
"storj.io/storj/storagenode/storagenodedb"
|
||||||
@ -439,6 +441,11 @@ func (planet *Planet) newSatellites(count int) ([]*satellite.Peer, error) {
|
|||||||
Rollup: rollup.Config{
|
Rollup: rollup.Config{
|
||||||
Interval: 120 * time.Second,
|
Interval: 120 * time.Second,
|
||||||
},
|
},
|
||||||
|
Mail: mailservice.Config{
|
||||||
|
SMTPServerAddress: "smtp.gmail.com:587",
|
||||||
|
From: "Labs <yaroslav-satellite-test@storj.io>",
|
||||||
|
AuthType: "simulate",
|
||||||
|
},
|
||||||
Console: consoleweb.Config{
|
Console: consoleweb.Config{
|
||||||
Address: "127.0.0.1:0",
|
Address: "127.0.0.1:0",
|
||||||
PasswordCost: console.TestPasswordCost,
|
PasswordCost: console.TestPasswordCost,
|
||||||
@ -448,8 +455,16 @@ func (planet *Planet) newSatellites(count int) ([]*satellite.Peer, error) {
|
|||||||
planet.config.Reconfigure.Satellite(log, i, &config)
|
planet.config.Reconfigure.Satellite(log, i, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: find source file, to set static path
|
||||||
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
return xs, errs.New("no caller information")
|
||||||
|
}
|
||||||
|
storjRoot := strings.TrimSuffix(filename, "/internal/testplanet/planet.go")
|
||||||
|
|
||||||
// TODO: for development only
|
// TODO: for development only
|
||||||
config.Console.StaticDir = "./web/satellite"
|
config.Console.StaticDir = filepath.Join(storjRoot, "web/satellite")
|
||||||
|
config.Mail.TemplatePath = filepath.Join(storjRoot, "web/satellite/static/emails")
|
||||||
|
|
||||||
peer, err := satellite.New(log, identity, db, &config)
|
peer, err := satellite.New(log, identity, db, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
44
satellite/console/consoleweb/consoleql/mail.go
Normal file
44
satellite/console/consoleweb/consoleql/mail.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information
|
||||||
|
|
||||||
|
package consoleql
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ActivationPath is key for pass which handles account activation
|
||||||
|
ActivationPath = "activationPath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountActivationEmail is mailservice template with activation data
|
||||||
|
type AccountActivationEmail struct {
|
||||||
|
ActivationLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template returns email template name
|
||||||
|
func (*AccountActivationEmail) Template() string { return "Welcome" }
|
||||||
|
|
||||||
|
// Subject gets email subject
|
||||||
|
func (*AccountActivationEmail) Subject() string { return "Activate your email" }
|
||||||
|
|
||||||
|
// ForgotPasswordEmail is mailservice template with reset password data
|
||||||
|
type ForgotPasswordEmail struct {
|
||||||
|
UserName string
|
||||||
|
ResetLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template returns email template name
|
||||||
|
func (*ForgotPasswordEmail) Template() string { return "Forgot" }
|
||||||
|
|
||||||
|
// Subject gets email subject
|
||||||
|
func (*ForgotPasswordEmail) Subject() string { return "" }
|
||||||
|
|
||||||
|
// ProjectInvitationEmail is mailservice template for project invitation email
|
||||||
|
type ProjectInvitationEmail struct {
|
||||||
|
UserName string
|
||||||
|
ProjectName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template returns email template name
|
||||||
|
func (*ProjectInvitationEmail) Template() string { return "Invite" }
|
||||||
|
|
||||||
|
// Subject gets email subject
|
||||||
|
func (*ProjectInvitationEmail) Subject() string { return "" }
|
@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/graphql-go/graphql"
|
"github.com/graphql-go/graphql"
|
||||||
"github.com/skyrings/skyring-common/tools/uuid"
|
"github.com/skyrings/skyring-common/tools/uuid"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/post"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -50,7 +52,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// rootMutation creates mutation for graphql populated by AccountsClient
|
// rootMutation creates mutation for graphql populated by AccountsClient
|
||||||
func rootMutation(service *console.Service, types Types) *graphql.Object {
|
func rootMutation(service *console.Service, mailService *mailservice.Service, types Types) *graphql.Object {
|
||||||
return graphql.NewObject(graphql.ObjectConfig{
|
return graphql.NewObject(graphql.ObjectConfig{
|
||||||
Name: Mutation,
|
Name: Mutation,
|
||||||
Fields: graphql.Fields{
|
Fields: graphql.Fields{
|
||||||
@ -68,7 +70,26 @@ func rootMutation(service *console.Service, types Types) *graphql.Object {
|
|||||||
|
|
||||||
user, err := service.CreateUser(p.Context, createUser)
|
user, err := service.CreateUser(p.Context, createUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := service.GenerateActivationToken(p.Context, user.ID, user.Email)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootObject := p.Info.RootValue.(map[string]interface{})
|
||||||
|
link := rootObject["origin"].(string) + rootObject[ActivationPath].(string) + token
|
||||||
|
|
||||||
|
err = mailService.SendRendered(
|
||||||
|
p.Context,
|
||||||
|
[]post.Address{{Address: user.Email, Name: user.FirstName}},
|
||||||
|
&AccountActivationEmail{
|
||||||
|
ActivationLink: link,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
|
@ -14,15 +14,30 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/post"
|
||||||
"storj.io/storj/internal/testcontext"
|
"storj.io/storj/internal/testcontext"
|
||||||
"storj.io/storj/pkg/auth"
|
"storj.io/storj/pkg/auth"
|
||||||
"storj.io/storj/satellite"
|
"storj.io/storj/satellite"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleauth"
|
"storj.io/storj/satellite/console/consoleauth"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// discardSender discard sending of an actual email
|
||||||
|
type discardSender struct{}
|
||||||
|
|
||||||
|
// SendEmail immediately returns with nil error
|
||||||
|
func (*discardSender) SendEmail(msg *post.Message) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromAddress returns empty post.Address
|
||||||
|
func (*discardSender) FromAddress() post.Address {
|
||||||
|
return post.Address{}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGrapqhlMutation(t *testing.T) {
|
func TestGrapqhlMutation(t *testing.T) {
|
||||||
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
|
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
|
||||||
ctx := testcontext.New(t)
|
ctx := testcontext.New(t)
|
||||||
@ -41,7 +56,16 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
schema, err := consoleql.CreateSchema(service)
|
mailService, err := mailservice.New(log, &discardSender{}, "testdata")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootObject := make(map[string]interface{})
|
||||||
|
rootObject["origin"] = "http://doesntmatter.com/"
|
||||||
|
rootObject[consoleql.ActivationPath] = "?activationToken="
|
||||||
|
|
||||||
|
schema, err := consoleql.CreateSchema(service, mailService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -65,7 +89,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
rootUser.ID,
|
rootUser.ID,
|
||||||
createUser.Email,
|
createUser.Email,
|
||||||
rootUser.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -77,7 +100,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
Schema: schema,
|
Schema: schema,
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
RequestString: query,
|
RequestString: query,
|
||||||
RootObject: make(map[string]interface{}),
|
RootObject: rootObject,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, err := range result.Errors {
|
for _, err := range result.Errors {
|
||||||
@ -129,7 +152,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
Schema: schema,
|
Schema: schema,
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
RequestString: query,
|
RequestString: query,
|
||||||
RootObject: make(map[string]interface{}),
|
RootObject: rootObject,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, err := range result.Errors {
|
for _, err := range result.Errors {
|
||||||
@ -159,7 +182,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
Schema: schema,
|
Schema: schema,
|
||||||
Context: authCtx,
|
Context: authCtx,
|
||||||
RequestString: query,
|
RequestString: query,
|
||||||
RootObject: make(map[string]interface{}),
|
RootObject: rootObject,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, err := range result.Errors {
|
for _, err := range result.Errors {
|
||||||
@ -372,7 +395,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
user1.ID,
|
user1.ID,
|
||||||
"u1@email.net",
|
"u1@email.net",
|
||||||
user1.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err, project)
|
t.Fatal(err, project)
|
||||||
@ -401,7 +423,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
user2.ID,
|
user2.ID,
|
||||||
"u2@email.net",
|
"u2@email.net",
|
||||||
user2.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err, project)
|
t.Fatal(err, project)
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleauth"
|
"storj.io/storj/satellite/console/consoleauth"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,8 +40,17 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mailService, err := mailservice.New(log, &discardSender{}, "testdata")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootObject := make(map[string]interface{})
|
||||||
|
rootObject["origin"] = "http://doesntmatter.com/"
|
||||||
|
rootObject[consoleql.ActivationPath] = "?activationToken="
|
||||||
|
|
||||||
creator := consoleql.TypeCreator{}
|
creator := consoleql.TypeCreator{}
|
||||||
if err = creator.Create(service); err != nil {
|
if err = creator.Create(service, mailService); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +82,6 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
rootUser.ID,
|
rootUser.ID,
|
||||||
"mtest@email.com",
|
"mtest@email.com",
|
||||||
rootUser.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -101,7 +110,7 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
Schema: schema,
|
Schema: schema,
|
||||||
Context: authCtx,
|
Context: authCtx,
|
||||||
RequestString: query,
|
RequestString: query,
|
||||||
RootObject: make(map[string]interface{}),
|
RootObject: rootObject,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, err := range result.Errors {
|
for _, err := range result.Errors {
|
||||||
@ -204,7 +213,6 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
user1.ID,
|
user1.ID,
|
||||||
"muu1@email.com",
|
"muu1@email.com",
|
||||||
user1.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -235,7 +243,6 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
ctx,
|
ctx,
|
||||||
user2.ID,
|
user2.ID,
|
||||||
"muu2@email.com",
|
"muu2@email.com",
|
||||||
user2.CreatedAt.Add(time.Hour*24),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -9,18 +9,19 @@ import (
|
|||||||
"github.com/graphql-go/graphql"
|
"github.com/graphql-go/graphql"
|
||||||
|
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
// creatingSchemaMutex locks graphql.NewSchema method because it's not thread-safe
|
// creatingSchemaMutex locks graphql.NewSchema method because it's not thread-safe
|
||||||
var creatingSchemaMutex sync.Mutex
|
var creatingSchemaMutex sync.Mutex
|
||||||
|
|
||||||
// CreateSchema creates both type
|
// CreateSchema creates both type
|
||||||
func CreateSchema(service *console.Service) (graphql.Schema, error) {
|
func CreateSchema(service *console.Service, mailService *mailservice.Service) (graphql.Schema, error) {
|
||||||
creatingSchemaMutex.Lock()
|
creatingSchemaMutex.Lock()
|
||||||
defer creatingSchemaMutex.Unlock()
|
defer creatingSchemaMutex.Unlock()
|
||||||
|
|
||||||
creator := TypeCreator{}
|
creator := TypeCreator{}
|
||||||
err := creator.Create(service)
|
err := creator.Create(service, mailService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return graphql.Schema{}, err
|
return graphql.Schema{}, err
|
||||||
}
|
}
|
||||||
|
@ -530,7 +530,7 @@ and setup your API keys within a team and project</span></p>
|
|||||||
|
|
||||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||||
<div class="btn btn--flat btn--large" style="text-align:left;">
|
<div class="btn btn--flat btn--large" style="text-align:left;">
|
||||||
<![if !mso]><a 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;" href="https://storj.io/">Verify your account</a><![endif]>
|
<a 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;" href="{{ .ActivationLink }}">Verify your account</a>
|
||||||
<!--[if mso]><p style="line-height:0;margin:0;"> </p><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://storj.io/" 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">Verify your account</center></v:textbox></v:roundrect><![endif]--></div>
|
<!--[if mso]><p style="line-height:0;margin:0;"> </p><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://storj.io/" 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">Verify your account</center></v:textbox></v:roundrect><![endif]--></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/graphql-go/graphql"
|
"github.com/graphql-go/graphql"
|
||||||
|
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Types return graphql type objects
|
// Types return graphql type objects
|
||||||
@ -44,7 +45,7 @@ type TypeCreator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create create types and check for error
|
// Create create types and check for error
|
||||||
func (c *TypeCreator) Create(service *console.Service) error {
|
func (c *TypeCreator) Create(service *console.Service, mailService *mailservice.Service) error {
|
||||||
// inputs
|
// inputs
|
||||||
c.userInput = graphqlUserInput(c)
|
c.userInput = graphqlUserInput(c)
|
||||||
if err := c.userInput.Error(); err != nil {
|
if err := c.userInput.Error(); err != nil {
|
||||||
@ -93,7 +94,7 @@ func (c *TypeCreator) Create(service *console.Service) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mutation = rootMutation(service, c)
|
c.mutation = rootMutation(service, mailService, c)
|
||||||
if err := c.mutation.Error(); err != nil {
|
if err := c.mutation.Error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"storj.io/storj/pkg/auth"
|
"storj.io/storj/pkg/auth"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -47,19 +48,22 @@ type Server struct {
|
|||||||
|
|
||||||
config Config
|
config Config
|
||||||
service *console.Service
|
service *console.Service
|
||||||
|
mailService *mailservice.Service
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
server http.Server
|
||||||
|
|
||||||
schema graphql.Schema
|
schema graphql.Schema
|
||||||
server http.Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates new instance of console server
|
// NewServer creates new instance of console server
|
||||||
func NewServer(logger *zap.Logger, config Config, service *console.Service, listener net.Listener) *Server {
|
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, listener net.Listener) *Server {
|
||||||
server := Server{
|
server := Server{
|
||||||
log: logger,
|
log: logger,
|
||||||
service: service,
|
|
||||||
config: config,
|
config: config,
|
||||||
listener: listener,
|
listener: listener,
|
||||||
|
service: service,
|
||||||
|
mailService: mailService,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Starting Satellite UI...")
|
logger.Debug("Starting Satellite UI...")
|
||||||
@ -105,13 +109,18 @@ func (s *Server) grapqlHandler(w http.ResponseWriter, req *http.Request) {
|
|||||||
ctx = console.WithAuth(ctx, auth)
|
ctx = console.WithAuth(ctx, auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootObject := make(map[string]interface{})
|
||||||
|
//TODO: add public address to config for production
|
||||||
|
rootObject["origin"] = "http://" + s.listener.Addr().String() + "/"
|
||||||
|
rootObject[consoleql.ActivationPath] = "?activationToken="
|
||||||
|
|
||||||
result := graphql.Do(graphql.Params{
|
result := graphql.Do(graphql.Params{
|
||||||
Schema: s.schema,
|
Schema: s.schema,
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
RequestString: query.Query,
|
RequestString: query.Query,
|
||||||
VariableValues: query.Variables,
|
VariableValues: query.Variables,
|
||||||
OperationName: query.OperationName,
|
OperationName: query.OperationName,
|
||||||
RootObject: make(map[string]interface{}),
|
RootObject: rootObject,
|
||||||
})
|
})
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(result)
|
err = json.NewEncoder(w).Encode(result)
|
||||||
@ -127,7 +136,7 @@ func (s *Server) grapqlHandler(w http.ResponseWriter, req *http.Request) {
|
|||||||
// Run starts the server that host webapp and api endpoint
|
// Run starts the server that host webapp and api endpoint
|
||||||
func (s *Server) Run(ctx context.Context) error {
|
func (s *Server) Run(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
s.schema, err = consoleql.CreateSchema(s.service)
|
s.schema, err = consoleql.CreateSchema(s.service, s.mailService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,12 @@ func NewService(log *zap.Logger, signer Signer, store DB, passwordCost int) (*Se
|
|||||||
passwordCost = bcrypt.DefaultCost
|
passwordCost = bcrypt.DefaultCost
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Service{Signer: signer, store: store, log: log, passwordCost: passwordCost}, nil
|
return &Service{
|
||||||
|
Signer: signer,
|
||||||
|
store: store,
|
||||||
|
log: log,
|
||||||
|
passwordCost: passwordCost,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser gets password hash value and creates new inactive User
|
// CreateUser gets password hash value and creates new inactive User
|
||||||
@ -93,23 +98,18 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser) (u *User, err
|
|||||||
PasswordHash: hash,
|
PasswordHash: hash,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: send "finish registration email" when email service will be ready
|
|
||||||
//activationToken, err := s.GenerateActivationToken(ctx, u.ID, email, u.CreatedAt.Add(tokenExpirationTime))
|
|
||||||
//if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
//}
|
|
||||||
|
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateActivationToken - is a method for generating activation token
|
// GenerateActivationToken - is a method for generating activation token
|
||||||
func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, email string, expirationDate time.Time) (activationToken string, err error) {
|
func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, email string) (activationToken string, err error) {
|
||||||
defer mon.Task()(&ctx)(&err)
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
|
//TODO: activation token should differ from auth token
|
||||||
claims := &consoleauth.Claims{
|
claims := &consoleauth.Claims{
|
||||||
ID: id,
|
ID: id,
|
||||||
Email: email,
|
Email: email,
|
||||||
Expiration: expirationDate,
|
Expiration: time.Now().Add(time.Hour * 24),
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.createToken(claims)
|
return s.createToken(claims)
|
||||||
|
130
satellite/mailservice/service.go
Normal file
130
satellite/mailservice/service.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information
|
||||||
|
|
||||||
|
package mailservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
htmltemplate "html/template"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
monkit "gopkg.in/spacemonkeygo/monkit.v2"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/post"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines values needed by mailservice service
|
||||||
|
type Config struct {
|
||||||
|
SMTPServerAddress string `help:"smtp server address" default:""`
|
||||||
|
TemplatePath string `help:"path to email templates source" default:""`
|
||||||
|
From string `help:"sender email address" default:""`
|
||||||
|
AuthType string `help:"smtp authentication type" default:"simulate"`
|
||||||
|
PlainLogin string `help:"plain auth user login" default:""`
|
||||||
|
PlainPassword string `help:"plain auth user password" default:""`
|
||||||
|
RefreshToken string `help:"refresh token used to retrieve new access token" default:""`
|
||||||
|
ClientID string `help:"oauth2 app's client id" default:""`
|
||||||
|
ClientSecret string `help:"oauth2 app's client secret" default:""`
|
||||||
|
TokenURI string `help:"uri which is used when retrieving new access token" default:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mon = monkit.Package()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sender is
|
||||||
|
type Sender interface {
|
||||||
|
SendEmail(msg *post.Message) error
|
||||||
|
FromAddress() post.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message defines mailservice template-backed message for SendRendered method
|
||||||
|
type Message interface {
|
||||||
|
Template() string
|
||||||
|
Subject() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service sends template-backed email messages through SMTP
|
||||||
|
type Service struct {
|
||||||
|
log *zap.Logger
|
||||||
|
sender Sender
|
||||||
|
|
||||||
|
html *htmltemplate.Template
|
||||||
|
// TODO(yar): prepare plain text version
|
||||||
|
//text *texttemplate.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates new service
|
||||||
|
func New(log *zap.Logger, sender Sender, templatePath string) (*Service, error) {
|
||||||
|
var err error
|
||||||
|
service := &Service{log: log, sender: sender}
|
||||||
|
|
||||||
|
// TODO(yar): prepare plain text version
|
||||||
|
//service.text, err = texttemplate.ParseGlob(filepath.Join(templatePath, "*.txt"))
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
|
||||||
|
service.html, err = htmltemplate.ParseGlob(filepath.Join(templatePath, "*.html"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send is generalized method for sending custom email message
|
||||||
|
func (service *Service) Send(ctx context.Context, msg *post.Message) (err error) {
|
||||||
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
return service.sender.SendEmail(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRendered renders content from htmltemplate and texttemplate templates then sends it
|
||||||
|
func (service *Service) SendRendered(ctx context.Context, to []post.Address, msg Message) (err error) {
|
||||||
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
|
var htmlBuffer bytes.Buffer
|
||||||
|
var textBuffer bytes.Buffer
|
||||||
|
|
||||||
|
// TODO(yar): prepare plain text version
|
||||||
|
//if err = service.text.ExecuteTemplate(&textBuffer, msg.Template() + ".txt", msg); err != nil {
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
if err = service.html.ExecuteTemplate(&htmlBuffer, msg.Template()+".html", msg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &post.Message{
|
||||||
|
From: service.sender.FromAddress(),
|
||||||
|
To: to,
|
||||||
|
Subject: msg.Subject(),
|
||||||
|
PlainText: textBuffer.String(),
|
||||||
|
Parts: []post.Part{
|
||||||
|
{
|
||||||
|
Type: "text/html; charset=UTF-8",
|
||||||
|
Content: htmlBuffer.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.sender.SendEmail(m)
|
||||||
|
// log error
|
||||||
|
|
||||||
|
var recipients []string
|
||||||
|
for _, recipient := range to {
|
||||||
|
recipients = append(recipients, recipient.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
service.log.Info("error from mail sender",
|
||||||
|
zap.String("error", err.Error()),
|
||||||
|
zap.Strings("recipients", recipients))
|
||||||
|
} else {
|
||||||
|
service.log.Info("successfully send message",
|
||||||
|
zap.Strings("recipients", recipients))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
72
satellite/mailservice/simulate/linkclicker.go
Normal file
72
satellite/mailservice/simulate/linkclicker.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information
|
||||||
|
|
||||||
|
package simulate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zeebo/errs"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/post"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinkClicker is mailservice.Sender that click all links
|
||||||
|
// from html msg parts
|
||||||
|
type LinkClicker struct{}
|
||||||
|
|
||||||
|
// FromAddress return empty mail address
|
||||||
|
func (clicker *LinkClicker) FromAddress() post.Address {
|
||||||
|
return post.Address{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEmail click all links from email html parts
|
||||||
|
func (clicker *LinkClicker) SendEmail(msg *post.Message) error {
|
||||||
|
// dirty way to find links without pulling in a html dependency
|
||||||
|
regx := regexp.MustCompile(`href="([^\s])+"`)
|
||||||
|
// collect all links
|
||||||
|
var links []string
|
||||||
|
for _, part := range msg.Parts {
|
||||||
|
tags := findLinkTags(part.Content)
|
||||||
|
for _, tag := range tags {
|
||||||
|
href := regx.FindString(tag)
|
||||||
|
if href == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
links = append(links, href[len(`href="`):len(href)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// click all links
|
||||||
|
var sendError error
|
||||||
|
for _, link := range links {
|
||||||
|
_, err := http.Get(link)
|
||||||
|
sendError = errs.Combine(sendError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendError
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLinkTags(body string) []string {
|
||||||
|
var tags []string
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
stTag := strings.Index(body, "<a")
|
||||||
|
if stTag < 0 {
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped := body[stTag:]
|
||||||
|
endTag := strings.Index(stripped, "</a>")
|
||||||
|
if endTag < 0 {
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := endTag + len("</a>") + 1
|
||||||
|
body = stripped[offset:]
|
||||||
|
tags = append(tags, stripped[:offset])
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
@ -8,6 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"net/smtp"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@ -16,6 +18,8 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/post"
|
||||||
|
"storj.io/storj/internal/post/oauth2"
|
||||||
"storj.io/storj/pkg/accounting"
|
"storj.io/storj/pkg/accounting"
|
||||||
"storj.io/storj/pkg/accounting/rollup"
|
"storj.io/storj/pkg/accounting/rollup"
|
||||||
"storj.io/storj/pkg/accounting/tally"
|
"storj.io/storj/pkg/accounting/tally"
|
||||||
@ -41,6 +45,8 @@ import (
|
|||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/console/consoleauth"
|
"storj.io/storj/satellite/console/consoleauth"
|
||||||
"storj.io/storj/satellite/console/consoleweb"
|
"storj.io/storj/satellite/console/consoleweb"
|
||||||
|
"storj.io/storj/satellite/mailservice"
|
||||||
|
"storj.io/storj/satellite/mailservice/simulate"
|
||||||
"storj.io/storj/storage"
|
"storj.io/storj/storage"
|
||||||
"storj.io/storj/storage/boltdb"
|
"storj.io/storj/storage/boltdb"
|
||||||
"storj.io/storj/storage/storelogger"
|
"storj.io/storj/storage/storelogger"
|
||||||
@ -97,6 +103,7 @@ type Config struct {
|
|||||||
Tally tally.Config
|
Tally tally.Config
|
||||||
Rollup rollup.Config
|
Rollup rollup.Config
|
||||||
|
|
||||||
|
Mail mailservice.Config
|
||||||
Console consoleweb.Config
|
Console consoleweb.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +170,10 @@ type Peer struct {
|
|||||||
Rollup *rollup.Rollup
|
Rollup *rollup.Rollup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mail struct {
|
||||||
|
Service *mailservice.Service
|
||||||
|
}
|
||||||
|
|
||||||
Console struct {
|
Console struct {
|
||||||
Listener net.Listener
|
Listener net.Listener
|
||||||
Service *console.Service
|
Service *console.Service
|
||||||
@ -363,30 +374,93 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, config *Config) (*
|
|||||||
peer.Accounting.Rollup = rollup.New(peer.Log.Named("rollup"), peer.DB.Accounting(), config.Rollup.Interval)
|
peer.Accounting.Rollup = rollup.New(peer.Log.Named("rollup"), peer.DB.Accounting(), config.Rollup.Interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // setup console
|
{ // setup mailservice
|
||||||
log.Debug("Setting up console")
|
log.Debug("Setting up mail service")
|
||||||
config := config.Console
|
// TODO(yar): test multiple satellites using same OAUTH credentials
|
||||||
|
mailConfig := config.Mail
|
||||||
|
|
||||||
peer.Console.Listener, err = net.Listen("tcp", config.Address)
|
// validate from mail address
|
||||||
|
from, err := mail.ParseAddress(mailConfig.From)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Combine(err, peer.Close())
|
return nil, errs.Combine(err, peer.Close())
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.Console.Service, err = console.NewService(peer.Log.Named("console:service"),
|
// validate smtp server address
|
||||||
// TODO: use satellite key
|
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(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.PlainLogin, mailConfig.PlainPassword, host),
|
||||||
|
ServerAddress: mailConfig.SMTPServerAddress,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
sender = &simulate.LinkClicker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Mail.Service, err = mailservice.New(
|
||||||
|
peer.Log.Named("mailservice:service"),
|
||||||
|
sender,
|
||||||
|
mailConfig.TemplatePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Combine(err, peer.Close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // setup console
|
||||||
|
log.Debug("Setting up console")
|
||||||
|
consoleConfig := config.Console
|
||||||
|
|
||||||
|
peer.Console.Listener, err = net.Listen("tcp", consoleConfig.Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Combine(err, peer.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Console.Service, err = console.NewService(
|
||||||
|
peer.Log.Named("console:service"),
|
||||||
|
// TODO(yar): use satellite key
|
||||||
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
|
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
|
||||||
peer.DB.Console(),
|
peer.DB.Console(),
|
||||||
config.PasswordCost,
|
consoleConfig.PasswordCost,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Combine(err, peer.Close())
|
return nil, errs.Combine(err, peer.Close())
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.Console.Endpoint = consoleweb.NewServer(peer.Log.Named("console:endpoint"),
|
peer.Console.Endpoint = consoleweb.NewServer(
|
||||||
config,
|
peer.Log.Named("console:endpoint"),
|
||||||
|
consoleConfig,
|
||||||
peer.Console.Service,
|
peer.Console.Service,
|
||||||
peer.Console.Listener)
|
peer.Mail.Service,
|
||||||
|
peer.Console.Listener,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return peer, nil
|
return peer, nil
|
||||||
|
@ -493,7 +493,7 @@
|
|||||||
|
|
||||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||||
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
||||||
<p class="size-20" style="Margin-top: 0;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">Hi %USERNAME%,
</span></p><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">
We received a request to reset your Satellite Account password 
</span></p>
|
<p class="size-20" style="Margin-top: 0;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">Hi {{ .UserName }},
</span></p><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">
We received a request to reset your Satellite Account password 
</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -511,7 +511,7 @@
|
|||||||
|
|
||||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||||
<div class="btn btn--flat btn--large" style="text-align:left;">
|
<div class="btn btn--flat btn--large" style="text-align:left;">
|
||||||
<![if !mso]><a 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;" href="https://storj.io/">Reset Password</a><![endif]>
|
<![if !mso]><a 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;" href="{{ .ResetLink }}">Reset Password</a><![endif]>
|
||||||
<!--[if mso]><p style="line-height:0;margin:0;"> </p><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://storj.io/" 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">Reset Password</center></v:textbox></v:roundrect><![endif]--></div>
|
<!--[if mso]><p style="line-height:0;margin:0;"> </p><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://storj.io/" 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">Reset Password</center></v:textbox></v:roundrect><![endif]--></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -531,7 +531,7 @@
|
|||||||
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
||||||
<p class="size-16" style="Margin-top: 0;Margin-bottom: 0;font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 16px;line-height: 24px;" lang="x-size-16"><span class="font-montserrat">Didn’t request this change?
<br /></span></p>
|
<p class="size-16" style="Margin-top: 0;Margin-bottom: 0;font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 16px;line-height: 24px;" lang="x-size-16"><span class="font-montserrat">Didn’t request this change?
<br /></span></p>
|
||||||
<p class="size-16" style="Margin-top: 0;Margin-bottom: 0;font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 16px;line-height: 24px;" lang="x-size-16"><span class="font-montserrat">If you didn’t request a new password
|
<p class="size-16" style="Margin-top: 0;Margin-bottom: 0;font-family: montserrat,dejavu sans,verdana,sans-serif;font-size: 16px;line-height: 24px;" lang="x-size-16"><span class="font-montserrat">If you didn’t request a new password
|
||||||
<a href="https://storj.io" style="color: #2683ff; text-decoration: none; font-weight: bold">%let us know%</a><br /></span></p>
|
<a href="https://storj.io" style="color: #2683ff; text-decoration: none; font-weight: bold">let us know</a><br /></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -475,7 +475,7 @@
|
|||||||
|
|
||||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||||
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
<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>Hi %Username%,</strong></span></h1>
|
<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>Hi {{ .UserName }},</strong></span></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -493,7 +493,7 @@
|
|||||||
|
|
||||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||||
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
|
||||||
<p class="size-20" style="Margin-top: 0;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">You were invited to the <a href="https://storj.io" style="color: #2683ff; text-decoration: none; font-weight: bold">%Project Name%</a> on Satellite network</span></p><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">

Now you can login and see who is already in your team! 
</span></p>
|
<p class="size-20" style="Margin-top: 0;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">You were invited to the <a href="https://storj.io" style="color: #2683ff; text-decoration: none; font-weight: bold">{{ .ProjectName }}</a> on Satellite network</span></p><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">

Now you can login and see who is already in your team! 
</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
640
web/satellite/static/emails/Welcome.html
Normal file
640
web/satellite/static/emails/Welcome.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user