refactor: completely restructure validation. split request and respond structs to their own package

This commit is contained in:
Derrick Hammer 2023-06-07 13:04:38 -04:00
parent bfbf13a57d
commit 2f7c31d53c
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
14 changed files with 232 additions and 117 deletions

View File

@ -1,15 +1,11 @@
package controller
import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/db"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/model"
"github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/kataras/iris/v12"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
@ -21,34 +17,6 @@ type AccountController struct {
Ctx iris.Context
}
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Pubkey string `json:"pubkey"`
}
func CheckPubkeyValidator(value interface{}) error {
p, _ := value.(string)
pubkeyBytes, err := hex.DecodeString(p)
if err != nil {
return err
}
if len(pubkeyBytes) != ed25519.PublicKeySize {
return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize))
}
return nil
}
func (r RegisterRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, validation.Required, is.EmailFormat),
validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(CheckPubkeyValidator))),
validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)),
)
}
func hashPassword(password string) (string, error) {
// Generate a new bcrypt hash from the provided password.
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
@ -62,12 +30,13 @@ func hashPassword(password string) (string, error) {
}
func (a *AccountController) PostRegister() {
var r RegisterRequest
if !tryParseRequest(r, a.Ctx) {
ri, success := tryParseRequest(request.RegisterRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.RegisterRequest)
// Check if an account with the same email address already exists.
existingAccount := model.Account{}
err := db.Get().Where("email = ?", r.Email).First(&existingAccount).Error

View File

@ -5,11 +5,11 @@ import (
"encoding/hex"
"errors"
"fmt"
"git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/controller/response"
"git.lumeweb.com/LumeWeb/portal/db"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/model"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/joomcode/errorx"
"github.com/kataras/iris/v12"
"github.com/kataras/jwt"
@ -31,54 +31,6 @@ type AuthController struct {
Ctx iris.Context
}
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (r LoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, is.EmailFormat, validation.Required),
validation.Field(&r.Password, validation.Required),
)
}
type LoginResponse struct {
Token string `json:"token"`
}
type LogoutRequest struct {
Token string `json:"token"`
}
type ChallengeRequest struct {
Pubkey string `json:"pubkey"`
}
func (r ChallengeRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(CheckPubkeyValidator)),
)
}
type ChallengeResponse struct {
Challenge string `json:"challenge"`
}
type PubkeyLoginRequest struct {
Pubkey string `json:"pubkey"`
Challenge string `json:"challenge"`
Signature string `json:"signature"`
}
func (r PubkeyLoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(CheckPubkeyValidator)),
validation.Field(&r.Challenge, validation.Required),
validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)),
)
}
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
func verifyPassword(hashedPassword, password string) error {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
@ -166,12 +118,13 @@ func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
func (a *AuthController) PostLogin() {
var r LoginRequest
if !tryParseRequest(r, a.Ctx) {
ri, success := tryParseRequest(request.LoginRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.LoginRequest)
// Retrieve the account for the given email.
account := model.Account{}
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
@ -205,7 +158,7 @@ func (a *AuthController) PostLogin() {
}
// Return the JWT token to the client.
err = a.Ctx.JSON(&LoginResponse{Token: token})
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
if err != nil {
logger.Get().Error("failed to generate response", zap.Error(err))
}
@ -213,12 +166,13 @@ func (a *AuthController) PostLogin() {
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
func (a *AuthController) PostPubkeyChallenge() {
var r ChallengeRequest
if !tryParseRequest(r, a.Ctx) {
ri, success := tryParseRequest(request.PubkeyChallengeRequest{}, a.Ctx)
if !success {
return
}
r, _ := (ri).(*request.PubkeyChallengeRequest)
r.Pubkey = strings.ToLower(r.Pubkey)
// Retrieve the account for the given email.
@ -236,20 +190,21 @@ func (a *AuthController) PostPubkeyChallenge() {
}
// Return the challenge to the client.
err = a.Ctx.JSON(&ChallengeResponse{Challenge: challenge})
err = a.Ctx.JSON(&response.ChallengeResponse{Challenge: challenge})
if err != nil {
panic(fmt.Errorf("Error with challenge request: %s \n", err))
logger.Get().Error("failed to create response", zap.Error(err))
}
}
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
func (a *AuthController) PostPubkeyLogin() {
var r PubkeyLoginRequest
if !tryParseRequest(r, a.Ctx) {
ri, success := tryParseRequest(request.PubkeyLoginRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.PubkeyLoginRequest)
r.Pubkey = strings.ToLower(r.Pubkey)
r.Signature = strings.ToLower(r.Signature)
@ -318,7 +273,7 @@ func (a *AuthController) PostPubkeyLogin() {
}
// Return the JWT token to the client.
err = a.Ctx.JSON(&LoginResponse{Token: token})
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
if err != nil {
logger.Get().Error("failed to create response", zap.Error(err))
}
@ -327,12 +282,13 @@ func (a *AuthController) PostPubkeyLogin() {
// PostLogout handles the POST /api/auth/logout request to invalidate a JWT token.
func (a *AuthController) PostLogout() {
var r LogoutRequest
if !tryParseRequest(r, a.Ctx) {
ri, success := tryParseRequest(request.LogoutRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.LogoutRequest)
// Verify the provided token.
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
if err != nil {

40
controller/controller.go Normal file
View File

@ -0,0 +1,40 @@
package controller
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
"git.lumeweb.com/LumeWeb/portal/logger"
"github.com/kataras/iris/v12"
"go.uber.org/zap"
)
func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) {
v, ok := r.(validators.Validatable)
if !ok {
return r, true
}
var d map[string]interface{}
// Read the logout request from the client.
if err := ctx.ReadJSON(&d); err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
data, err := v.Import(d)
if err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
if err := data.Validate(); err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
return data, true
}

View File

@ -0,0 +1,23 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
type LoginRequest struct {
validatable validators.ValidatableImpl
Email string `json:"email"`
Password string `json:"password"`
}
func (r LoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, is.EmailFormat, validation.Required),
validation.Field(&r.Password, validation.Required),
)
}
func (r LoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -0,0 +1,19 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type LogoutRequest struct {
validatable validators.ValidatableImpl
Token string `json:"token"`
}
func (r LogoutRequest) Validate() error {
return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required))
}
func (r LogoutRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -0,0 +1,21 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type PubkeyChallengeRequest struct {
validatable validators.ValidatableImpl
Pubkey string `json:"pubkey"`
}
func (r PubkeyChallengeRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
)
}
func (r PubkeyChallengeRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -0,0 +1,25 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type PubkeyLoginRequest struct {
validatable validators.ValidatableImpl
Pubkey string `json:"pubkey"`
Challenge string `json:"challenge"`
Signature string `json:"signature"`
}
func (r PubkeyLoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
validation.Field(&r.Challenge, validation.Required),
validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)),
)
}
func (r PubkeyLoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -0,0 +1,21 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Pubkey string `json:"pubkey"`
}
func (r RegisterRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, validation.Required, is.EmailFormat),
validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(validators.CheckPubkeyValidator))),
validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)),
)
}

View File

@ -0,0 +1,5 @@
package response
type ChallengeResponse struct {
Challenge string `json:"challenge"`
}

View File

@ -0,0 +1,5 @@
package response
type LoginResponse struct {
Token string `json:"token"`
}

View File

@ -0,0 +1,43 @@
package validators
import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/imdario/mergo"
"reflect"
)
func CheckPubkeyValidator(value interface{}) error {
p, _ := value.(string)
pubkeyBytes, err := hex.DecodeString(p)
if err != nil {
return err
}
if len(pubkeyBytes) != ed25519.PublicKeySize {
return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize))
}
return nil
}
type Validatable interface {
validation.Validatable
Import(d map[string]interface{}) (Validatable, error)
}
type ValidatableImpl struct {
}
func (v ValidatableImpl) Import(d map[string]interface{}, destType Validatable) (Validatable, error) {
instance := reflect.New(reflect.TypeOf(destType)).Interface().(Validatable)
// Perform the import logic
if err := mergo.Map(instance, d, mergo.WithOverride); err != nil {
return nil, err
}
return instance, nil
}

1
go.mod
View File

@ -55,6 +55,7 @@ require (
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect

2
go.sum
View File

@ -810,6 +810,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=

15
main.go
View File

@ -9,7 +9,6 @@ import (
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/service/files"
"git.lumeweb.com/LumeWeb/portal/tus"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/iris-contrib/swagger"
"github.com/iris-contrib/swagger/swaggerFiles"
"github.com/kataras/iris/v12"
@ -49,8 +48,6 @@ func main() {
// Create a new Iris app instance
app := iris.New()
app.Validator = ozzValidator{}
// Enable Gzip compression for responses
app.Use(iris.Compression)
@ -105,15 +102,3 @@ func main() {
logger.Get().Error("Failed starting webserver proof", zap.Error(err))
}
}
type ozzValidator struct{}
func (o ozzValidator) Struct(d interface{}) error {
v, ok := d.(validation.Validatable)
if !ok {
return nil
}
return v.Validate()
}