Add mail service to the satellite (#1302)
This commit is contained in:
parent
54f68347ad
commit
a30ba4eca8
@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -159,10 +160,18 @@ func newNetwork(flags *Flags) (*Processes, error) {
|
||||
// satellite must wait for bootstrap to start
|
||||
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{
|
||||
"setup": {
|
||||
"--identity-dir", process.Directory,
|
||||
"--console.address", net.JoinHostPort(host, strconv.Itoa(consolePort+i)),
|
||||
"--console.static-dir", filepath.Join(storjRoot, "web/satellite/"),
|
||||
"--server.address", process.Address,
|
||||
|
||||
"--kademlia.bootstrap-addr", bootstrap.Address,
|
||||
@ -171,6 +180,10 @@ func newNetwork(flags *Flags) (*Processes, error) {
|
||||
|
||||
"--server.extensions.revocation=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": {},
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information
|
||||
|
||||
package mail
|
||||
package post
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -10,15 +10,14 @@ import (
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message is RFC compliant email message
|
||||
type Message struct {
|
||||
From mail.Address
|
||||
To []mail.Address
|
||||
From Address
|
||||
To []Address
|
||||
Subject string
|
||||
ID string
|
||||
Date time.Time
|
@ -1,7 +1,7 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information
|
||||
|
||||
package gmail
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -16,7 +16,7 @@ import (
|
||||
"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 {
|
||||
UserEmail string
|
||||
|
||||
@ -43,7 +43,7 @@ func (auth *Auth) Next(fromServer []byte, more bool) (toServer []byte, err error
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Token represents OAUTH2 token
|
||||
// Token represents OAuth2 token
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
@ -51,14 +51,14 @@ type Token struct {
|
||||
Expiry time.Time `json:"expiry"`
|
||||
}
|
||||
|
||||
// Credentials represents OAUTH2 credentials
|
||||
// Credentials represents OAuth2 credentials
|
||||
type Credentials struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
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 {
|
||||
mu sync.Mutex
|
||||
token Token
|
@ -1,7 +1,7 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information
|
||||
|
||||
package mail
|
||||
package post
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@ -12,20 +12,28 @@ import (
|
||||
"github.com/zeebo/errs"
|
||||
)
|
||||
|
||||
// Address is alias of net/mail.Address
|
||||
type Address = mail.Address
|
||||
|
||||
// SMTPSender is smtp sender
|
||||
type SMTPSender struct {
|
||||
ServerAddress string
|
||||
|
||||
From mail.Address
|
||||
From Address
|
||||
Auth smtp.Auth
|
||||
}
|
||||
|
||||
// FromAddress implements satellite/mail.SMTPSender
|
||||
func (sender *SMTPSender) FromAddress() Address {
|
||||
return sender.From
|
||||
}
|
||||
|
||||
// SendEmail sends email message to the given recipient
|
||||
func (sender *SMTPSender) SendEmail(msg *Message) error {
|
||||
host, _, err := net.SplitHostPort(sender.ServerAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: validate address before initializing SMTPSender
|
||||
// suppress error because address should be validated
|
||||
// before creating SMTPSender
|
||||
host, _, _ := net.SplitHostPort(sender.ServerAddress)
|
||||
|
||||
client, err := smtp.Dial(sender.ServerAddress)
|
||||
if err != nil {
|
||||
@ -73,5 +81,8 @@ func (sender *SMTPSender) SendEmail(msg *Message) error {
|
||||
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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -45,6 +46,7 @@ import (
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleweb"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/satellitedb"
|
||||
"storj.io/storj/storagenode"
|
||||
"storj.io/storj/storagenode/storagenodedb"
|
||||
@ -439,6 +441,11 @@ func (planet *Planet) newSatellites(count int) ([]*satellite.Peer, error) {
|
||||
Rollup: rollup.Config{
|
||||
Interval: 120 * time.Second,
|
||||
},
|
||||
Mail: mailservice.Config{
|
||||
SMTPServerAddress: "smtp.gmail.com:587",
|
||||
From: "Labs <yaroslav-satellite-test@storj.io>",
|
||||
AuthType: "simulate",
|
||||
},
|
||||
Console: consoleweb.Config{
|
||||
Address: "127.0.0.1:0",
|
||||
PasswordCost: console.TestPasswordCost,
|
||||
@ -448,8 +455,16 @@ func (planet *Planet) newSatellites(count int) ([]*satellite.Peer, error) {
|
||||
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
|
||||
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)
|
||||
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/skyrings/skyring-common/tools/uuid"
|
||||
|
||||
"storj.io/storj/internal/post"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -50,7 +52,7 @@ const (
|
||||
)
|
||||
|
||||
// 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{
|
||||
Name: Mutation,
|
||||
Fields: graphql.Fields{
|
||||
@ -68,7 +70,26 @@ func rootMutation(service *console.Service, types Types) *graphql.Object {
|
||||
|
||||
user, err := service.CreateUser(p.Context, createUser)
|
||||
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
|
||||
|
@ -14,15 +14,30 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"storj.io/storj/internal/post"
|
||||
"storj.io/storj/internal/testcontext"
|
||||
"storj.io/storj/pkg/auth"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"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) {
|
||||
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
|
||||
ctx := testcontext.New(t)
|
||||
@ -41,7 +56,16 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -65,7 +89,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
ctx,
|
||||
rootUser.ID,
|
||||
createUser.Email,
|
||||
rootUser.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -77,7 +100,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
Schema: schema,
|
||||
Context: ctx,
|
||||
RequestString: query,
|
||||
RootObject: make(map[string]interface{}),
|
||||
RootObject: rootObject,
|
||||
})
|
||||
|
||||
for _, err := range result.Errors {
|
||||
@ -129,7 +152,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
Schema: schema,
|
||||
Context: ctx,
|
||||
RequestString: query,
|
||||
RootObject: make(map[string]interface{}),
|
||||
RootObject: rootObject,
|
||||
})
|
||||
|
||||
for _, err := range result.Errors {
|
||||
@ -159,7 +182,7 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
Schema: schema,
|
||||
Context: authCtx,
|
||||
RequestString: query,
|
||||
RootObject: make(map[string]interface{}),
|
||||
RootObject: rootObject,
|
||||
})
|
||||
|
||||
for _, err := range result.Errors {
|
||||
@ -372,7 +395,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
ctx,
|
||||
user1.ID,
|
||||
"u1@email.net",
|
||||
user1.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err, project)
|
||||
@ -401,7 +423,6 @@ func TestGrapqhlMutation(t *testing.T) {
|
||||
ctx,
|
||||
user2.ID,
|
||||
"u2@email.net",
|
||||
user2.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err, project)
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
)
|
||||
|
||||
@ -39,8 +40,17 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
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{}
|
||||
if err = creator.Create(service); err != nil {
|
||||
if err = creator.Create(service, mailService); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -72,7 +82,6 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
ctx,
|
||||
rootUser.ID,
|
||||
"mtest@email.com",
|
||||
rootUser.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -101,7 +110,7 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
Schema: schema,
|
||||
Context: authCtx,
|
||||
RequestString: query,
|
||||
RootObject: make(map[string]interface{}),
|
||||
RootObject: rootObject,
|
||||
})
|
||||
|
||||
for _, err := range result.Errors {
|
||||
@ -204,7 +213,6 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
ctx,
|
||||
user1.ID,
|
||||
"muu1@email.com",
|
||||
user1.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -235,7 +243,6 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
ctx,
|
||||
user2.ID,
|
||||
"muu2@email.com",
|
||||
user2.CreatedAt.Add(time.Hour*24),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -9,18 +9,19 @@ import (
|
||||
"github.com/graphql-go/graphql"
|
||||
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
)
|
||||
|
||||
// creatingSchemaMutex locks graphql.NewSchema method because it's not thread-safe
|
||||
var creatingSchemaMutex sync.Mutex
|
||||
|
||||
// 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()
|
||||
defer creatingSchemaMutex.Unlock()
|
||||
|
||||
creator := TypeCreator{}
|
||||
err := creator.Create(service)
|
||||
err := creator.Create(service, mailService)
|
||||
if err != nil {
|
||||
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 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>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/graphql-go/graphql"
|
||||
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
)
|
||||
|
||||
// Types return graphql type objects
|
||||
@ -44,7 +45,7 @@ type TypeCreator struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
c.userInput = graphqlUserInput(c)
|
||||
if err := c.userInput.Error(); err != nil {
|
||||
@ -93,7 +94,7 @@ func (c *TypeCreator) Create(service *console.Service) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutation = rootMutation(service, c)
|
||||
c.mutation = rootMutation(service, mailService, c)
|
||||
if err := c.mutation.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"storj.io/storj/pkg/auth"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -45,21 +46,24 @@ type Config struct {
|
||||
type Server struct {
|
||||
log *zap.Logger
|
||||
|
||||
config Config
|
||||
service *console.Service
|
||||
config Config
|
||||
service *console.Service
|
||||
mailService *mailservice.Service
|
||||
|
||||
listener net.Listener
|
||||
server http.Server
|
||||
|
||||
schema graphql.Schema
|
||||
server http.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{
|
||||
log: logger,
|
||||
service: service,
|
||||
config: config,
|
||||
listener: listener,
|
||||
log: logger,
|
||||
config: config,
|
||||
listener: listener,
|
||||
service: service,
|
||||
mailService: mailService,
|
||||
}
|
||||
|
||||
logger.Debug("Starting Satellite UI...")
|
||||
@ -105,13 +109,18 @@ func (s *Server) grapqlHandler(w http.ResponseWriter, req *http.Request) {
|
||||
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{
|
||||
Schema: s.schema,
|
||||
Context: ctx,
|
||||
RequestString: query.Query,
|
||||
VariableValues: query.Variables,
|
||||
OperationName: query.OperationName,
|
||||
RootObject: make(map[string]interface{}),
|
||||
RootObject: rootObject,
|
||||
})
|
||||
|
||||
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
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
var err error
|
||||
s.schema, err = consoleql.CreateSchema(s.service)
|
||||
s.schema, err = consoleql.CreateSchema(s.service, s.mailService)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
@ -62,7 +62,12 @@ func NewService(log *zap.Logger, signer Signer, store DB, passwordCost int) (*Se
|
||||
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
|
||||
@ -93,23 +98,18 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser) (u *User, err
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
//TODO: activation token should differ from auth token
|
||||
claims := &consoleauth.Claims{
|
||||
ID: id,
|
||||
Email: email,
|
||||
Expiration: expirationDate,
|
||||
Expiration: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
|
||||
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"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -16,6 +18,8 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
"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/rollup"
|
||||
"storj.io/storj/pkg/accounting/tally"
|
||||
@ -41,6 +45,8 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"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/boltdb"
|
||||
"storj.io/storj/storage/storelogger"
|
||||
@ -97,6 +103,7 @@ type Config struct {
|
||||
Tally tally.Config
|
||||
Rollup rollup.Config
|
||||
|
||||
Mail mailservice.Config
|
||||
Console consoleweb.Config
|
||||
}
|
||||
|
||||
@ -163,6 +170,10 @@ type Peer struct {
|
||||
Rollup *rollup.Rollup
|
||||
}
|
||||
|
||||
Mail struct {
|
||||
Service *mailservice.Service
|
||||
}
|
||||
|
||||
Console struct {
|
||||
Listener net.Listener
|
||||
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)
|
||||
}
|
||||
|
||||
{ // setup console
|
||||
log.Debug("Setting up console")
|
||||
config := config.Console
|
||||
{ // setup mailservice
|
||||
log.Debug("Setting up mail service")
|
||||
// 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 {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
peer.Console.Service, err = console.NewService(peer.Log.Named("console:service"),
|
||||
// TODO: use satellite key
|
||||
// validate smtp server address
|
||||
host, _, err := net.SplitHostPort(mailConfig.SMTPServerAddress)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
var sender mailservice.Sender
|
||||
switch mailConfig.AuthType {
|
||||
case "oauth2":
|
||||
creds := oauth2.Credentials{
|
||||
ClientID: mailConfig.ClientID,
|
||||
ClientSecret: mailConfig.ClientSecret,
|
||||
TokenURI: mailConfig.TokenURI,
|
||||
}
|
||||
token, err := oauth2.RefreshToken(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")},
|
||||
peer.DB.Console(),
|
||||
config.PasswordCost,
|
||||
consoleConfig.PasswordCost,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
peer.Console.Endpoint = consoleweb.NewServer(peer.Log.Named("console:endpoint"),
|
||||
config,
|
||||
peer.Console.Endpoint = consoleweb.NewServer(
|
||||
peer.Log.Named("console:endpoint"),
|
||||
consoleConfig,
|
||||
peer.Console.Service,
|
||||
peer.Console.Listener)
|
||||
peer.Mail.Service,
|
||||
peer.Console.Listener,
|
||||
)
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
|
@ -493,7 +493,7 @@
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
|
||||
@ -511,7 +511,7 @@
|
||||
|
||||
<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;">
|
||||
<![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>
|
||||
</div>
|
||||
|
||||
@ -531,7 +531,7 @@
|
||||
<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">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>
|
||||
|
@ -475,7 +475,7 @@
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
|
||||
@ -493,7 +493,7 @@
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
|
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