mirror of
https://github.com/volatiletech/authboss.git
synced 2025-02-01 13:17:43 +02:00
Add authboss error types.
- Add error handling at the routing level so that all errors can bubble up and be handled and logged there in one place. - Add Err variants for ClientStorer and Attributes to facilitate generating errors for missing type-failing arguments. - Add better control flow and error handling for callbacks.
This commit is contained in:
parent
679798564f
commit
6a4feaa2ea
21
callbacks.go
21
callbacks.go
@ -15,10 +15,10 @@ const (
|
|||||||
|
|
||||||
// Before callbacks can interrupt the flow by returning an error. This is used to stop
|
// Before callbacks can interrupt the flow by returning an error. This is used to stop
|
||||||
// the callback chain and the original handler from executing.
|
// the callback chain and the original handler from executing.
|
||||||
type Before func(*Context) error
|
type Before func(*Context) (bool, error)
|
||||||
|
|
||||||
// After is a request callback that happens after the event.
|
// After is a request callback that happens after the event.
|
||||||
type After func(*Context)
|
type After func(*Context) error
|
||||||
|
|
||||||
// Callbacks is a collection of callbacks that fire before and after certain
|
// Callbacks is a collection of callbacks that fire before and after certain
|
||||||
// methods.
|
// methods.
|
||||||
@ -49,22 +49,27 @@ func (c *Callbacks) After(e Event, f After) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FireBefore event to all the callbacks with a context.
|
// FireBefore event to all the callbacks with a context.
|
||||||
func (c *Callbacks) FireBefore(e Event, ctx *Context) error {
|
func (c *Callbacks) FireBefore(e Event, ctx *Context) (interrupted bool, err error) {
|
||||||
callbacks := c.before[e]
|
callbacks := c.before[e]
|
||||||
for _, fn := range callbacks {
|
for _, fn := range callbacks {
|
||||||
err := fn(ctx)
|
interrupted, err = fn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
|
}
|
||||||
|
if interrupted {
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FireAfter event to all the callbacks with a context.
|
// FireAfter event to all the callbacks with a context.
|
||||||
func (c *Callbacks) FireAfter(e Event, ctx *Context) {
|
func (c *Callbacks) FireAfter(e Event, ctx *Context) (err error) {
|
||||||
callbacks := c.after[e]
|
callbacks := c.after[e]
|
||||||
for _, fn := range callbacks {
|
for _, fn := range callbacks {
|
||||||
fn(ctx)
|
if err = fn(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
context.go
39
context.go
@ -19,13 +19,10 @@ type Context struct {
|
|||||||
|
|
||||||
postFormValues map[string][]string
|
postFormValues map[string][]string
|
||||||
formValues map[string][]string
|
formValues map[string][]string
|
||||||
keyValues map[string]interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContext() *Context {
|
func NewContext() *Context {
|
||||||
return &Context{
|
return &Context{}
|
||||||
keyValues: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextFromRequest creates a context from an http request.
|
// ContextFromRequest creates a context from an http request.
|
||||||
@ -40,17 +37,6 @@ func ContextFromRequest(r *http.Request) (*Context, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put an arbitrary key-value into the context.
|
|
||||||
func (c *Context) Put(key string, thing interface{}) {
|
|
||||||
c.keyValues[key] = thing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an arbitrary key-value from the context.
|
|
||||||
func (c *Context) Get(key string) (thing interface{}, ok bool) {
|
|
||||||
thing, ok = c.keyValues[key]
|
|
||||||
return thing, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormValue gets a form value from a context created with a request.
|
// FormValue gets a form value from a context created with a request.
|
||||||
func (c *Context) FormValue(key string) ([]string, bool) {
|
func (c *Context) FormValue(key string) ([]string, bool) {
|
||||||
val, ok := c.formValues[key]
|
val, ok := c.formValues[key]
|
||||||
@ -85,6 +71,29 @@ func (c *Context) FirstPostFormValue(key string) (string, bool) {
|
|||||||
return val[0], ok
|
return val[0], ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FirstFormValueErrr gets the first form value from a context created with a request
|
||||||
|
// and additionally returns an error not a bool if it's not found.
|
||||||
|
func (c *Context) FirstFormValueErr(key string) (string, error) {
|
||||||
|
val, ok := c.formValues[key]
|
||||||
|
|
||||||
|
if !ok || len(val) == 0 || len(val[0]) == 0 {
|
||||||
|
return "", ClientDataErr{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstPostFormValue gets the first form value from a context created with a request.
|
||||||
|
func (c *Context) FirstPostFormValueErr(key string) (string, error) {
|
||||||
|
val, ok := c.postFormValues[key]
|
||||||
|
|
||||||
|
if !ok || len(val) == 0 || len(val[0]) == 0 {
|
||||||
|
return "", ClientDataErr{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadUser loads the user Attributes if they haven't already been loaded.
|
// LoadUser loads the user Attributes if they haven't already been loaded.
|
||||||
func (c *Context) LoadUser(key string) error {
|
func (c *Context) LoadUser(key string) error {
|
||||||
if c.User != nil {
|
if c.User != nil {
|
||||||
|
62
errors.go
Normal file
62
errors.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package authboss
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// AttributeErr represents a failure to retrieve a critical
|
||||||
|
// piece of data from the storer.
|
||||||
|
type AttributeErr struct {
|
||||||
|
Name string
|
||||||
|
WantKind DataType
|
||||||
|
GotKind string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeAttributeErr(name, kind DataType, val interface{}) AttributeErr {
|
||||||
|
return AttributeErr{
|
||||||
|
Name: name,
|
||||||
|
WantKind: kind,
|
||||||
|
GotKind: fmt.Sprintf("%T", val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AttributeErr) Error() string {
|
||||||
|
if len(a.KindIssue) == 0 {
|
||||||
|
return fmt.Sprintf("Failed to retrieve database attribute: %s", a.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Failed to retrieve database attribute, type was wrong: %s (want: %v, got: %s)", a.Name, a.WantKind, a.GotKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientDataErr represents a failure to retrieve a critical
|
||||||
|
// piece of client information such as a cookie or session value.
|
||||||
|
type ClientDataErr struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ClientDataErr) Error() string {
|
||||||
|
return fmt.Sprintf("Failed to retrieve client attribute: %s (%v)", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAndRedirect represents a general error whose response should
|
||||||
|
// be to redirect.
|
||||||
|
type ErrAndRedirect struct {
|
||||||
|
Err error
|
||||||
|
Endpoint string
|
||||||
|
FlashSuccess string
|
||||||
|
FlashError string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrAndRedirect) Error() string {
|
||||||
|
return fmt.Sprintf("Error: %v, Redirecting to: %s", e.Error, e.Endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderErr represents an error that occured during rendering
|
||||||
|
// of a template.
|
||||||
|
type RenderErr struct {
|
||||||
|
TemplateName string
|
||||||
|
Data interface{}
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RenderErr) Error() string {
|
||||||
|
return fmt.Sprintf("Error rendering template %q (%#v): %v", r.TemplateName, r.Data, r.Err)
|
||||||
|
}
|
@ -9,9 +9,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/authboss.v0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -22,21 +26,48 @@ var (
|
|||||||
// Templates is a map depicting the forms a template needs wrapped within the specified layout
|
// Templates is a map depicting the forms a template needs wrapped within the specified layout
|
||||||
type Templates map[string]*template.Template
|
type Templates map[string]*template.Template
|
||||||
|
|
||||||
// ExecuteTemplate is a convenience wrapper for executing a template from the layout. Returns
|
// Render renders a view with xsrf and flash attributes.
|
||||||
// ErrTemplateNotFound when the template is missing, othwerise error.
|
func (t Templates) Render(ctx *authboss.Context, w http.ResponseWriter, r *http.Request, name string, data authboss.HTMLData) error {
|
||||||
func (t Templates) ExecuteTemplate(name string, data interface{}) (buffer *bytes.Buffer, err error) {
|
|
||||||
tpl, ok := t[name]
|
tpl, ok := t[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrTemplateNotFound
|
return authboss.RenderErr{tpl.Name(), data, ErrTemplateNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Merge("xsrfName", authboss.Cfg.XSRFName, "xsrfToken", authboss.Cfg.XSRFMaker(w, r))
|
||||||
|
|
||||||
|
if flash, ok := ctx.CookieStorer.Get(authboss.FlashSuccessKey); ok {
|
||||||
|
ctx.CookieStorer.Del(authboss.FlashSuccessKey)
|
||||||
|
data.Merge(authboss.FlashSuccessKey, flash)
|
||||||
|
}
|
||||||
|
if flash, ok := ctx.CookieStorer.Get(authboss.FlashErrorKey); ok {
|
||||||
|
ctx.CookieStorer.Del(authboss.FlashErrorKey)
|
||||||
|
data.Merge(authboss.FlashErrorKey, flash)
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer = &bytes.Buffer{}
|
buffer = &bytes.Buffer{}
|
||||||
err = tpl.ExecuteTemplate(buffer, tpl.Name(), data)
|
err = tpl.ExecuteTemplate(buffer, tpl.Name(), data)
|
||||||
|
if err != nil {
|
||||||
return buffer, err
|
return authboss.RenderErr{tpl.Name(), data, err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get parses all speicified files located in path. Each template is wrapped
|
err = io.Copy(w, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return authboss.RenderErr{tpl.Name(), data, err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashRedirect sets any flash messages given and redirects the user.
|
||||||
|
func FlashRedirect(ctx *authboss.Context, w http.ResponseWriter, r *http.Request, path, flashSuccess, flashError string) {
|
||||||
|
if len(flashSuccess) > 0 {
|
||||||
|
ctx.CookieStorer.Put(authboss.FlashSuccessKey, flashSuccess)
|
||||||
|
}
|
||||||
|
if len(flashError) > 0 {
|
||||||
|
ctx.CookieStorer.Put(authboss.FlashErrorKey, flashError)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, path, http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parses all specified files located in path. Each template is wrapped
|
||||||
// in a unique clone of layout. All templates are expecting {{authboss}} handlebars
|
// in a unique clone of layout. All templates are expecting {{authboss}} handlebars
|
||||||
// for parsing.
|
// for parsing.
|
||||||
func Get(layout *template.Template, path string, files ...string) (Templates, error) {
|
func Get(layout *template.Template, path string, files ...string) (Templates, error) {
|
||||||
|
@ -94,18 +94,14 @@ func (l *Lock) AfterAuthFail(ctx *authboss.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastAttempt := time.Now().UTC()
|
lastAttempt := time.Now().UTC()
|
||||||
if attemptTimeIntf, ok := ctx.User[StoreAttemptTime]; ok {
|
if attemptTime, ok := ctx.User.DateTime(StoreAttemptTime); ok {
|
||||||
if attemptTime, ok := attemptTimeIntf.(time.Time); ok {
|
|
||||||
lastAttempt = attemptTime
|
lastAttempt = attemptTime
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
nAttempts := 0
|
nAttempts := 0
|
||||||
if attemptsIntf, ok := ctx.User[StoreAttemptNumber]; ok {
|
if attempts, ok := ctx.User.Int(StoreAttemptNumber); ok {
|
||||||
if attempts, ok := attemptsIntf.(int); ok {
|
|
||||||
nAttempts = attempts
|
nAttempts = attempts
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
nAttempts++
|
nAttempts++
|
||||||
|
|
||||||
|
25
router.go
25
router.go
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Handler augments http.HandlerFunc with a context.
|
// Handler augments http.HandlerFunc with a context.
|
||||||
type HandlerFunc func(*Context, http.ResponseWriter, *http.Request)
|
type HandlerFunc func(*Context, http.ResponseWriter, *http.Request) error
|
||||||
|
|
||||||
// RouteTable is a routing table from a path to a handlerfunc.
|
// RouteTable is a routing table from a path to a handlerfunc.
|
||||||
type RouteTable map[string]HandlerFunc
|
type RouteTable map[string]HandlerFunc
|
||||||
@ -40,5 +40,26 @@ func (c contextRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx.CookieStorer = Cfg.CookieStoreMaker(w, r)
|
ctx.CookieStorer = Cfg.CookieStoreMaker(w, r)
|
||||||
ctx.SessionStorer = Cfg.SessionStoreMaker(w, r)
|
ctx.SessionStorer = Cfg.SessionStoreMaker(w, r)
|
||||||
|
|
||||||
c.fn(ctx, w, r)
|
err = c.fn(ctx, w, r)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(Cfg.LogWriter, "Error Occurred at %s: %v", r.URL.Path, err)
|
||||||
|
switch e := err.(type) {
|
||||||
|
case AttributeErr:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
case ClientDataErr:
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
case RedirectErr:
|
||||||
|
if len(e.FlashSuccess) > 0 {
|
||||||
|
ctx.CookieStorer.Put(FlashSuccessKey, e.FlashSuccess)
|
||||||
|
}
|
||||||
|
if len(e.FlashError) > 0 {
|
||||||
|
ctx.CookieStorer.Put(FlashErrorKey, e.FlashError)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, e.Endpoint, http.StatusTemporaryRedirect)
|
||||||
|
case RenderErr:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
52
storer.go
52
storer.go
@ -177,6 +177,58 @@ func (a Attributes) DateTime(key string) (time.Time, bool) {
|
|||||||
return val, ok
|
return val, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringErr returns a single value as a string
|
||||||
|
func (a Attributes) StringErr(key string) (val string, err error) {
|
||||||
|
inter, ok := a[key]
|
||||||
|
if !ok {
|
||||||
|
return "", AttributeErr{Name: key}
|
||||||
|
}
|
||||||
|
val, ok = inter.(string)
|
||||||
|
if !ok {
|
||||||
|
return val, MakeAttributeErr(key, String, inter)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntErr returns a single value as a int
|
||||||
|
func (a Attributes) IntErr(key string) (val int, err error) {
|
||||||
|
inter, ok := a[key]
|
||||||
|
if !ok {
|
||||||
|
return "", AttributeErr{Name: key}
|
||||||
|
}
|
||||||
|
val, ok = inter.(int)
|
||||||
|
if !ok {
|
||||||
|
return val, MakeAttributeErr(key, Integer, inter)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolErr returns a single value as a bool.
|
||||||
|
func (a Attributes) BoolErr(key string) (val bool, err error) {
|
||||||
|
inter, ok := a[key]
|
||||||
|
if !ok {
|
||||||
|
return "", AttributeErr{Name: key}
|
||||||
|
}
|
||||||
|
val, ok = inter.(int)
|
||||||
|
if !ok {
|
||||||
|
return val, MakeAttributeErr(key, Integer, inter)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DateTimeErr returns a single value as a time.Time
|
||||||
|
func (a Attributes) DateTimeErr(key string) (val time.Time, err error) {
|
||||||
|
inter, ok := a[key]
|
||||||
|
if !ok {
|
||||||
|
return val, AttributeErr{Name: key}
|
||||||
|
}
|
||||||
|
val, ok = inter.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return val, MakeAttributeErr(key, DateTime, inter)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Bind the data in the attributes to the given struct. This means the
|
// Bind the data in the attributes to the given struct. This means the
|
||||||
// struct creator must have read the documentation and decided what fields
|
// struct creator must have read the documentation and decided what fields
|
||||||
// will be needed ahead of time.
|
// will be needed ahead of time.
|
||||||
|
55
views.go
55
views.go
@ -1,21 +1,42 @@
|
|||||||
package authboss
|
package authboss
|
||||||
|
|
||||||
import "net/http"
|
// HTMLData is used to render templates with.
|
||||||
|
type HTMLData map[string]interface{}
|
||||||
|
|
||||||
// ViewDataMaker is set in the config and is called before
|
// NewHTMLData creates HTMLData from key-value pairs. The input is a key-value
|
||||||
// template rendering to help correctly render the layout page.
|
// slice, where odd elements are keys, and the following even element is their value.
|
||||||
type ViewDataMaker func(r *http.Request) ViewHelper
|
func NewHTMLData(data ...interface{}) HTMLData {
|
||||||
|
if len(data)%2 != 0 {
|
||||||
// ViewData is the data authboss uses for rendering a page.
|
panic("It should be a key value list of arguments.")
|
||||||
// Typically this goes on your layout page's data struct.
|
}
|
||||||
type ViewData interface{}
|
|
||||||
|
h := make(HTMLData)
|
||||||
// ViewHelper is a type that implements a Put() and Get() method for
|
|
||||||
// authboss's view data. Before a template is rendered
|
for i := 0; i < len(data)-1; i += 2 {
|
||||||
// by the authboss http handlers, it will call the config's ViewDataMaker
|
k, ok := data[i].(string)
|
||||||
// callback to get a ViewHelper containing data that will be useful for the
|
if !ok {
|
||||||
// layout page, then use Put to set it, and inside the template use Get to get it.
|
panic("Keys must be strings.")
|
||||||
type ViewHelper interface {
|
}
|
||||||
Put(ViewData)
|
|
||||||
Get() ViewData
|
h[k] = data[i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge adds extra key-values to the HTMLData. The input is a key-value
|
||||||
|
// slice, where odd elements are keys, and the following even element is their value.
|
||||||
|
func (h HTMLData) Merge(data ...interface{}) HTMLData {
|
||||||
|
if len(data)%2 != 0 {
|
||||||
|
panic("It should be a key value list of arguments.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(data)-1; i += 2 {
|
||||||
|
k, ok := data[i].(string)
|
||||||
|
if !ok {
|
||||||
|
panic("Keys must be strings.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user