Skip to content

Commit

Permalink
Merge pull request #3 from wawandco/task-moving-to-options
Browse files Browse the repository at this point in the history
task: moving to the options pattern to configure the handler
  • Loading branch information
paganotoni committed Mar 19, 2022
2 parents db37cc2 + a413f7e commit 4bc030f
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 261 deletions.
48 changes: 22 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,27 @@ And then using it accordingly in your app. See the Usage section for detailed in
Maildoor instances satisfy the http.Handler interface and can be mounted into Mupliplexers. To initialize a Maildoor instance, use the New function:

```go
auth, err := maildoor.New(maildoor.Options{
BaseURL: "http://localhost:8080",
Prefix: "/auth",

FinderFn: finder,
AfterLoginFn: afterLogin,
LogoutFn: logout,

// Here we're using the SMTP sender but you can use your own.
SenderFn: maildoor.NewSMTPSender(maildoor.SMTPOptions{
From: os.Getenv("SMTP_FROM_EMAIL"),
Host: os.Getenv("SMTP_HOST"), // p.e. "smtp.gmail.com",
Port: os.Getenv("SMTP_PORT"), //"587",
Password: os.Getenv("SMTP_PASSWORD"),
}),

CSRFTokenSecret: os.Getenv("SECRET_VALUE"),
TokenManager: maildoor.DefaultTokenManager(os.Getenv("SECRET_VALUE")),
})

if err != nil {
return nil, fmt.Errorf("error initializing maildoor: %w", err)
}
// Initialize the maildoor handler to take care of the web requests.
auth, err := maildoor.NewWithOptions(
os.Getenv("SECRET_KEY"),

maildoor.UseFinder(finder),
maildoor.UseAfterLogin(afterLogin),
maildoor.UseLogout(logout),
maildoor.UseTokenManager(maildoor.DefaultTokenManager(os.Getenv("SECRET_KEY"))),
maildoor.UseSender(
maildoor.NewSMTPSender(maildoor.SMTPOptions{
From: os.Getenv("SMTP_FROM_EMAIL"),
Host: os.Getenv("SMTP_HOST"), // p.e. "smtp.gmail.com",
Port: os.Getenv("SMTP_PORT"), //"587",
Password: os.Getenv("SMTP_PASSWORD"),
}),
),
)

if err != nil {
return nil, fmt.Errorf("error initializing maildoor: %w", err)
}
```

After initializing the Maildoor instance, you can mount it into a multiplexer:
Expand All @@ -72,9 +70,7 @@ After seeing how to initialize the Maildoor Instance, lets dig a deeper into wha

#### FinderFn

The finder function is used to find a user by email address. The logic for looking up users is up to the application developer, but it should return an `Emailable` instance to be used on the signin flow.

The signature of the finder function is:
The finder function is used to find a user by email address. The logic for looking up users is up to the application developer, but it should return an `Emailable` instance to be used on the signin flow. The signature of the finder function is:

```go
func(string) (Emailable, error)
Expand Down
4 changes: 3 additions & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ type handler struct {
prefix string
baseURL string
csrfTokenSecret string
product Product

// Product settings
product productConfig

finderFn func(token string) (Emailable, error)
senderFn func(message *Message) error
Expand Down
5 changes: 1 addition & 4 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ func (sl *stringLogger) Errorf(format string, args ...interface{}) {

func TestCustomLogger(t *testing.T) {
lg := &stringLogger{}
h, err := maildoor.New(maildoor.Options{
CSRFTokenSecret: "secret",
Logger: lg,
})
h, err := maildoor.NewWithOptions("secret", maildoor.UseLogger(lg))

testhelpers.NoError(t, err)
w := httptest.NewRecorder()
Expand Down
4 changes: 1 addition & 3 deletions login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import (
)

func TestLogin(t *testing.T) {
h, err := maildoor.New(maildoor.Options{
CSRFTokenSecret: "secret",
})
h, err := maildoor.NewWithOptions("secret")

testhelpers.NoError(t, err)

Expand Down
12 changes: 5 additions & 7 deletions logout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import (
)

func TestLogout(t *testing.T) {
h, err := maildoor.New(maildoor.Options{
CSRFTokenSecret: "secret",
LogoutFn: func(w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
},
})
logout := func(w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}

h, err := maildoor.NewWithOptions("secret", maildoor.UseLogout(logout))
testhelpers.NoError(t, err)

w := httptest.NewRecorder()
Expand Down
62 changes: 8 additions & 54 deletions maildoor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
defaultPrefix = "/auth"
defaultBaseURL = "http://127.0.0.1:8080"
defaultTokenManager = DefaultTokenManager("not-so-secret-key")
defaultProduct = Product{
defaultProduct = productConfig{
Name: "maildoor",
LogoURL: "https://github.com/wawandco/maildoor/raw/main/images/maildoor_logo.png",
FaviconURL: "https://github.com/wawandco/maildoor/raw/main/images/favicon.png",
Expand All @@ -37,9 +37,7 @@ var (
assets embed.FS
)

// New maildoor handler with the given options, all of the options have defaults,
// if not specified this method pulls the default value for them.
func New(o Options) (*handler, error) {
func NewWithOptions(csrfToken string, options ...Option) (*handler, error) {
h := &handler{
product: defaultProduct,
prefix: defaultPrefix,
Expand All @@ -50,60 +48,16 @@ func New(o Options) (*handler, error) {
tokenManager: defaultTokenManager,
logger: defaultLogger,

assetsServer: http.FileServer(http.FS(assets)),
assetsServer: http.FileServer(http.FS(assets)),
csrfTokenSecret: csrfToken,
}

h.product.LogoURL = h.logoPath()
h.product.FaviconURL = h.faviconPath()

if o.CSRFTokenSecret == "" {
return nil, errors.New("CSRF Token secret is required")
}

h.csrfTokenSecret = o.CSRFTokenSecret

if o.Product != (Product{}) {
if o.Product.LogoURL == "" {
o.Product.LogoURL = h.logoPath()
}

if o.Product.FaviconURL == "" {
o.Product.FaviconURL = h.faviconPath()
}

h.product = o.Product
}

if o.Prefix != "" {
h.prefix = o.Prefix
}

if o.BaseURL != "" {
h.baseURL = o.BaseURL
}

if o.SenderFn != nil {
h.senderFn = o.SenderFn
}

if o.FinderFn != nil {
h.finderFn = o.FinderFn
}

if o.AfterLoginFn != nil {
h.afterLoginFn = o.AfterLoginFn
}

if o.LogoutFn != nil {
h.logoutFn = o.LogoutFn
}

if o.TokenManager != nil {
h.tokenManager = o.TokenManager
if csrfToken == "" {
return nil, errors.New("CSRF token is empty")
}

if o.Logger != nil {
h.logger = o.Logger
for _, option := range options {
option(h)
}

return h, nil
Expand Down
35 changes: 13 additions & 22 deletions maildoor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,12 @@ import (
func TestNew(t *testing.T) {
tcases := []struct {
name string
opts Options
opts []Option
verify func(*testing.T, *handler, error)
}{
{
name: "totally empty it errors",
opts: Options{},
verify: func(t *testing.T, h *handler, err error) {
testhelpers.Error(t, err)
},
},
{
name: "empty defaults",
opts: Options{
CSRFTokenSecret: "secret",
},
opts: []Option{},
verify: func(t *testing.T, h *handler, err error) {
testhelpers.NoError(t, err)
testhelpers.NotEquals(t, "", h.product.Name)
Expand All @@ -39,13 +30,10 @@ func TestNew(t *testing.T) {

{
name: "some empty defaults",
opts: Options{
CSRFTokenSecret: "secret",
Product: Product{
Name: "MyProduct",
LogoURL: "logoURL",
FaviconURL: "faviconURL",
},
opts: []Option{
UseProductName("MyProduct"),
UseLogo("logoURL"),
UseFavicon("faviconURL"),
},
verify: func(t *testing.T, h *handler, err error) {
testhelpers.NoError(t, err)
Expand All @@ -71,9 +59,7 @@ func TestNew(t *testing.T) {
},
{
name: "product images",
opts: Options{
CSRFTokenSecret: "secret",
},
opts: []Option{},
verify: func(t *testing.T, h *handler, err error) {
testhelpers.NoError(t, err)
testhelpers.NotEquals(t, "", h.product.Name)
Expand All @@ -90,9 +76,14 @@ func TestNew(t *testing.T) {

for _, v := range tcases {
t.Run(v.name, func(tt *testing.T) {
h, err := New(v.opts)
h, err := NewWithOptions("secret", v.opts...)
v.verify(tt, h, err)
})
}

}

func TestEmptyToken(t *testing.T) {
_, err := NewWithOptions("")
testhelpers.Error(t, err)
}
112 changes: 80 additions & 32 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,84 @@ package maildoor

import "net/http"

// Options for the handler, these define the behavior of the
// handler while running, cannot be changed after initialized.
type Options struct {
Prefix string
BaseURL string
Product Product

FinderFn func(token string) (Emailable, error)
SenderFn func(message *Message) error
AfterLoginFn func(w http.ResponseWriter, r *http.Request, user Emailable) error
LogoutFn func(w http.ResponseWriter, r *http.Request) error

TokenManager TokenManager

// CSRFTokenSecret is used to generate signed CSRF tokens for the forms
// to be secure, it MUST be specified and is recommended to make it an
// environment variable or secret in application infrastructure,
// NOT in application code.
CSRFTokenSecret string

// Logger that the app uses, this by default is the default logger
// which prints using the std `log` package.
Logger Logger
}

// Product options allow to customize the product name and logo
// as well as the favicon. These are used in the email that gets
// sent to the user and the login form.
type Product struct {
Name string
LogoURL string
FaviconURL string
// Option is a function that can be passed to NewWithOptions to customize the
// behavior of the handler.
type Option func(*handler)

// UseProductName to be used in emails and login form.
func UseProductName(name string) Option {
return func(h *handler) {
h.product.Name = name
}
}

// UseLogo for the login form and email.
func UseLogo(logoURL string) Option {
return func(h *handler) {
h.product.LogoURL = logoURL
}
}

// UseFavicon for the login form and email.
func UseFavicon(faviconURL string) Option {
return func(h *handler) {
h.product.FaviconURL = faviconURL
}
}

// UseLogger across the lifecycle of the handler.
func UseLogger(logger Logger) Option {
return func(h *handler) {
h.logger = logger
}
}

// UsePrefix sets the prefix for the handler, this is
// useful for links and mounting the handler.
func UsePrefix(prefix string) Option {
return func(h *handler) {
h.prefix = prefix
}
}

// UseBaseURL for links
func UseBaseURL(baseURL string) Option {
return func(h *handler) {
h.baseURL = baseURL
}
}

// UseSender Specify the sender to be used by the handler.
func UseSender(fn func(message *Message) error) Option {
return func(h *handler) {
h.senderFn = fn
}
}

// UseFinderFn sets the finder to be used.
func UseFinder(fn func(token string) (Emailable, error)) Option {
return func(h *handler) {
h.finderFn = fn
}
}

// UseAfterLogin sets the function to be called after a successful login.
func UseAfterLogin(fn func(w http.ResponseWriter, r *http.Request, user Emailable) error) Option {
return func(h *handler) {
h.afterLoginFn = fn
}
}

// UseLogout sets the function to be called after a successful logout.
func UseLogout(fn func(w http.ResponseWriter, r *http.Request) error) Option {
return func(h *handler) {
h.logoutFn = fn
}
}

// UseTokenManager sets the token manager to be used.
func UseTokenManager(tokenManager TokenManager) Option {
return func(h *handler) {
h.tokenManager = tokenManager
}
}
Loading

0 comments on commit 4bc030f

Please sign in to comment.