diff --git a/app/server.go b/app/server.go index b8c158c..66d4ffb 100644 --- a/app/server.go +++ b/app/server.go @@ -8,6 +8,7 @@ import ( "net/http" "regexp" + "git.dotya.ml/mirre-mt/pcmt/modules/validation" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -73,6 +74,8 @@ func (a *App) SetServerSettings() { )) } + e.Validator = validation.New() + // TODO: add check for prometheus config setting. // if true { // // import "github.com/labstack/echo-contrib/prometheus" diff --git a/go.mod b/go.mod index e4c4f56..f9c42eb 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( entgo.io/ent v0.12.3 github.com/CAFxX/httpcompression v0.0.8 + github.com/go-playground/validator/v10 v10.15.3 github.com/google/uuid v1.3.0 github.com/gorilla/sessions v1.2.1 github.com/labstack/echo-contrib v0.15.0 @@ -29,7 +30,10 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/context v1.1.1 // indirect @@ -38,6 +42,7 @@ require ( github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.16.7 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect diff --git a/go.sum b/go.sum index abc1519..13fb70d 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,17 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fxamacker/cbor/v2 v2.2.1-0.20200511212021-28e39be4a84f/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= +github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= @@ -71,6 +80,8 @@ github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8 github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406 h1:+OUpk+IVvmKU0jivOVFGtOzA6U5AWFs8HE4DRzWLOUE= github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/matthewhartstonge/argon2 v0.3.3 h1:38/hupgfzqO2UGxqXqmSqErE8KJvQnIxWWg7IXUqWgQ= @@ -108,9 +119,14 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= diff --git a/handlers/error.go b/handlers/error.go index b7729b1..eebd00d 100644 --- a/handlers/error.go +++ b/handlers/error.go @@ -13,8 +13,9 @@ import ( ) var ( - ErrNoSession = errors.New("No session found, please log in") - ErrSessionExpired = errors.New("Session expired, log in again") + ErrNoSession = errors.New("No session found, please log in") + ErrSessionExpired = errors.New("Session expired, log in again") + ErrValidationFailed = errors.New("Check your input data") ) func renderErrorPage(c echo.Context, status int, statusText, error string) error { diff --git a/handlers/manage-user.go b/handlers/manage-user.go index bb40612..9772543 100644 --- a/handlers/manage-user.go +++ b/handlers/manage-user.go @@ -216,6 +216,7 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit data["flash"] = msg data["form"] = uc p.Data = data + p.User = u return c.Render( http.StatusBadRequest, @@ -224,6 +225,15 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit ) } + if err := c.Validate(uc); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + var msg string usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin) @@ -309,6 +319,15 @@ func ViewUser() echo.HandlerFunc { err := c.Bind(uid) if err == nil { //nolint:dupl + if err := c.Validate(uid); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - make sure to pass in a proper UUID", + err.Error(), + ) + } + usr, err := getUserByID(ctx, dbclient, uid.ID) if err != nil { if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic @@ -636,6 +655,15 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit } } + if err := c.Validate(uu); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + if usr.Username != uu.Username { exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username) if err != nil { diff --git a/handlers/search-hibp.go b/handlers/search-hibp.go index 454c7de..13d03a4 100644 --- a/handlers/search-hibp.go +++ b/handlers/search-hibp.go @@ -145,6 +145,15 @@ func SearchHIBP() echo.HandlerFunc { ) } + if err := c.Validate(a); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + breachNames, err := hibp.GetAllBreachesForAccount(a.Account) if err != nil { msg := "Error getting breaches for this account" diff --git a/handlers/signin.go b/handlers/signin.go index 7bafed2..f8b4696 100644 --- a/handlers/signin.go +++ b/handlers/signin.go @@ -123,6 +123,15 @@ func SigninPost(client *ent.Client) echo.HandlerFunc { ) } + if err := c.Validate(cu); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + loginFailed := "Login Failed!" ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger) diff --git a/handlers/signup.go b/handlers/signup.go index 6248168..b84c077 100644 --- a/handlers/signup.go +++ b/handlers/signup.go @@ -72,6 +72,15 @@ func SignupPost(client *ent.Client) echo.HandlerFunc { return c.Redirect(http.StatusFound, "/singup") } + if err := c.Validate(cu); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + username := cu.Username email := cu.Email passwd := cu.Password diff --git a/handlers/type.go b/handlers/type.go index 46508a5..bfad0a3 100644 --- a/handlers/type.go +++ b/handlers/type.go @@ -4,39 +4,40 @@ package handlers type userSignin struct { - Username string `form:"username" json:"username" validate:"required,username,gte=2"` - Password string `form:"password" json:"password" validate:"required,password,gte=12"` + Username string `form:"username" json:"username" validate:"required,gte=2"` + Password string `form:"password" json:"password" validate:"required,gte=20"` } type userSignup struct { - Username string `form:"username" json:"username" validate:"required,username,gte=2"` - Email string `form:"email" json:"email" validate:"required,email"` - Password string `form:"password" json:"password" validate:"required,password,gte=20"` + Username string `form:"username" json:"username" validate:"required,gte=2"` + Email string `form:"email" json:"email" validate:"required,email,gte=3"` + Password string `form:"password" json:"password" validate:"required,gte=20"` } // this struct is also used on update by admins, which is why the password fields are omitempty. // when users finish setting up, admins can no longer change their passwords. type userCreate struct { - Username string `form:"username" json:"username" validate:"required,username,gte=2"` - Email string `form:"email" json:"email" validate:"required,email"` - Password string `form:"password" json:"password" validate:"omitempty,password,gte=20"` - RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,repeatPassword,gte=20"` - IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"required,isAdmin"` - IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty,isActive"` + Username string `form:"username" json:"username" validate:"required,gte=2"` + Email string `form:"email" json:"email" validate:"required,email,gte=3"` + Password string `form:"password" json:"password" validate:"omitempty,gte=20,eqfield=RepeatPassword"` + RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,gte=20,eqfield=Password"` + IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"omitempty"` + IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty"` } type userID struct { - ID string `param:"id" validate:"required,id"` + ID string `param:"id" validate:"required,uuid"` } type initPasswordChange struct { - NewPassword string `form:"new-password" validate:"required,new-password,gte=20"` + NewPassword string `form:"new-password" validate:"required,gte=20,eqfield=RepeatNewPassword"` + RepeatNewPassword string `form:"repeat-new-password" validate:"required,gte=20,eqfield=NewPassword"` } type hibpSearch struct { - Account string `form:"search" validate:"required,search,gt=2"` + Account string `form:"search" validate:"required,gt=2"` } type hibpBreachDetail struct { - BreachName string `param:"name" validate:"required,name,gt=0"` + BreachName string `param:"name" validate:"required,gt=0"` } diff --git a/handlers/user-init-password-change.go b/handlers/user-init-password-change.go index 35167c3..6519d6c 100644 --- a/handlers/user-init-password-change.go +++ b/handlers/user-init-password-change.go @@ -65,6 +65,24 @@ func InitialPasswordChangePost() echo.HandlerFunc { ) } + if pw.NewPassword != pw.RepeatNewPassword { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - the passwords were not the same", + err.Error(), + ) + } + + if err := c.Validate(pw); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword) if err != nil { c.Logger().Errorf("error changing initial user password: %q", err) diff --git a/handlers/view-hibp.go b/handlers/view-hibp.go index 68601d7..5058f0d 100644 --- a/handlers/view-hibp.go +++ b/handlers/view-hibp.go @@ -38,6 +38,15 @@ func ViewHIBP() echo.HandlerFunc { ) } + if err := c.Validate(h); err != nil { + return renderErrorPage( + c, + http.StatusBadRequest, + http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), + err.Error(), + ) + } + ctx, ok := c.Get("sloggerCtx").(context.Context) if !ok { ctx = context.WithValue(context.Background(), hibp.CtxKey{}, slogger) diff --git a/modules/validation/validator.go b/modules/validation/validator.go new file mode 100644 index 0000000..7ebdc4f --- /dev/null +++ b/modules/validation/validator.go @@ -0,0 +1,36 @@ +// Copyright 2023 wanderer +// SPDX-License-Identifier: AGPL-3.0-only + +package validation + +import ( + "net/http" + + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" +) + +// Validator defines a validator that can be used with the echo framework. +type Validator struct { + validator *validator.Validate +} + +// New provides a new instance of type Validator, initialised with the default +// validator. +func New() *Validator { + return &Validator{ + validator: validator.New( + validator.WithRequiredStructEnabled(), + ), + } +} + +// Validate implements echo framework's Validator interface. +func (v *Validator) Validate(a any) error { + if err := v.validator.Struct(a); err != nil { + // Optionally, you could return the error to give each route more control over the status code + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return nil +} diff --git a/templates/user/init-password-change.tmpl b/templates/user/init-password-change.tmpl index 66fc5cf..c01031e 100644 --- a/templates/user/init-password-change.tmpl +++ b/templates/user/init-password-change.tmpl @@ -37,8 +37,18 @@ {{ template "svg-password.tmpl" }} - + +
+ + {{ template "svg-password.tmpl" }} + +