1
0
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:
Aaron 2015-02-19 14:34:29 -08:00
parent 679798564f
commit 6a4feaa2ea
8 changed files with 251 additions and 54 deletions

View File

@ -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
}
}
}

View File

@ -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
View 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)
}

View File

@ -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) {

View File

@ -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++

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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
}