mirror of
https://github.com/volatiletech/authboss.git
synced 2025-02-09 13:47:09 +02:00
Add a way to read values and validate
In order to support multiple different types of requests, there needed to be an interface to be able to read values from a request, and subsequently validate them to return any errors. So we've adjusted the Validator interface to no longer validate a request but instead validate the object it lives on. And we've created a new BodyReader interface.
This commit is contained in:
parent
7119b3a90e
commit
386133a84b
@ -15,6 +15,7 @@ var blankRegex = regexp.MustCompile(`^\s*$`)
|
||||
type Rules struct {
|
||||
// FieldName is the name of the field this is intended to validate.
|
||||
FieldName string
|
||||
|
||||
// MatchError describes the MustMatch regexp to a user.
|
||||
Required bool
|
||||
MatchError string
|
||||
@ -27,11 +28,6 @@ type Rules struct {
|
||||
AllowWhitespace bool
|
||||
}
|
||||
|
||||
// Field names the field this ruleset applies to.
|
||||
func (r Rules) Field() string {
|
||||
return r.FieldName
|
||||
}
|
||||
|
||||
// Errors returns an array of errors for each validation error that
|
||||
// is present in the given string. Returns nil if there are no errors.
|
||||
func (r Rules) Errors(toValidate string) authboss.ErrorList {
|
||||
|
@ -2,37 +2,42 @@ package defaults
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
)
|
||||
|
||||
// HTTPFormValidator validates HTTP post type inputs
|
||||
type HTTPFormValidator struct {
|
||||
Ruleset []authboss.FieldValidator
|
||||
Values map[string]string
|
||||
|
||||
Ruleset []Rules
|
||||
ConfirmFields []string
|
||||
}
|
||||
|
||||
// Validate validates a request using the given ruleset.
|
||||
func (h HTTPFormValidator) Validate(r *http.Request) authboss.ErrorList {
|
||||
func (h HTTPFormValidator) Validate() []error {
|
||||
var errList authboss.ErrorList
|
||||
|
||||
for _, fieldValidator := range h.Ruleset {
|
||||
field := fieldValidator.Field()
|
||||
for _, rule := range h.Ruleset {
|
||||
field := rule.FieldName
|
||||
|
||||
val := r.FormValue(field)
|
||||
if errs := fieldValidator.Errors(val); len(errs) > 0 {
|
||||
val := h.Values[field]
|
||||
if errs := rule.Errors(val); len(errs) > 0 {
|
||||
errList = append(errList, errs...)
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(h.ConfirmFields); l != 0 && l%2 != 0 {
|
||||
panic("HTTPFormValidator given an odd number of confirm fields")
|
||||
}
|
||||
|
||||
for i := 0; i < len(h.ConfirmFields)-1; i += 2 {
|
||||
main := r.FormValue(h.ConfirmFields[i])
|
||||
main := h.Values[h.ConfirmFields[i]]
|
||||
if len(main) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
confirm := r.FormValue(h.ConfirmFields[i+1])
|
||||
confirm := h.Values[h.ConfirmFields[i+1]]
|
||||
if len(confirm) == 0 || main != confirm {
|
||||
errList = append(errList, FieldError{h.ConfirmFields[i+1], fmt.Errorf("Does not match %s", h.ConfirmFields[i])})
|
||||
}
|
||||
|
@ -3,39 +3,36 @@ package defaults
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/volatiletech/authboss"
|
||||
"github.com/volatiletech/authboss/internal/mocks"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := mocks.Request("POST", "username", "john", "email", "john@john.com")
|
||||
|
||||
validator := HTTPFormValidator{
|
||||
Ruleset: []authboss.FieldValidator{
|
||||
mocks.FieldValidator{
|
||||
Values: map[string]string{
|
||||
"username": "john",
|
||||
"email": "john@john.com",
|
||||
},
|
||||
Ruleset: []Rules{
|
||||
Rules{
|
||||
FieldName: "username",
|
||||
Errs: authboss.ErrorList{FieldError{"username", errors.New("must be longer than 4")}},
|
||||
MinLength: 5,
|
||||
},
|
||||
mocks.FieldValidator{
|
||||
Rules{
|
||||
FieldName: "missing_field",
|
||||
Errs: authboss.ErrorList{FieldError{"missing_field", errors.New("Expected field to exist")}},
|
||||
},
|
||||
mocks.FieldValidator{
|
||||
FieldName: "email", Errs: nil,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errList := validator.Validate(req)
|
||||
errList := authboss.ErrorList(validator.Validate())
|
||||
|
||||
errs := errList.Map()
|
||||
if errs["username"][0] != "must be longer than 4" {
|
||||
if errs["username"][0] != "Must be at least 5 characters" {
|
||||
t.Error("Expected a different error for username:", errs["username"][0])
|
||||
}
|
||||
if errs["missing_field"][0] != "Expected field to exist" {
|
||||
if errs["missing_field"][0] != "Cannot be blank" {
|
||||
t.Error("Expected a different error for missing_field:", errs["missing_field"][0])
|
||||
}
|
||||
if _, ok := errs["email"]; ok {
|
||||
@ -47,17 +44,23 @@ func TestValidate_Confirm(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := HTTPFormValidator{
|
||||
ConfirmFields: []string{"username", "confirmUsername"},
|
||||
Values: map[string]string{
|
||||
"username": "john",
|
||||
"confirm_username": "johnny",
|
||||
},
|
||||
ConfirmFields: []string{"username", "confirm_username"},
|
||||
}
|
||||
|
||||
req := mocks.Request("POST", "username", "john", "confirmUsername", "johnny")
|
||||
errs := validator.Validate(req).Map()
|
||||
if errs["confirmUsername"][0] != "Does not match username" {
|
||||
t.Error("Expected a different error for confirmUsername:", errs["confirmUsername"][0])
|
||||
mapped := authboss.ErrorList(validator.Validate()).Map()
|
||||
if mapped["confirm_username"][0] != "Does not match username" {
|
||||
t.Error("Expected a different error for confirmUsername:", mapped["confirmUsername"][0])
|
||||
}
|
||||
|
||||
req = mocks.Request("POST", "username", "john", "confirmUsername", "john")
|
||||
errs = validator.Validate(req).Map()
|
||||
validator.Values = map[string]string{
|
||||
"username": "john",
|
||||
"confirm_username": "john",
|
||||
}
|
||||
errs := authboss.ErrorList(validator.Validate())
|
||||
if len(errs) != 0 {
|
||||
t.Error("Expected no errors:", errs)
|
||||
}
|
||||
@ -66,9 +69,19 @@ func TestValidate_Confirm(t *testing.T) {
|
||||
ConfirmFields: []string{"username"},
|
||||
}
|
||||
|
||||
req = mocks.Request("POST", "username", "john", "confirmUsername", "john")
|
||||
errs = validator.Validate(req).Map()
|
||||
paniced := false
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
paniced = true
|
||||
}
|
||||
}()
|
||||
|
||||
errs = authboss.ErrorList(validator.Validate())
|
||||
if len(errs) != 0 {
|
||||
t.Error("Expected no errors:", errs)
|
||||
}
|
||||
|
||||
if !paniced {
|
||||
t.Error("Want a panic due to bad confirm fields slice")
|
||||
}
|
||||
}
|
||||
|
129
defaults/values.go
Normal file
129
defaults/values.go
Normal file
@ -0,0 +1,129 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/volatiletech/authboss"
|
||||
)
|
||||
|
||||
// FormValue types
|
||||
const (
|
||||
FormValueEmail = "email"
|
||||
FormValuePassword = "password"
|
||||
FormValueUsername = "username"
|
||||
)
|
||||
|
||||
// UserValues from the login form
|
||||
type UserValues struct {
|
||||
HTTPFormValidator
|
||||
|
||||
PID string
|
||||
Password string
|
||||
}
|
||||
|
||||
// GetPID from the values
|
||||
func (u UserValues) GetPID() string {
|
||||
return u.PID
|
||||
}
|
||||
|
||||
// GetPassword from the values
|
||||
func (u UserValues) GetPassword() string {
|
||||
return u.Password
|
||||
}
|
||||
|
||||
// HTTPFormReader reads forms from various pages and decodes
|
||||
// them.
|
||||
type HTTPFormReader struct {
|
||||
UseUsername bool
|
||||
Rulesets map[string][]Rules
|
||||
}
|
||||
|
||||
// NewHTTPFormReader creates a form reader with default validation rules
|
||||
// for each page.
|
||||
func NewHTTPFormReader(useUsernameNotEmail bool) *HTTPFormReader {
|
||||
var pid string
|
||||
var pidRules Rules
|
||||
|
||||
if useUsernameNotEmail {
|
||||
pid = "username"
|
||||
pidRules = Rules{
|
||||
FieldName: pid, Required: true,
|
||||
MatchError: "Usernames must only start with letters, and contain letters and numbers",
|
||||
MustMatch: regexp.MustCompile(`(?i)[a-z][a-z0-9]?`),
|
||||
}
|
||||
} else {
|
||||
pid = "email"
|
||||
pidRules = Rules{
|
||||
FieldName: pid, Required: true,
|
||||
MatchError: "Must be a valid e-mail address",
|
||||
MustMatch: regexp.MustCompile(`.*@.*\.[a-z]{1,}`),
|
||||
}
|
||||
}
|
||||
|
||||
passwordRule := Rules{
|
||||
FieldName: "password",
|
||||
MinLength: 8,
|
||||
MinNumeric: 1,
|
||||
MinSymbols: 1,
|
||||
MinUpper: 1,
|
||||
MinLower: 1,
|
||||
}
|
||||
|
||||
return &HTTPFormReader{
|
||||
UseUsername: useUsernameNotEmail,
|
||||
Rulesets: map[string][]Rules{
|
||||
"login": []Rules{pidRules, passwordRule},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read the form pages
|
||||
func (h HTTPFormReader) Read(page string, r *http.Request) (authboss.Validator, error) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse form on page: %s", page)
|
||||
}
|
||||
|
||||
rules := h.Rulesets[page]
|
||||
values := URLValuesToMap(r.Form)
|
||||
|
||||
switch page {
|
||||
case "login":
|
||||
var pid string
|
||||
if h.UseUsername {
|
||||
pid = values[FormValueUsername]
|
||||
} else {
|
||||
pid = values[FormValueEmail]
|
||||
}
|
||||
|
||||
validator := HTTPFormValidator{
|
||||
Values: values,
|
||||
Ruleset: rules,
|
||||
ConfirmFields: []string{FormValuePassword, "confirm_" + FormValuePassword},
|
||||
}
|
||||
password := values[FormValuePassword]
|
||||
|
||||
return UserValues{
|
||||
HTTPFormValidator: validator,
|
||||
PID: pid,
|
||||
Password: password,
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("failed to parse unknown page's form: %s", page)
|
||||
}
|
||||
}
|
||||
|
||||
// URLValuesToMap helps create a map from url.Values
|
||||
func URLValuesToMap(form url.Values) map[string]string {
|
||||
values := make(map[string]string)
|
||||
|
||||
for k, v := range form {
|
||||
if len(v) != 0 {
|
||||
values[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
28
defaults/values_test.go
Normal file
28
defaults/values_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
"github.com/volatiletech/authboss/internal/mocks"
|
||||
)
|
||||
|
||||
func TestHTTPFormReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := NewHTTPFormReader(false)
|
||||
r := mocks.Request("POST", "email", "john@john.john", "password", "flowers")
|
||||
|
||||
validator, err := h.Read("login", r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
uv := validator.(authboss.UserValuer)
|
||||
if "john@john.john" != uv.GetPID() {
|
||||
t.Error("wrong e-mail:", uv.GetPID())
|
||||
}
|
||||
if "flowers" != uv.GetPassword() {
|
||||
t.Error("wrong password:", uv.GetPassword())
|
||||
}
|
||||
}
|
@ -349,25 +349,3 @@ func NewAfterCallback() *AfterCallback {
|
||||
|
||||
return &m
|
||||
}
|
||||
|
||||
// FieldValidator mock
|
||||
type FieldValidator struct {
|
||||
FieldName string
|
||||
Errs []error
|
||||
Ruleset []string
|
||||
}
|
||||
|
||||
// Field being validated
|
||||
func (f FieldValidator) Field() string {
|
||||
return f.FieldName
|
||||
}
|
||||
|
||||
// Errors list
|
||||
func (f FieldValidator) Errors(in string) []error {
|
||||
return f.Errs
|
||||
}
|
||||
|
||||
// Rules present
|
||||
func (f FieldValidator) Rules() []string {
|
||||
return f.Ruleset
|
||||
}
|
||||
|
@ -12,20 +12,14 @@ const (
|
||||
// Validator takes a form name and a set of inputs and returns any validation errors
|
||||
// for the inputs.
|
||||
type Validator interface {
|
||||
// Validate inputs from the named form
|
||||
Validate(name string, fieldValues map[string]string) []error
|
||||
}
|
||||
|
||||
// FieldValidator is anything that can validate a string and provide a list of errors
|
||||
// and describe its set of rules.
|
||||
type FieldValidator interface {
|
||||
Field() string
|
||||
Errors(in string) []error
|
||||
Rules() []string
|
||||
// Validate makes the type validate itself and return
|
||||
// a list of validation errors.
|
||||
Validate() []error
|
||||
}
|
||||
|
||||
// FieldError describes an error on a field
|
||||
type FieldError interface {
|
||||
error
|
||||
Name() string
|
||||
Err() error
|
||||
}
|
||||
|
29
values.go
Normal file
29
values.go
Normal file
@ -0,0 +1,29 @@
|
||||
package authboss
|
||||
|
||||
import "net/http"
|
||||
|
||||
// BodyReader reads data from the request
|
||||
// and returns it in an abstract form.
|
||||
// Typically used to decode JSON responses
|
||||
// or Url Encoded request bodies.
|
||||
//
|
||||
// The first parameter is the page that this request
|
||||
// was made on so we can tell what kind of JSON object
|
||||
// or form was present as well as create the proper
|
||||
// validation mechanisms.
|
||||
//
|
||||
// A typical example of this is taking the request
|
||||
// and turning it into a JSON struct that knows how
|
||||
// to validate itself and return certain fields.
|
||||
type BodyReader interface {
|
||||
Read(page string, r *http.Request) (Validator, error)
|
||||
}
|
||||
|
||||
// UserValuer gets a string from a map-like data structure
|
||||
// Typically a decoded JSON or form auth request
|
||||
type UserValuer interface {
|
||||
Validator
|
||||
|
||||
GetPID() string
|
||||
GetPassword() string
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user