1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-15 00:15:15 +02:00

improved unique validation function to support multiple fields.

This commit is contained in:
Lee Brown
2019-08-07 21:17:57 -08:00
parent 83bcfce508
commit 7217deaf15
8 changed files with 43 additions and 96 deletions

View File

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by geeks-accelerator/swag at // This file was generated by geeks-accelerator/swag at
// 2019-08-07 19:21:01.42416 -0800 AKDT m=+430.010377170 // 2019-08-07 20:20:04.308889 -0800 AKDT m=+439.591671736
package docs package docs

View File

@ -114,7 +114,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
data["form"] = req data["form"] = req
if verr, ok := weberror.NewValidationError(ctx, signup.Validator().Struct(signup.SignupRequest{})); ok { if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(signup.SignupRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error) data["validationDefaults"] = verr.(*weberror.Error)
} }

4
go.mod
View File

@ -9,9 +9,9 @@ require (
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/fatih/camelcase v1.0.0 github.com/fatih/camelcase v1.0.0
github.com/fatih/structtag v1.0.0 github.com/fatih/structtag v1.0.0
github.com/geeks-accelerator/files v0.0.0-20190704085106-630677cd5c14 // indirect github.com/geeks-accelerator/files v0.0.0-20190704085106-630677cd5c14
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db
github.com/geeks-accelerator/swag v1.6.3 // indirect github.com/geeks-accelerator/swag v1.6.3
github.com/go-openapi/spec v0.19.2 // indirect github.com/go-openapi/spec v0.19.2 // indirect
github.com/go-openapi/swag v0.19.4 // indirect github.com/go-openapi/swag v0.19.4 // indirect
github.com/go-playground/locales v0.12.1 github.com/go-playground/locales v0.12.1

View File

@ -8,12 +8,12 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/pborman/uuid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"github.com/pkg/errors" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/geeks-accelerator/files" "github.com/geeks-accelerator/files"
"github.com/geeks-accelerator/swag" "github.com/geeks-accelerator/swag"
"github.com/pborman/uuid"
"github.com/pkg/errors"
) )
var ( var (

View File

@ -91,23 +91,6 @@ func init() {
zh_translations.RegisterDefaultTranslations(validate, transZh) zh_translations.RegisterDefaultTranslations(validate, transZh)
/* /*
validate.RegisterTranslation("required", transEn, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
validate.RegisterTranslation("required", transFr, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
validate.RegisterTranslation("unique", transEn, func(ut ut.Translator) error { validate.RegisterTranslation("unique", transEn, func(ut ut.Translator) error {
return ut.Add("unique", "{0} must be unique", true) // see universal-translator for details return ut.Add("unique", "{0} must be unique", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string { }, func(ut ut.Translator, fe validator.FieldError) string {
@ -122,14 +105,20 @@ func init() {
t, _ := ut.T("unique", fe.Field()) t, _ := ut.T("unique", fe.Field())
return t return t
})*/ })
*/
} }
// ctxKeyTagUnique represents the type of unique value for the context key used by the validation function.
type ctxKeyTagUnique int type ctxKeyTagUnique int
// KeyTagUnique defines the value used in the context key for storing the uniqueness of a field.
const KeyTagUnique ctxKeyTagUnique = 1 const KeyTagUnique ctxKeyTagUnique = 1
// KeyTagFieldValue defined the struct+field name used as the context key for storing whether the field is unique
// or not that is used by the custom validation function registered in newValidator.
type KeyTagFieldValue string
// newValidator inits a new validator with custom settings. // newValidator inits a new validator with custom settings.
func newValidator() *validator.Validate { func newValidator() *validator.Validate {
var v = validator.New() var v = validator.New()
@ -145,20 +134,24 @@ func newValidator() *validator.Validate {
return "{{" + name + "}}" return "{{" + name + "}}"
}) })
// Empty method that can be overwritten in business logic packages to prevent web.Decode from failing. // Custom Validation function for the unique tag that checks the context to determine if a field is
f := func(fl validator.FieldLevel) bool { // unique or not. First it will check a field specific context key and if that doesn't work, it will
return false // check a shared context key.
}
v.RegisterValidation("unique", f)
fctx := func(ctx context.Context, fl validator.FieldLevel) bool { fctx := func(ctx context.Context, fl validator.FieldLevel) bool {
if fl.Field().String() == "invalid" { if fl.Field().String() == "invalid" {
return false return false
} }
cv := ctx.Value(KeyTagUnique) // First check to see if a value is set for the specific field.
fk := KeyTagFieldValue(fl.Parent().Type().String() + "." + fl.StructFieldName())
cv := ctx.Value(fk)
// Second check if the default unique key is set in context.
if cv == nil { if cv == nil {
return false cv := ctx.Value(KeyTagUnique)
if cv == nil {
return false
}
} }
if v, ok := cv.(bool); ok { if v, ok := cv.(bool); ok {
@ -172,6 +165,14 @@ func newValidator() *validator.Validate {
return v return v
} }
// ContextAddUniqueValue allows multiple fields to be validated using the same unique validation function by added
// the unique value to the context using the struct name and field name.
func ContextAddUniqueValue(ctx context.Context, req interface{}, fieldName string, unique bool) context.Context {
fk := KeyTagFieldValue(reflect.TypeOf(req).String() + "." + fieldName)
return context.WithValue(ctx, fk, unique)
}
// Validator returns the current init validator. // Validator returns the current init validator.
func Validator() *validator.Validate { func Validator() *validator.Validate {
return validate return validate

View File

@ -14,7 +14,7 @@ type SignupRequest struct {
// SignupAccount defined the details needed for account. // SignupAccount defined the details needed for account.
type SignupAccount struct { type SignupAccount struct {
Name string `json:"name" validate:"required,unique-name" example:"Company {RANDOM_UUID}"` Name string `json:"name" validate:"required,unique" example:"Company {RANDOM_UUID}"`
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"` Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"` Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
City string `json:"city" validate:"required" example:"Valdez"` City string `json:"city" validate:"required" example:"Valdez"`
@ -28,7 +28,7 @@ type SignupAccount struct {
type SignupUser struct { type SignupUser struct {
FirstName string `json:"first_name" validate:"required" example:"Gabi"` FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"` LastName string `json:"last_name" validate:"required" example:"May"`
Email string `json:"email" validate:"required,email,unique-email" example:"{RANDOM_EMAIL}"` Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"`
Password string `json:"password" validate:"required" example:"SecretString"` Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"` PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
} }

View File

@ -11,62 +11,8 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/user_account" "geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/go-playground/validator.v9"
) )
type ctxKeyTagUniqueName int
const KeyTagUniqueName ctxKeyTagUniqueName = 1
type ctxKeyTagUniqueEmail int
const KeyTagUniqueEmail ctxKeyTagUniqueEmail = 1
// validate holds the settings and caches for validating request struct values.
var validate *validator.Validate
// Validator returns the current init validator.
func Validator() *validator.Validate {
if validate == nil {
validate = webcontext.Validator()
validate.RegisterValidationCtx("unique-name", func(ctx context.Context, fl validator.FieldLevel) bool {
if fl.Field().String() == "invalid" {
return false
}
cv := ctx.Value(KeyTagUniqueName)
if cv == nil {
return false
}
if v, ok := cv.(bool); ok {
return v
}
return false
})
validate.RegisterValidationCtx("unique-email", func(ctx context.Context, fl validator.FieldLevel) bool {
if fl.Field().String() == "invalid" {
return false
}
cv := ctx.Value(KeyTagUniqueEmail)
if cv == nil {
return false
}
if v, ok := cv.(bool); ok {
return v
}
return false
})
}
return validate
}
// Signup performs the steps needed to create a new account, new user and then associate // Signup performs the steps needed to create a new account, new user and then associate
// both records with a new user_account entry. // both records with a new user_account entry.
func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResult, error) { func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResult, error) {
@ -78,17 +24,17 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
if err != nil { if err != nil {
return nil, err return nil, err
} }
ctx = context.WithValue(ctx, KeyTagUniqueEmail, uniqEmail) ctx = webcontext.ContextAddUniqueValue(ctx, req.User, "Email", uniqEmail)
// Validate the account name is unique in the database. // Validate the account name is unique in the database.
uniqName, err := account.UniqueName(ctx, dbConn, req.Account.Name, "") uniqName, err := account.UniqueName(ctx, dbConn, req.Account.Name, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
ctx = context.WithValue(ctx, KeyTagUniqueName, uniqName) ctx = webcontext.ContextAddUniqueValue(ctx, req.Account, "Name", uniqName)
// Validate the request. // Validate the request.
err = Validator().StructCtx(ctx, req) err = webcontext.Validator().StructCtx(ctx, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -113,8 +113,8 @@ func TestSignupFull(t *testing.T) {
Zipcode: "99686", Zipcode: "99686",
}, },
User: SignupUser{ User: SignupUser{
FirstName: "Lee", FirstName: "Lee",
LastName: "Brown", LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -163,7 +163,7 @@ func TestSignupFull(t *testing.T) {
// Verify that the user can be authenticated with the updated password. // Verify that the user can be authenticated with the updated password.
_, err = user_auth.Authenticate(ctx, test.MasterDB, tknGen, user_auth.AuthenticateRequest{ _, err = user_auth.Authenticate(ctx, test.MasterDB, tknGen, user_auth.AuthenticateRequest{
Email:res.User.Email, Email: res.User.Email,
Password: req.User.Password, Password: req.User.Password,
}, time.Hour, now) }, time.Hour, now)
if err != nil { if err != nil {