Add mail service to the satellite (#1302)

This commit is contained in:
Yaroslav Vorobiov 2019-03-02 17:22:20 +02:00 committed by GitHub
parent 54f68347ad
commit a30ba4eca8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1128 additions and 70 deletions

View File

@ -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": {},
}) })

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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 {

View 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 "" }

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
} }

View File

@ -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;">&nbsp;</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;">&nbsp;</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>

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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)

View 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
}

View 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
}

View File

@ -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

View File

@ -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">&#8232;We received a request to reset your Satellite Account password &#8232;</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">&#8232;We received a request to reset your Satellite Account password &#8232;</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;">&nbsp;</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;">&nbsp;</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">Didnt 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">Didnt 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 didnt 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 didnt 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>

View File

@ -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">&#8232;Now you can login and see who is already in your team! &#8232;</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">&#8232;Now you can login and see who is already in your team! &#8232;</span></p>
</div> </div>
</div> </div>

File diff suppressed because one or more lines are too long