mirror of
https://github.com/volatiletech/authboss.git
synced 2025-01-20 04:59:29 +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
|
||||
// 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.
|
||||
type After func(*Context)
|
||||
type After func(*Context) error
|
||||
|
||||
// Callbacks is a collection of callbacks that fire before and after certain
|
||||
// methods.
|
||||
@ -49,22 +49,27 @@ func (c *Callbacks) After(e Event, f After) {
|
||||
}
|
||||
|
||||
// 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]
|
||||
for _, fn := range callbacks {
|
||||
err := fn(ctx)
|
||||
interrupted, err = fn(ctx)
|
||||
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.
|
||||
func (c *Callbacks) FireAfter(e Event, ctx *Context) {
|
||||
func (c *Callbacks) FireAfter(e Event, ctx *Context) (err error) {
|
||||
callbacks := c.after[e]
|
||||
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
|
||||
formValues map[string][]string
|
||||
keyValues map[string]interface{}
|
||||
}
|
||||
|
||||
func NewContext() *Context {
|
||||
return &Context{
|
||||
keyValues: make(map[string]interface{}),
|
||||
}
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
// ContextFromRequest creates a context from an http request.
|
||||
@ -40,17 +37,6 @@ func ContextFromRequest(r *http.Request) (*Context, error) {
|
||||
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.
|
||||
func (c *Context) FormValue(key string) ([]string, bool) {
|
||||
val, ok := c.formValues[key]
|
||||
@ -85,6 +71,29 @@ func (c *Context) FirstPostFormValue(key string) (string, bool) {
|
||||
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.
|
||||
func (c *Context) LoadUser(key string) error {
|
||||
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"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/authboss.v0"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -22,21 +26,48 @@ var (
|
||||
// Templates is a map depicting the forms a template needs wrapped within the specified layout
|
||||
type Templates map[string]*template.Template
|
||||
|
||||
// ExecuteTemplate is a convenience wrapper for executing a template from the layout. Returns
|
||||
// ErrTemplateNotFound when the template is missing, othwerise error.
|
||||
func (t Templates) ExecuteTemplate(name string, data interface{}) (buffer *bytes.Buffer, err error) {
|
||||
// Render renders a view with xsrf and flash attributes.
|
||||
func (t Templates) Render(ctx *authboss.Context, w http.ResponseWriter, r *http.Request, name string, data authboss.HTMLData) error {
|
||||
tpl, ok := t[name]
|
||||
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{}
|
||||
err = tpl.ExecuteTemplate(buffer, tpl.Name(), data)
|
||||
if err != nil {
|
||||
return authboss.RenderErr{tpl.Name(), data, err}
|
||||
}
|
||||
|
||||
return buffer, err
|
||||
err = io.Copy(w, buffer)
|
||||
if err != nil {
|
||||
return authboss.RenderErr{tpl.Name(), data, err}
|
||||
}
|
||||
}
|
||||
|
||||
// Get parses all speicified files located in path. Each template is wrapped
|
||||
// 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
|
||||
// for parsing.
|
||||
func Get(layout *template.Template, path string, files ...string) (Templates, error) {
|
||||
|
12
lock/lock.go
12
lock/lock.go
@ -94,17 +94,13 @@ func (l *Lock) AfterAuthFail(ctx *authboss.Context) {
|
||||
}
|
||||
|
||||
lastAttempt := time.Now().UTC()
|
||||
if attemptTimeIntf, ok := ctx.User[StoreAttemptTime]; ok {
|
||||
if attemptTime, ok := attemptTimeIntf.(time.Time); ok {
|
||||
lastAttempt = attemptTime
|
||||
}
|
||||
if attemptTime, ok := ctx.User.DateTime(StoreAttemptTime); ok {
|
||||
lastAttempt = attemptTime
|
||||
}
|
||||
|
||||
nAttempts := 0
|
||||
if attemptsIntf, ok := ctx.User[StoreAttemptNumber]; ok {
|
||||
if attempts, ok := attemptsIntf.(int); ok {
|
||||
nAttempts = attempts
|
||||
}
|
||||
if attempts, ok := ctx.User.Int(StoreAttemptNumber); ok {
|
||||
nAttempts = attempts
|
||||
}
|
||||
|
||||
nAttempts++
|
||||
|
25
router.go
25
router.go
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// 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.
|
||||
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.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
|
||||
}
|
||||
|
||||
// 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
|
||||
// struct creator must have read the documentation and decided what fields
|
||||
// will be needed ahead of time.
|
||||
|
51
views.go
51
views.go
@ -1,21 +1,42 @@
|
||||
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
|
||||
// template rendering to help correctly render the layout page.
|
||||
type ViewDataMaker func(r *http.Request) ViewHelper
|
||||
// NewHTMLData creates HTMLData from key-value pairs. The input is a key-value
|
||||
// slice, where odd elements are keys, and the following even element is their value.
|
||||
func NewHTMLData(data ...interface{}) HTMLData {
|
||||
if len(data)%2 != 0 {
|
||||
panic("It should be a key value list of arguments.")
|
||||
}
|
||||
|
||||
// ViewData is the data authboss uses for rendering a page.
|
||||
// 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
|
||||
// by the authboss http handlers, it will call the config's ViewDataMaker
|
||||
// callback to get a ViewHelper containing data that will be useful for the
|
||||
// layout page, then use Put to set it, and inside the template use Get to get it.
|
||||
type ViewHelper interface {
|
||||
Put(ViewData)
|
||||
Get() ViewData
|
||||
for i := 0; i < len(data)-1; i += 2 {
|
||||
k, ok := data[i].(string)
|
||||
if !ok {
|
||||
panic("Keys must be strings.")
|
||||
}
|
||||
|
||||
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