D
D
dexdev2021-05-22 21:46:43
go
dexdev, 2021-05-22 21:46:43

How to parse multipart/form-data form in Go with echo framework?

Good day! I'm learning the golang language, I chose the echo framework. In principle, everything is clear except for some fundamental things and syntax. I decided to study on a real project, because I think that this is the most effective way. I took on a project, a regular site + admin panel, everything was fine until I got to HTML, namely forms, you need to authorize the user and give him a token, everything works fine with JSON, but in order to have SEO, you have to render HTML, so I decided not to use JS Front end frameworks like VUE, and do everything in HTML5, I got to the forms and I can’t figure out how to parse the data from the form and shove it into MONGODB.

Created a model

package model

import (
  mongopagination "github.com/gobeam/mongo-go-pagination"
  "go.mongodb.org/mongo-driver/bson/primitive"
)

type User struct {
  *UserInput `bson:",inline"`
  ID         primitive.ObjectID `json:"id" xml:"id" form:"id" bson:"_id,omitempty"`
}

type UserInput struct {
  FirstName string `json:"firstName" xml:"firstName" form:"firstName" bson:"firstName" validate:"required"`
  LastName  string `json:"lastName" xml:"lastName" form:"lastName" bson:"lastName" validate:"required"`
  Email     string `json:"email" xml:"email" form:"email" bson:"email" validate:"required,email"`
  Password  string `json:"password,omitempty" form:"password,omitempty" xml:"password,omitempty" bson:"password" validate:"required"`
}

type LoginInput struct {
  Email    string `json:"email" xml:"email" bson:"email" validate:"required,email"`
  Password string `json:"password" xml:"password" bson:"password" validate:"required"`
}

type PagedUser struct {
  Data     []User                         `json:"data" xml:"data"`
  PageInfo mongopagination.PaginationData `json:"pageInfo" xml:"pageInfo"`
}


From the form public/signup.html

{{define "signup"}}
  {{template "head"}}
  {{template "header"}}
    <div class="signup">
      <div class="container">
        <div class="row">
          <div class="col-md-6">
            <form method="POST" action="/api/v1/signup">     
              <label>Email</label>
              <input email="email" type="email"/>

              <label>FirstName</label>
              <input firstName="firstName" type="text"/>

              <label>LastName</label>
              <input lastName="lastName" type="text"/>  

              <label>Password</label>
              <input password="password" type="password"/>

              <input type="submit" value="submit" />
          </form>
          </div>
          <div class="col-md-6">dd</div>
        </div>
      </div>
    </div>
  {{template "footer"}}
{{end}}


Submit form to route

package routes

import (
  "Avangard/controller"

  "github.com/labstack/echo/v4"
)

func GetUserApiRoutes(e *echo.Echo, userController *controller.UserController) {
  v1 := e.Group("/api/v1")
  {
    v1.POST("/login", userController.AuthenticateUser)
    v1.GET("/users", userController.GetAllUser)
    v1.POST("/signup", userController.SaveUser)
    v1.GET("/users/:id", userController.GetUser)
    v1.PUT("/users/:id", userController.UpdateUser)
    v1.DELETE("/users/:id", userController.DeleteUser)

  }
}


Then I process in the controller

package controller

import (
  "Avangard/exception"
  model "Avangard/models"
  "Avangard/repository"
  "Avangard/security"
  "Avangard/util"
  "log"
  "net/http"
  "strconv"

  "github.com/labstack/echo/v4"
)

type UserController struct {
  userRepository repository.UserRepository
  authValidator  *security.AuthValidator
}

func NewUserController(userRepository repository.UserRepository, authValidator *security.AuthValidator) *UserController {
  return &UserController{userRepository: userRepository, authValidator: authValidator}
}

func (userController *UserController) SaveUser(c echo.Context) error {

  payload := new(model.UserInput)
  //email := c.FormValue("email")
  log.Println(c, payload)
  if err := util.BindAndValidate(c, payload); err != nil {
    return err
  }

  _, err := userController.userRepository.FindByEmail(payload.Email)
  if err == nil {
    return exception.ConflictException("User", "email", payload.Email)
  }

  user := &model.User{UserInput: payload}

  //encrypt password
  err = beforeSave(user)
  if err != nil {
    return err
  }

  createdUser, err := userController.userRepository.SaveUser(user)
  if err != nil {
    return err
  }

  return util.Negotiate(c, http.StatusCreated, createdUser)
}

func beforeSave(user *model.User) (err error) {
  hashedPassword, err := util.EncryptPassword(user.Password)
  if err != nil {
    return err
  }
  user.Password = string(hashedPassword)
  return nil
}


After some magic in the repository (I still don’t understand how it works, I understand)

package repository

import (
  "Avangard/exception"
  model "Avangard/models"
  "context"

  paginate "github.com/gobeam/mongo-go-pagination"
  "go.mongodb.org/mongo-driver/bson"
  "go.mongodb.org/mongo-driver/bson/primitive"
  "go.mongodb.org/mongo-driver/mongo"
)

var cntx context.Context = context.TODO()

type UserRepository interface {
  GetAllUser(page int64, limit int64) (*model.PagedUser, error)
  SaveUser(user *model.User) (*model.User, error)
  FindByEmail(email string) (*model.User, error)
  GetUser(id string) (*model.User, error)
  UpdateUser(id string, user *model.User) (*model.User, error)
  DeleteUser(id string) error
}

type userRepositoryImpl struct {
  Connection *mongo.Database
}

func (userRepository *userRepositoryImpl) FindByEmail(email string) (*model.User, error) {
  var existingUser model.User
  filter := bson.M{"email": email}
  err := userRepository.Connection.Collection("users").FindOne(cntx, filter).Decode(&existingUser)
  if err != nil {
    return nil, err
  }
  return &existingUser, nil
}

func NewUserRepository(Connection *mongo.Database) UserRepository {
  return &userRepositoryImpl{Connection: Connection}
}

func (userRepository *userRepositoryImpl) GetAllUser(page int64, limit int64) (*model.PagedUser, error) {
  var users []model.User

  filter := bson.M{}

  collection := userRepository.Connection.Collection("users")

  //	projection := bson.D{
  //		{"id", 1},
  //		{"firstName", 1},
  //		{"lastName", 1},
  //		{"email", 1},
  //	}
  //

  projection := bson.D{
    {
      Key:   "id",
      Value: 1,
    },
    {
      Key:   "firstName",
      Value: 1,
    }, {
      Key:   "lastName",
      Value: 1,
    },
    {
      Key:   "email",
      Value: 1,
    }}

  paginatedData, err := paginate.New(collection).Context(cntx).Limit(limit).Page(page).Select(projection).Filter(filter).Decode(&users).Find()
  if err != nil {
    return nil, err
  }

  return &model.PagedUser{
    Data:     users,
    PageInfo: paginatedData.Pagination,
  }, nil
}

func (userRepository *userRepositoryImpl) SaveUser(user *model.User) (*model.User, error) {
  user.ID = primitive.NewObjectID()

  _, err := userRepository.Connection.Collection("users").InsertOne(cntx, user)
  if err != nil {
    return nil, err
  }

  user.Password = ""
  return user, nil
}

func (userRepository *userRepositoryImpl) GetUser(id string) (*model.User, error) {
  var existingUser model.User
  objectId, _ := primitive.ObjectIDFromHex(id)
  filter := bson.M{"_id": objectId}

  err := userRepository.Connection.Collection("users").FindOne(cntx, filter).Decode(&existingUser)
  if err != nil {
    return nil, exception.ResourceNotFoundException("User", "id", id)
  }

  existingUser.Password = ""
  return &existingUser, nil
}

func (userRepository *userRepositoryImpl) UpdateUser(id string, user *model.User) (*model.User, error) {
  objectId, _ := primitive.ObjectIDFromHex(id)
  filter := bson.M{"_id": objectId}

  result, err := userRepository.Connection.Collection("users").ReplaceOne(cntx, filter, user)
  if err != nil {
    return nil, err
  }
  if result.MatchedCount == 0 {
    return nil, exception.ResourceNotFoundException("User", "id", id)
  }

  user.ID = objectId
  user.Password = ""
  return user, nil
}

func (userRepository *userRepositoryImpl) DeleteUser(id string) error {
  objectId, _ := primitive.ObjectIDFromHex(id)
  filter := bson.M{"_id": objectId}

  result, err := userRepository.Connection.Collection("users").DeleteOne(cntx, filter)
  if err != nil {
    return err
  }
  if result.DeletedCount == 0 {
    return exception.ResourceNotFoundException("User", "id", id)
  }

  return nil
}


This magic is also not fully understood, how to process form-data and what exactly to return

package util

import (
  "github.com/labstack/echo/v4"
)

func Negotiate(c echo.Context, code int, i interface{}) error {
  mediaType := c.QueryParam("mediaType")

  switch mediaType {
  case "xml":
    return c.XML(code, i)
  case "json":
    return c.JSON(code, i)
  //case "multipart/form-data":
  //	return
  default:
    return c.JSON(code, i)
  }
}


If you specify Content-Type application/json with Postman and send the data, then everything works fine, but if you send from the multipart/form-data form, then the validation simply does not pass, it says that the validation did not pass {"time":"2021-05-22T21 :45:31.134641998+05:00","level":"ERROR","prefix":"echo","file":"error.go","line":"53","message":"code =400, message=Key: 'UserInput.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag\nKey: 'UserInput.LastName' \nKey: 'UserInput.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'UserInput.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

Please help!

Answer the question

In order to leave comments, you need to log in

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question