diff --git a/main b/main index 118af4d..1cea59f 100755 Binary files a/main and b/main differ diff --git a/modules/equipment/equipment.go b/modules/equipment/equipment.go index 35cb1d3..efda679 100644 --- a/modules/equipment/equipment.go +++ b/modules/equipment/equipment.go @@ -1,10 +1,16 @@ package equipment import ( + "encoding/base64" + "encoding/json" + "jannex/admin-dashboard-backend/modules/config" "jannex/admin-dashboard-backend/modules/database" "jannex/admin-dashboard-backend/modules/structs" + "os" + "strings" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" "github.com/rs/zerolog/log" ) @@ -12,39 +18,139 @@ const Base = "https://inv.ex.umbach.dev" const apiBase = Base + "/api" const ApiToken = "1367f15d21935e4eb540f897946fb5cd98485c3f" -func GetEquipmentDocumentation(stockItemId string, c *fiber.Ctx) error { +func InvexApiRequestClient(requestMethod string, url string) (statusCode int, body []byte, err error) { + a := fiber.AcquireAgent() + + a.Add("Authorization", "Token "+ApiToken) + + req := a.Request() + req.Header.SetMethod(requestMethod) + req.SetRequestURI(url) + + if err := a.Parse(); err != nil { + log.Error().Msgf("Failed to parse request, err: %s", err) + return 0, nil, err + } + + code, body, _ := a.Bytes() + + if code == 401 { + log.Error().Msgf("invex not authorized, code: %d", code) + return code, nil, err + } + + if code == 404 { + log.Error().Msgf("Invex stock item not found, statusCode: %d", statusCode) + return statusCode, nil, err + } + + return code, body, nil +} + +type Notes struct { + Image string + Description string +} + +func getImageType(data string) string { + switch data { + case "data:image/png;base64": + return "png" + default: + return "jpeg" + } +} + +func CreateEquipmentDocumentation(c *fiber.Ctx, body structs.ApiCreateEquipmentDocumentationRequest) error { + var bodyNotes []map[string]string + + err := json.Unmarshal(body.Notes, &bodyNotes) + + if err != nil { + log.Error().Msgf("Failed to unmarshal json, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + newEquipmentDocumentation := structs.EquipmentDocumentation{ + Id: uuid.New().String(), + StockItemId: body.StockItemId, + Type: body.Type, + Title: body.Title, + } + + if err := os.Mkdir(config.Cfg.FolderPaths.PublicStatic+"/equipmentdocumentation/"+newEquipmentDocumentation.Id, os.ModePerm); err != nil { + log.Error().Msgf("Failed to create folder, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + var notes []Notes + + // loop throught bodyNotes and save image to static folder under random name + for _, bodyNote := range bodyNotes { + var newFileName string + + if bodyNote["Image"] != "" { + // image is sent as base64 string + // decode it and save it to the static folder + + // encoded base64 image structure is: data:image/jpeg;base64,/9j/4AA... + splittedImageData := strings.Split(bodyNote["Image"], ",") + + decodedImage, err := base64.StdEncoding.DecodeString(splittedImageData[1]) + + if err != nil { + log.Error().Msgf("Failed to decode base64 string, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + + } + + newFileName = uuid.New().String() + "." + getImageType(splittedImageData[0]) + + err = os.WriteFile(config.Cfg.FolderPaths.PublicStatic+"/equipmentdocumentation/"+newEquipmentDocumentation.Id+"/"+newFileName, decodedImage, 0644) + + if err != nil { + log.Error().Msgf("Failed to save image, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + } + + notes = append(notes, Notes{ + Image: newFileName, + Description: bodyNote["Description"], + }) + } + + marshaledNotes, err := json.Marshal(notes) + + if err != nil { + log.Error().Msgf("Failed to marshal notes, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + newEquipmentDocumentation.Notes = string(marshaledNotes) + + database.DB.Create(&newEquipmentDocumentation) + + return c.JSON(fiber.Map{"message": "ok"}) +} + +func GetEquipmentDocumentations(stockItemId string, c *fiber.Ctx) error { var documentations []structs.EquipmentDocumentation database.DB.Where("stock_item_id = ?", stockItemId).Find(&documentations) + var err error statusCode := 200 if len(documentations) == 0 { // there are no documentations for this equipment on the our database // so there will be checked on invex if the stock item exists + statusCode, _, err = InvexApiRequestClient(fiber.MethodGet, apiBase+"/stock/"+stockItemId+"/") - a := fiber.AcquireAgent() - - a.Add("Authorization", "Token "+ApiToken) - - req := a.Request() - req.Header.SetMethod(fiber.MethodGet) - req.SetRequestURI(apiBase + "/stock/" + stockItemId + "/") - - if err := a.Parse(); err != nil { - log.Error().Msgf("Failed to parse request, err: %s", err) + if err != nil { + log.Error().Msgf("Invex api request error: %s", err) return c.SendStatus(fiber.StatusInternalServerError) } - - statusCode, _, _ = a.Bytes() - - if statusCode == 401 { - log.Error().Msgf("invex not authorized, statusCode: %d", statusCode) - } - - if statusCode == 404 { - log.Error().Msgf("Invex stock item not found, statusCode: %d", statusCode) - } } return c.JSON(structs.ApiEquipmentDocumentationResponse{ @@ -52,51 +158,88 @@ func GetEquipmentDocumentation(stockItemId string, c *fiber.Ctx) error { Documentations: documentations}) } -/* -func GetEquipmentDocumentation(stockItemId string, c *fiber.Ctx) error { - equipment := structs.Equipment{Id: stockItemId} +func GetEquipmentDocumentation(stockItemId string, documentationId string, c *fiber.Ctx) error { + var documentation structs.EquipmentDocumentation - if err := database.DB.Where("id = ?", stockItemId).First(&equipment).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - log.Info().Msgf("a %s", err) + database.DB.Where("id = ? AND stock_item_id = ?", documentationId, stockItemId).Find(&documentation) - log.Error().Msgf("Failed to get equipment, err: %s", err) - return c.JSON([]structs.EquipmentDocumentation{}) + return c.JSON(documentation) +} - } +func EditEquipmentDocumentation(c *fiber.Ctx, body structs.ApiEditEquipmentDocumentationRequest) error { + var bodyNotes []map[string]string - // if equipment already exists, return the documentation - if equipment.Name != "" { - var equipmentDocumentations []structs.EquipmentDocumentation + err := json.Unmarshal(body.Notes, &bodyNotes) - database.DB.Where("equipment_id = ?", stockItemId).Find(&equipmentDocumentations) - - return c.JSON(equipmentDocumentations) - } - - // create new equipment - a := fiber.AcquireAgent() - - a.Add("Authorization", "Token "+ApiToken) - - req := a.Request() - req.Header.SetMethod(fiber.MethodGet) - req.SetRequestURI(apiBase + "/stock/" + stockItemId + "/") - - if err := a.Parse(); err != nil { - log.Error().Msgf("Failed to parse request, err: %s", err) + if err != nil { + log.Error().Msgf("Failed to unmarshal json, err: %s", err) return c.SendStatus(fiber.StatusInternalServerError) } - code, body, _ := a.Bytes() + var notes []Notes - if code == 401 { - log.Error().Msgf("invex not authorized, code: %d", code) + // loop throught bodyNotes and save image to static folder under random name + for _, bodyNote := range bodyNotes { + newFileName := bodyNote["Image"] + + if bodyNote["Image"] != "" { + // check if image starts with base64 string + if strings.HasPrefix(bodyNote["Image"], "data:") { + // image is sent as base64 string + // decode it and save it to the static folder + + // encoded base64 image structure is: data:image/jpeg;base64,/9j/4AA... + splittedImageData := strings.Split(bodyNote["Image"], ",") + + decodedImage, err := base64.StdEncoding.DecodeString(splittedImageData[1]) + + if err != nil { + log.Error().Msgf("Failed to decode base64 string, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + + } + + newFileName = uuid.New().String() + "." + getImageType(splittedImageData[0]) + + err = os.WriteFile(config.Cfg.FolderPaths.PublicStatic+"/equipmentdocumentation/"+body.DocumentationId+"/"+newFileName, decodedImage, 0644) + + if err != nil { + log.Error().Msgf("Failed to save image, err: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + } + } + + notes = append(notes, Notes{ + Image: newFileName, + Description: bodyNote["Description"], + }) + } + + marshaledNotes, err := json.Marshal(notes) + + if err != nil { + log.Error().Msgf("Failed to marshal notes, err: %s", err) return c.SendStatus(fiber.StatusInternalServerError) } - if code == 404 { - log.Error().Msgf("Inven stock item not found, code: %d", code) - return c.SendStatus(fiber.StatusNotFound) + database.DB.Model(&structs.EquipmentDocumentation{}).Where("id = ?", body.DocumentationId).Updates(structs.EquipmentDocumentation{ + Type: body.Type, + Title: body.Title, + Notes: string(marshaledNotes), + }) + + return c.JSON(fiber.Map{"message": "ok"}) +} + +// fetching the thumbnail from the invex server and sending it back to the client +func GetEquipmentInvexThumbnail(c *fiber.Ctx, stockItemId string) error { + // first request to /api/stock/:stockItemId/ to get the thumbnail url + _, body, err := InvexApiRequestClient(fiber.MethodGet, apiBase+"/stock/"+stockItemId+"/") + + if err != nil { + log.Error().Msgf("Invex api request error: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) } // parse body as json @@ -108,32 +251,17 @@ func GetEquipmentDocumentation(stockItemId string, c *fiber.Ctx) error { } partDetail := data["part_detail"].(map[string]interface{}) + thumbnail := partDetail["thumbnail"].(string) - database.DB.Create(&structs.Equipment{ - Id: stockItemId, - Name: partDetail["name"].(string), - Thumbnail: partDetail["thumbnail"].(string), - }) + // second request to /media/part_images/:thumbnail to get the thumbnail image + _, body, err = InvexApiRequestClient(fiber.MethodGet, Base+"/"+thumbnail) - return c.JSON([]structs.EquipmentDocumentation{}) + if err != nil { + log.Error().Msgf("Invex api request error: %s", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + c.Set("Content-Type", "image/png") + + return c.Send(body) } -*/ - -/* -func GetEquipment() []structs.Equipment { - var equipments []structs.Equipment - - database.DB.Find(&equipments) - - return equipments -}*/ - -/* -// return whether the scanned equipment is existing in the database -func IsEquipmentExisting(stockItemId string) bool { - var equipment structs.Equipment - - database.DB.Where("id = ?", stockItemId).FirstOrCreate(&equipment) - - return equipment.Id != "" -} */ diff --git a/modules/structs/equipment.go b/modules/structs/equipment.go index 86c3321..1acad6a 100644 --- a/modules/structs/equipment.go +++ b/modules/structs/equipment.go @@ -25,10 +25,15 @@ type EquipmentDocumentation struct { UpdatedAt time.Time } -type ApiEquipmentParamsRequest struct { +type ApiEquipmentRequest struct { StockItemId string `json:"stockItemId"` } +type ApiGetDocumentationEquipmentRequest struct { + StockItemId string `json:"stockItemId"` + DocumentationId string `json:"documentationId"` +} + type ApiCreateEquipmentDocumentationRequest struct { StockItemId string `json:"stockItemId"` Type uint8 `json:"type"` @@ -40,3 +45,10 @@ type ApiEquipmentDocumentationResponse struct { Status int Documentations []EquipmentDocumentation } + +type ApiEditEquipmentDocumentationRequest struct { + DocumentationId string `json:"documentationId"` + Type uint8 `json:"type"` + Title string `json:"title"` + Notes json.RawMessage `json:"notes"` +} diff --git a/modules/utils/utils.go b/modules/utils/utils.go index e5f0b9b..a10243f 100644 --- a/modules/utils/utils.go +++ b/modules/utils/utils.go @@ -66,3 +66,31 @@ func GenerateSession() (string, error) { return string(r), nil } + +func ParamsParserHelper(c *fiber.Ctx, params interface{}) error { + if err := c.ParamsParser(params); err != nil { + log.Error().Msgf("Failed to parse params, err: %s", err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(err) + } + + if errValidation := ValidateStruct(params); errValidation != nil { + log.Error().Msgf("Failed to validate params, err: %v", errValidation) + return c.Status(fiber.StatusBadRequest).JSON(errValidation) + } + + return c.Next() +} + +func BodyParserHelper(c *fiber.Ctx, body interface{}) error { + if err := c.BodyParser(body); err != nil { + log.Error().Msgf("Failed to parse body, err: %s", err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(err) + } + + if errValidation := ValidateStruct(body); errValidation != nil { + log.Error().Msgf("Failed to validate body, err: %v", errValidation) + return c.Status(fiber.StatusBadRequest).JSON(errValidation) + } + + return c.Next() +} diff --git a/modules/utils/validator.go b/modules/utils/validator.go index 5a007e1..9894559 100644 --- a/modules/utils/validator.go +++ b/modules/utils/validator.go @@ -38,5 +38,5 @@ func ValidatorInit() { Validate.RegisterStructValidationMapRules(groupTaskRules, structs.ApiGroupTaskRequest{}) - Validate.RegisterStructValidationMapRules(equipmentRules, structs.ApiEquipmentParamsRequest{}) + Validate.RegisterStructValidationMapRules(equipmentRules, structs.ApiEquipmentRequest{}) } diff --git a/routers/router/api/v1/equipment/equipment.go b/routers/router/api/v1/equipment/equipment.go index 60a70de..dce80b8 100644 --- a/routers/router/api/v1/equipment/equipment.go +++ b/routers/router/api/v1/equipment/equipment.go @@ -1,153 +1,52 @@ package equipment import ( - "encoding/base64" - "encoding/json" - "jannex/admin-dashboard-backend/modules/config" - "jannex/admin-dashboard-backend/modules/database" "jannex/admin-dashboard-backend/modules/equipment" "jannex/admin-dashboard-backend/modules/structs" "jannex/admin-dashboard-backend/modules/utils" - "os" - "strings" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" "github.com/rs/zerolog/log" ) -func getImageType(data string) string { - switch data { - case "data:image/png;base64": - return "png" - default: - return "jpeg" - } -} - -type Notes struct { - Image string - Description string -} - func CreateEquipmentDocumentation(c *fiber.Ctx) error { var body structs.ApiCreateEquipmentDocumentationRequest - if err := c.BodyParser(&body); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) - } + utils.BodyParserHelper(c, &body) - var bodyNotes []map[string]string + log.Info().Msgf("body StockItemId: %+v", body.StockItemId) - err := json.Unmarshal(body.Notes, &bodyNotes) + return equipment.CreateEquipmentDocumentation(c, body) +} - if err != nil { - log.Error().Msgf("Failed to unmarshal json, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - } +func GetEquipmentDocumentations(c *fiber.Ctx) error { + var params structs.ApiEquipmentRequest - newEquipmentDocumentation := structs.EquipmentDocumentation{ - Id: uuid.New().String(), - StockItemId: body.StockItemId, - Type: body.Type, - Title: body.Title, - } + utils.ParamsParserHelper(c, ¶ms) - // TODO: only create folder if there is an image - if err := os.Mkdir(config.Cfg.FolderPaths.PublicStatic+"/equipmentdocumentation/"+newEquipmentDocumentation.Id, os.ModePerm); err != nil { - log.Error().Msgf("Failed to create folder, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - - var notes []Notes - - // loop throught bodyNotes and save image to static folder under random name - for _, bodyNote := range bodyNotes { - var newFileName string - - if bodyNote["image"] != "" { - // image is sent as base64 string - // decode it and save it to the static folder - - // encoded base64 image structure is: data:image/jpeg;base64,/9j/4AA... - splittedImageData := strings.Split(bodyNote["image"], ",") - - decodedImage, err := base64.StdEncoding.DecodeString(splittedImageData[1]) - - if err != nil { - log.Error().Msgf("Failed to decode base64 string, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - - } - - newFileName = uuid.New().String() + "." + getImageType(splittedImageData[0]) - - err = os.WriteFile(config.Cfg.FolderPaths.PublicStatic+"/equipmentdocumentation/"+newEquipmentDocumentation.Id+"/"+newFileName, decodedImage, 0644) - - if err != nil { - log.Error().Msgf("Failed to save image, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - } - - notes = append(notes, Notes{ - Image: newFileName, - Description: bodyNote["description"], - }) - } - - marshaledNotes, err := json.Marshal(notes) - - if err != nil { - log.Error().Msgf("Failed to marshal notes, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - - newEquipmentDocumentation.Notes = string(marshaledNotes) - - database.DB.Create(&newEquipmentDocumentation) - - return c.JSON(fiber.Map{"message": "ok"}) + return equipment.GetEquipmentDocumentations(params.StockItemId, c) } func GetEquipmentDocumentation(c *fiber.Ctx) error { - var body structs.ApiEquipmentParamsRequest + var params structs.ApiGetDocumentationEquipmentRequest - if err := c.ParamsParser(&body); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) - } + utils.ParamsParserHelper(c, ¶ms) - if errValidation := utils.ValidateStruct(body); errValidation != nil { - log.Error().Msgf("Failed to validate body, err: %v", errValidation) - return c.Status(fiber.StatusBadRequest).JSON(errValidation) - } - - return equipment.GetEquipmentDocumentation(body.StockItemId, c) + return equipment.GetEquipmentDocumentation(params.StockItemId, params.DocumentationId, c) +} + +func EditEquipmentDocumentation(c *fiber.Ctx) error { + var body structs.ApiEditEquipmentDocumentationRequest + + utils.BodyParserHelper(c, &body) + + return equipment.EditEquipmentDocumentation(c, body) } -// fetching the thumbnail from the invex server and sending it back to the client func GetEquipmentThumbnail(c *fiber.Ctx) error { - //resp, err := http.Get(equipment.Base + "/media/part_images/part_152_image.thumbnail.png") + var params structs.ApiEquipmentRequest - a := fiber.AcquireAgent() - - a.Add("Authorization", "Token "+equipment.ApiToken) - - req := a.Request() - req.Header.SetMethod(fiber.MethodGet) - req.SetRequestURI(equipment.Base + "/media/part_images/" + c.Params("stockItemThumbnail")) - - if err := a.Parse(); err != nil { - log.Error().Msgf("Failed to parse request, err: %s", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - - code, body, errs := a.Bytes() - - log.Info().Msgf("code %d %s", code, errs) - - c.Set("Content-Type", "image/png") - - return c.Send(body) + utils.ParamsParserHelper(c, ¶ms) + return equipment.GetEquipmentInvexThumbnail(c, params.StockItemId) } diff --git a/routers/router/router.go b/routers/router/router.go index a51d47f..698bf2d 100644 --- a/routers/router/router.go +++ b/routers/router/router.go @@ -41,10 +41,12 @@ func SetupRoutes(app *fiber.App) { // TODO: add user session validation //e.Get("/scanned/:stockItemId", requestAccessValidation, equipment.EquipmentScanned) //e.Get("/", requestAccessValidation, equipment.GetEquipment) - e.Get("/documentation/:stockItemId", requestAccessValidation, equipment.GetEquipmentDocumentation) + e.Get("/documentations/:stockItemId", requestAccessValidation, equipment.GetEquipmentDocumentations) e.Post("/documentation/create", requestAccessValidation, equipment.CreateEquipmentDocumentation) - // access validation here implented as it would require to implement authorization on web client side on Avatar Component - e.Get("/thumbnail/media/part_images/:stockItemThumbnail", equipment.GetEquipmentThumbnail) + e.Get("/documentation/:stockItemId/:documentationId", requestAccessValidation, equipment.GetEquipmentDocumentation) + e.Post("/documentation/edit", requestAccessValidation, equipment.EditEquipmentDocumentation) + // access validation here implemented as it would require to implement authorization on web client side on Avatar Component + e.Get("/thumbnail/:stockItemId", equipment.GetEquipmentThumbnail) app.Static("/", config.Cfg.FolderPaths.PublicStatic) }