diff --git a/config.go b/config.go index 0488204..97e0ffd 100644 --- a/config.go +++ b/config.go @@ -28,9 +28,6 @@ type Config struct { } Modules struct { - // BCryptCost is the cost of the bcrypt password hashing function. - AuthBCryptCost int - // AuthLogoutMethod is the method the logout route should use (default should be DELETE) AuthLogoutMethod string @@ -45,8 +42,17 @@ type Config struct { // LockDuration is how long an account is locked for. LockDuration time.Duration + // RegBCryptCost is the cost of the bcrypt password hashing function. + RegisterBCryptCost int // RegisterPreserveFields are fields used with registration that are to be rendered when - // post fails. + // post fails in a normal way (for example validation errors), they will be passed + // back in the data of the response under the key DataPreserve which will be a map[string]string. + // + // All fields that are to be preserved must be able to be returned by the ArbitraryValuer.GetValues() + // + // This means in order to have a field named "address" you would need to have that returned by + // the ArbitraryValuer.GetValues() method and then it would be available to be whitelisted by this + // configuration variable. RegisterPreserveFields []string // RecoverTokenDuration controls how long a token sent via email for password @@ -126,11 +132,11 @@ func (c *Config) Defaults() { c.Paths.RecoverOK = "/" c.Paths.RegisterOK = "/" - c.Modules.AuthBCryptCost = bcrypt.DefaultCost c.Modules.AuthLogoutMethod = "DELETE" c.Modules.ExpireAfter = 60 * time.Minute c.Modules.LockAfter = 3 c.Modules.LockWindow = 5 * time.Minute c.Modules.LockDuration = 5 * time.Hour c.Modules.RecoverTokenDuration = time.Duration(24) * time.Hour + c.Modules.RegisterBCryptCost = bcrypt.DefaultCost } diff --git a/html_data.go b/html_data.go index 1fc3d96..9dc47c4 100644 --- a/html_data.go +++ b/html_data.go @@ -7,6 +7,8 @@ const ( DataErr = "error" // DataValidation is for validation errors DataValidation = "errors" + // DataPreserve preserves fields + DataPreserve = "preserve" ) // HTMLData is used to render templates with. @@ -16,7 +18,7 @@ type HTMLData map[string]interface{} // 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.") + panic("it should be a key value list of arguments.") } h := make(HTMLData) diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go index 3dc3ecf..6551be0 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/mocks.go @@ -28,6 +28,8 @@ type User struct { OAuthToken string OAuthRefresh string OAuthExpiry time.Time + + Arbitrary map[string]string } func (m User) GetPID() string { return m.Email } @@ -43,6 +45,7 @@ func (m User) GetAttemptTime() time.Time { return m.AttemptTime } func (m User) GetOAuthToken() string { return m.OAuthToken } func (m User) GetOAuthRefresh() string { return m.OAuthRefresh } func (m User) GetOAuthExpiry() time.Time { return m.OAuthExpiry } +func (m User) GetArbitrary() map[string]string { return m.Arbitrary } func (m *User) PutPID(email string) { m.Email = email } func (m *User) PutUsername(username string) { m.Username = username } @@ -60,6 +63,7 @@ func (m *User) PutAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTi func (m *User) PutOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken } func (m *User) PutOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh } func (m *User) PutOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry } +func (m *User) PutArbitrary(arb map[string]string) { m.Arbitrary = arb } // ServerStorer should be valid for any module storer defined in authboss. type ServerStorer struct { @@ -75,6 +79,21 @@ func NewServerStorer() *ServerStorer { } } +// New constructs a blank user to later be created +func (s *ServerStorer) New(context.Context) authboss.User { + return &User{} +} + +// Create a user +func (s *ServerStorer) Create(ctx context.Context, user authboss.User) error { + u := user.(*User) + if _, ok := s.Users[u.Email]; ok { + return authboss.ErrUserFound + } + s.Users[u.Email] = u + return nil +} + // Load a user func (s *ServerStorer) Load(ctx context.Context, key string) (authboss.User, error) { user, ok := s.Users[key] @@ -88,6 +107,9 @@ func (s *ServerStorer) Load(ctx context.Context, key string) (authboss.User, err // Save a user func (s *ServerStorer) Save(ctx context.Context, user authboss.User) error { u := user.(*User) + if _, ok := s.Users[u.Email]; !ok { + return authboss.ErrUserNotFound + } s.Users[u.Email] = u return nil } @@ -265,7 +287,7 @@ func (c *ClientStateRW) WriteState(w http.ResponseWriter, cstate authboss.Client return nil } -// Request returns a new request with optional key-value body (form-post) +// Request returns a new request with optional key-value body (form-post) func Request(method string, postKeyValues ...string) *http.Request { var body io.Reader location := "http://localhost" @@ -399,13 +421,16 @@ type Redirector struct { // Redirect a request func (r *Redirector) Redirect(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error { r.Options = ro + if len(ro.RedirectPath) == 0 { + panic("no redirect path on redirect call") + } http.Redirect(w, req, ro.RedirectPath, ro.Code) return nil } // BodyReader reads the body of a request and returns some values type BodyReader struct { - Return Values + Return authboss.Validator } // Read the return values @@ -417,6 +442,8 @@ func (b BodyReader) Read(page string, r *http.Request) (authboss.Validator, erro type Values struct { PID string Password string + + Errors []error } // GetPID from values @@ -431,7 +458,33 @@ func (v Values) GetPassword() string { // Validate the values func (v Values) Validate() []error { - return nil + return v.Errors +} + +// ArbValues is arbitrary value storage +type ArbValues struct { + Values map[string]string + Errors []error +} + +// GetPID gets the pid +func (a ArbValues) GetPID() string { + return a.Values["email"] +} + +// GetPassword gets the password +func (a ArbValues) GetPassword() string { + return a.Values["password"] +} + +// GetValues returns all values +func (a ArbValues) GetValues() map[string]string { + return a.Values +} + +// Validate nothing +func (a ArbValues) Validate() []error { + return a.Errors } // Logger logs to the void diff --git a/storage.go b/storage.go index bab2ed5..0190970 100644 --- a/storage.go +++ b/storage.go @@ -1,5 +1,12 @@ package authboss +// A concious decision was made to put all storer +// and user types into this file despite them truly +// belonging to outside modules. The reason for this +// is because documentation-wise, it was previously +// difficult to find what you had to implement or even +// what you could implement. + import ( "context" "time" @@ -39,10 +46,33 @@ type ServerStorer interface { // Load will look up the user based on the passed the PrimaryID Load(ctx context.Context, key string) (User, error) - // Save persists the user in the database + // Save persists the user in the database, this should never + // create a user and instead return ErrUserNotFound if the user + // does not exist. Save(ctx context.Context, user User) error } +// CreatingServerStorer is used for creating new users +// like when Registration is being done. +type CreatingServerStorer interface { + // New creates a blank user, it is not yet persisted in the database + // but is just for storing data + New(ctx context.Context) User + // Create the user in storage, it should not overwrite a user + // and should return ErrUserFound if it currently exists. + Create(ctx context.Context, user User) error +} + +// EnsureCanCreate makes sure the server storer supports create operations +func EnsureCanCreate(storer ServerStorer) CreatingServerStorer { + s, ok := storer.(CreatingServerStorer) + if !ok { + panic("could not upgrade serverstorer to creatingserverstorer, check your struct") + } + + return s +} + // User has functions for each piece of data it requires. // Data should not be persisted on each function call. // User has a PID (primary ID) that is used on the site as diff --git a/values.go b/values.go index 20b4b4d..23b9e79 100644 --- a/values.go +++ b/values.go @@ -40,3 +40,21 @@ func MustHaveUserValues(v Validator) UserValuer { panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to UserValuer: %T", v)) } + +// ArbitraryValuer provides the "rest" of the fields +// that aren't strictly needed for anything in particular, +// address, secondary e-mail, etc. +// +// There are two important notes about this interface: +// +// 1. That this is composed with Validator, as these fields +// should both be validated and culled of invalid pieces +// as they will be passed into ArbitraryUser.PutArbitrary() +// +// 2. These values will also be culled according to the RegisterPreserveFields +// whitelist and sent back in the data under the key DataPreserve. +type ArbitraryValuer interface { + Validator + + GetValues() map[string]string +}