refactor: completely restructure validation. split request and respond structs to their own package
This commit is contained in:
parent
bfbf13a57d
commit
2f7c31d53c
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package response
|
||||
|
||||
type ChallengeResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package response
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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
15
main.go
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue