go,tmpl: implement+activate validator
All checks were successful
continuous-integration/drone/push Build is passing

also ad initial password change:
* switch the password field type to `password`
* add a field for repeated password
This commit is contained in:
surtur 2023-09-08 22:56:17 +02:00
parent ff87c35dd1
commit 96c0b53493
Signed by: wanderer
SSH Key Fingerprint: SHA256:MdCZyJ2sHLltrLBp0xQO0O1qTW9BT/xl5nXkDvhlMCI
13 changed files with 173 additions and 19 deletions

View File

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

5
go.mod
View File

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

16
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
// Copyright 2023 wanderer <a_mirre at utb dot cz>
// 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
}

View File

@ -37,8 +37,18 @@
<span class="absolute" role="img" aria-label="password lock icon">
{{ template "svg-password.tmpl" }}
</span>
<input name="new-password" type="new-password"
placeholder="New password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{else}}{{end}}
<input name="new-password" type="password"
placeholder="New password" {{if and .Data.form .Data.form.NewPassword}}value="{{.Data.form.NewPassword}}"{{else}}{{end}}
minlength=20
required
class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
</div>
<div class="relative flex items-center">
<span class="absolute" role="img" aria-label="password lock icon">
{{ template "svg-password.tmpl" }}
</span>
<input name="repeat-new-password" type="password"
placeholder="New password repeated" {{if and .Data.form .Data.form.RepeatNewPassword}}value="{{.Data.form.RepeatNewPassword}}"{{else}}{{end}}
minlength=20
required
class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">