minor code style changes

This commit is contained in:
dhax 2017-09-27 17:42:14 +02:00
parent 4c4041a981
commit 232463e1db
11 changed files with 82 additions and 74 deletions

View file

@ -27,13 +27,13 @@ func NewAPI() (*chi.Mux, error) {
return nil, err return nil, err
} }
emailService, err := email.NewEmailService() mailer, err := email.NewMailer()
if err != nil { if err != nil {
return nil, err return nil, err
} }
authStore := database.NewAuthStore(db) authStore := database.NewAuthStore(db)
authResource, err := auth.NewResource(authStore, emailService) authResource, err := auth.NewResource(authStore, mailer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -46,7 +46,7 @@ func NewServer() (*Server, error) {
func (srv *Server) Start() { func (srv *Server) Start() {
log.Println("starting server...") log.Println("starting server...")
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
panic(err) panic(err)
} }
}() }()

View file

@ -12,8 +12,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// Store defines database operations on account and token data. // Storer defines database operations on account and token data.
type Store interface { type Storer interface {
GetByID(id int) (*models.Account, error) GetByID(id int) (*models.Account, error)
GetByEmail(email string) (*models.Account, error) GetByEmail(email string) (*models.Account, error)
GetByRefreshToken(token string) (*models.Account, *models.Token, error) GetByRefreshToken(token string) (*models.Account, *models.Token, error)
@ -23,21 +23,21 @@ type Store interface {
PurgeExpiredToken() error PurgeExpiredToken() error
} }
// EmailService defines methods to send account emails. // Mailer defines methods to send account emails.
type EmailService interface { type Mailer interface {
LoginToken(name, email string, c email.LoginTokenContent) error LoginToken(name, email string, c email.ContentLoginToken) error
} }
// Resource implements passwordless token authentication against a database. // Resource implements passwordless token authentication against a database.
type Resource struct { type Resource struct {
Login *LoginTokenAuth Login *LoginTokenAuth
Token *TokenAuth Token *TokenAuth
store Store store Storer
mailer EmailService mailer Mailer
} }
// NewResource returns a configured authentication resource. // NewResource returns a configured authentication resource.
func NewResource(store Store, mailer EmailService) (*Resource, error) { func NewResource(store Storer, mailer Mailer) (*Resource, error) {
loginAuth, err := NewLoginTokenAuth() loginAuth, err := NewLoginTokenAuth()
if err != nil { if err != nil {
return nil, err return nil, err
@ -55,7 +55,7 @@ func NewResource(store Store, mailer EmailService) (*Resource, error) {
mailer: mailer, mailer: mailer,
} }
resource.Cleanup() resource.cleanupTicker()
return resource, nil return resource, nil
} }
@ -75,7 +75,7 @@ func (rs *Resource) Router() *chi.Mux {
return r return r
} }
func (rs *Resource) Cleanup() { func (rs *Resource) cleanupTicker() {
ticker := time.NewTicker(time.Hour * 1) ticker := time.NewTicker(time.Hour * 1)
go func() { go func() {
for range ticker.C { for range ticker.C {

View file

@ -8,8 +8,7 @@ const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456
func randStringBytes(n int) string { func randStringBytes(n int) string {
buf := make([]byte, n) buf := make([]byte, n)
_, err := rand.Read(buf) if _, err := rand.Read(buf); err != nil {
if err != nil {
panic(err) panic(err)
} }

View file

@ -65,15 +65,15 @@ func (rs *Resource) login(w http.ResponseWriter, r *http.Request) {
lt := rs.Login.CreateToken(acc.ID) lt := rs.Login.CreateToken(acc.ID)
go func() { go func() {
err := rs.mailer.LoginToken(acc.Name, acc.Email, email.LoginTokenContent{ content := email.ContentLoginToken{
Email: acc.Email, Email: acc.Email,
Name: acc.Name, Name: acc.Name,
URL: path.Join(rs.Login.loginURL, lt.Token), URL: path.Join(rs.Login.loginURL, lt.Token),
Token: lt.Token, Token: lt.Token,
Expiry: lt.Expiry, Expiry: lt.Expiry,
}) }
if err != nil { if err := rs.mailer.LoginToken(acc.Name, acc.Email, content); err != nil {
log(r).WithField("module", "email").Error(err.Error()) log(r).WithField("module", "email").Error(err)
} }
}() }()
@ -128,6 +128,7 @@ func (rs *Resource) token(w http.ResponseWriter, r *http.Request) {
ua := user_agent.New(r.UserAgent()) ua := user_agent.New(r.UserAgent())
browser, _ := ua.Browser() browser, _ := ua.Browser()
token := &models.Token{ token := &models.Token{
Token: uuid.NewV4().String(), Token: uuid.NewV4().String(),
Expiry: time.Now().Add(time.Minute * rs.Token.jwtRefreshExpiry), Expiry: time.Now().Add(time.Minute * rs.Token.jwtRefreshExpiry),

View file

@ -25,7 +25,7 @@ import (
var ( var (
auth *Resource auth *Resource
authstore mock.AuthStore authstore mock.AuthStore
mailer mock.EmailService mailer mock.Mailer
ts *httptest.Server ts *httptest.Server
) )
@ -72,7 +72,7 @@ func TestAuthResource_login(t *testing.T) {
return &a, err return &a, err
} }
mailer.LoginTokenFn = func(n, e string, c email.LoginTokenContent) error { mailer.LoginTokenFn = func(n, e string, c email.ContentLoginToken) error {
return nil return nil
} }

View file

@ -12,8 +12,8 @@ var (
errTokenNotFound = errors.New("login token not found") errTokenNotFound = errors.New("login token not found")
) )
// LoginToken is an in-memory saved token referencing an account ID and an expiry date. // loginToken is an in-memory saved token referencing an account ID and an expiry date.
type LoginToken struct { type loginToken struct {
Token string Token string
AccountID int AccountID int
Expiry time.Time Expiry time.Time
@ -21,7 +21,7 @@ type LoginToken struct {
// LoginTokenAuth implements passwordless login authentication flow using temporary in-memory stored tokens. // LoginTokenAuth implements passwordless login authentication flow using temporary in-memory stored tokens.
type LoginTokenAuth struct { type LoginTokenAuth struct {
token map[string]LoginToken token map[string]loginToken
mux sync.RWMutex mux sync.RWMutex
loginURL string loginURL string
loginTokenLength int loginTokenLength int
@ -31,7 +31,7 @@ type LoginTokenAuth struct {
// NewLoginTokenAuth configures and returns a LoginToken authentication instance. // NewLoginTokenAuth configures and returns a LoginToken authentication instance.
func NewLoginTokenAuth() (*LoginTokenAuth, error) { func NewLoginTokenAuth() (*LoginTokenAuth, error) {
a := &LoginTokenAuth{ a := &LoginTokenAuth{
token: make(map[string]LoginToken), token: make(map[string]loginToken),
loginURL: viper.GetString("auth_login_url"), loginURL: viper.GetString("auth_login_url"),
loginTokenLength: viper.GetInt("auth_login_token_length"), loginTokenLength: viper.GetInt("auth_login_token_length"),
loginTokenExpiry: viper.GetDuration("auth_login_token_expiry"), loginTokenExpiry: viper.GetDuration("auth_login_token_expiry"),
@ -40,8 +40,8 @@ func NewLoginTokenAuth() (*LoginTokenAuth, error) {
} }
// CreateToken creates an in-memory login token referencing account ID. It returns a token containing a random tokenstring and expiry date. // CreateToken creates an in-memory login token referencing account ID. It returns a token containing a random tokenstring and expiry date.
func (a *LoginTokenAuth) CreateToken(id int) LoginToken { func (a *LoginTokenAuth) CreateToken(id int) loginToken {
lt := LoginToken{ lt := loginToken{
Token: randStringBytes(a.loginTokenLength), Token: randStringBytes(a.loginTokenLength),
AccountID: id, AccountID: id,
Expiry: time.Now().Add(time.Minute * a.loginTokenExpiry), Expiry: time.Now().Add(time.Minute * a.loginTokenExpiry),
@ -61,14 +61,14 @@ func (a *LoginTokenAuth) GetAccountID(token string) (int, error) {
return lt.AccountID, nil return lt.AccountID, nil
} }
func (a *LoginTokenAuth) get(token string) (LoginToken, bool) { func (a *LoginTokenAuth) get(token string) (loginToken, bool) {
a.mux.RLock() a.mux.RLock()
lt, ok := a.token[token] lt, ok := a.token[token]
a.mux.RUnlock() a.mux.RUnlock()
return lt, ok return lt, ok
} }
func (a *LoginTokenAuth) add(lt LoginToken) { func (a *LoginTokenAuth) add(lt loginToken) {
a.mux.Lock() a.mux.Lock()
a.token[lt.Token] = lt a.token[lt.Token] = lt
a.mux.Unlock() a.mux.Unlock()

View file

@ -2,7 +2,8 @@ package email
import "time" import "time"
type LoginTokenContent struct { // ContentLoginToken defines content for login token email template
type ContentLoginToken struct {
Email string Email string
Name string Name string
URL string URL string
@ -10,15 +11,16 @@ type LoginTokenContent struct {
Expiry time.Time Expiry time.Time
} }
func (s *EmailService) LoginToken(name, address string, content LoginTokenContent) error { // LoginToken creates and sends a login token email with provided template content
msg := &Message{ func (m *Mailer) LoginToken(name, address string, content ContentLoginToken) error {
from: NewEmail(s.fromName, s.from), msg := &Mail{
from: NewEmail(m.fromName, m.from),
to: NewEmail(name, address), to: NewEmail(name, address),
subject: "Login Token", subject: "Login Token",
template: "loginToken", template: "loginToken",
content: content, content: content,
} }
err := s.send(msg) err := m.Send(msg)
return err return err
} }

View file

@ -21,13 +21,15 @@ var (
debug bool debug bool
) )
type EmailService struct { // Mailer is a SMTP mailer
type Mailer struct {
client *gomail.Dialer client *gomail.Dialer
templates *template.Template templates *template.Template
from, fromName string from, fromName string
} }
func NewEmailService() (*EmailService, error) { // NewMailer returns a configured SMTP Mailer
func NewMailer() (*Mailer, error) {
templates, err := parseTemplates() templates, err := parseTemplates()
if err != nil { if err != nil {
return nil, err return nil, err
@ -38,7 +40,7 @@ func NewEmailService() (*EmailService, error) {
smtpUser := viper.GetString("email_smtp_user") smtpUser := viper.GetString("email_smtp_user")
smtpPass := viper.GetString("email_smtp_password") smtpPass := viper.GetString("email_smtp_password")
s := &EmailService{ s := &Mailer{
client: gomail.NewPlainDialer(smtpHost, smtpPort, smtpUser, smtpPass), client: gomail.NewPlainDialer(smtpHost, smtpPort, smtpUser, smtpPass),
templates: templates, templates: templates,
from: viper.GetString("email_from_address"), from: viper.GetString("email_from_address"),
@ -56,9 +58,10 @@ func NewEmailService() (*EmailService, error) {
return s, nil return s, nil
} }
func (s *EmailService) send(msg *Message) error { // Send parses the corrsponding template and send the mail via smtp
func (m *Mailer) Send(mail *Mail) error {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
if err := s.templates.ExecuteTemplate(buf, msg.template, msg.content); err != nil { if err := m.templates.ExecuteTemplate(buf, mail.template, mail.content); err != nil {
return err return err
} }
prem := premailer.NewPremailerFromString(buf.String(), premailer.NewOptions()) prem := premailer.NewPremailerFromString(buf.String(), premailer.NewOptions())
@ -73,38 +76,27 @@ func (s *EmailService) send(msg *Message) error {
} }
if debug { if debug {
log.Println("To:", msg.to.Address) log.Println("To:", mail.to.Address)
log.Println("Subject:", msg.subject) log.Println("Subject:", mail.subject)
log.Println(text) log.Println(text)
return nil return nil
} }
m := gomail.NewMessage() msg := gomail.NewMessage()
m.SetAddressHeader("From", msg.from.Address, msg.from.Name) msg.SetAddressHeader("From", mail.from.Address, mail.from.Name)
m.SetAddressHeader("To", msg.to.Address, msg.to.Name) msg.SetAddressHeader("To", mail.to.Address, mail.to.Name)
m.SetHeader("Subject", msg.subject) msg.SetHeader("Subject", mail.subject)
m.SetBody("text/plain", text) msg.SetBody("text/plain", text)
m.AddAlternative("text/html", html) msg.AddAlternative("text/html", html)
if err := s.client.DialAndSend(m); err != nil { if err := m.client.DialAndSend(msg); err != nil {
return err return err
} }
return nil return nil
} }
type Email struct { // Mail struct holds all parts of a specific email
Name string type Mail struct {
Address string
}
func NewEmail(name string, address string) *Email {
return &Email{
Name: name,
Address: address,
}
}
type Message struct {
from *Email from *Email
to *Email to *Email
subject string subject string
@ -112,6 +104,20 @@ type Message struct {
content interface{} content interface{}
} }
// Email struct holds email address and recipient name
type Email struct {
Name string
Address string
}
// NewEmail returns an email address
func NewEmail(name string, address string) *Email {
return &Email{
Name: name,
Address: address,
}
}
func parseTemplates() (*template.Template, error) { func parseTemplates() (*template.Template, error) {
tmpl := template.New("").Funcs(fMap) tmpl := template.New("").Funcs(fMap)
err := filepath.Walk("./templates", func(path string, info os.FileInfo, err error) error { err := filepath.Walk("./templates", func(path string, info os.FileInfo, err error) error {

View file

@ -1,13 +0,0 @@
package mock
import "github.com/dhax/go-base/email"
type EmailService struct {
LoginTokenFn func(name, email string, c email.LoginTokenContent) error
LoginTokenInvoked bool
}
func (s *EmailService) LoginToken(n, e string, c email.LoginTokenContent) error {
s.LoginTokenInvoked = true
return s.LoginTokenFn(n, e, c)
}

13
testing/mock/mailer.go Normal file
View file

@ -0,0 +1,13 @@
package mock
import "github.com/dhax/go-base/email"
type Mailer struct {
LoginTokenFn func(name, email string, c email.ContentLoginToken) error
LoginTokenInvoked bool
}
func (s *Mailer) LoginToken(n, e string, c email.ContentLoginToken) error {
s.LoginTokenInvoked = true
return s.LoginTokenFn(n, e, c)
}