minor code style changes
This commit is contained in:
parent
4c4041a981
commit
232463e1db
11 changed files with 82 additions and 74 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
20
auth/api.go
20
auth/api.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
13
testing/mock/mailer.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue