Merge pull request 'create CMS error handling' (#25) from TM-578 into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/25 Reviewed-by: Hadi Barzegar <barzagarhadi@gmail.com>
This commit is contained in:
@@ -204,6 +204,7 @@ func main() {
|
||||
userValidator := user.NewValidationService()
|
||||
customerValidator := customer.NewValidationService()
|
||||
_ = kanban.NewValidationService() // Register hexcolor validator
|
||||
_ = cms.NewValidationService() // Register mediaRef validator
|
||||
|
||||
// Initialize services with repositories
|
||||
auditLogger := audit.NewLogger(logger)
|
||||
|
||||
@@ -7,7 +7,7 @@ type CMSForm struct {
|
||||
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
|
||||
Language string `json:"language" valid:"optional,length(2|100)" example:"en"`
|
||||
CompanyName string `json:"company_name" valid:"optional,length(2|100)" example:"Opplens"`
|
||||
Country string `json:"country" valid:"optional,length(3|3)" example:"SWE"`
|
||||
Country string `json:"country" valid:"optional,length(2|3)" example:"SWE"`
|
||||
Type CMSType `json:"type" valid:"optional,in(company|organization)" example:"company"`
|
||||
Hero heroSectionForm `json:"hero" valid:"optional"`
|
||||
Chart chartSectionForm `json:"chart" valid:"optional"`
|
||||
@@ -24,7 +24,7 @@ type heroSectionForm struct {
|
||||
Description string `json:"description" valid:"optional,length(2|1000)" example:"Hero Description"`
|
||||
ButtonText string `json:"button_text" valid:"optional,length(2|100)" example:"Button Text"`
|
||||
ButtonLink string `json:"button_link" valid:"optional,url" example:"https://example.com"`
|
||||
GifFile string `json:"gif_file" valid:"optional,url" example:"https://example.com/gif.gif"`
|
||||
GifFile string `json:"gif_file" valid:"optional,mediaRef" example:"674abc123def456789012345"`
|
||||
}
|
||||
|
||||
// chartSectionForm represents the form for creating/updating the chart section
|
||||
@@ -49,7 +49,7 @@ type cardSectionForm struct {
|
||||
|
||||
// cardForm represents the form for creating/updating a card
|
||||
type cardForm struct {
|
||||
Icon string `json:"icon" valid:"optional,url" example:"https://example.com/icon.png"`
|
||||
Icon string `json:"icon" valid:"optional,mediaRef" example:"674abc123def456789012345"`
|
||||
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
|
||||
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
|
||||
}
|
||||
@@ -73,7 +73,7 @@ type formFieldForm struct {
|
||||
// footerSectionForm represents the form for creating/updating the footer section
|
||||
type footerSectionForm struct {
|
||||
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
|
||||
Phone string `json:"phone" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||
Phone string `json:"phone" valid:"optional,length(0|30)" example:"+1234567890"`
|
||||
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
|
||||
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
|
||||
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
|
||||
|
||||
+12
-29
@@ -53,21 +53,17 @@ func (h *Handler) GetByKey(c echo.Context) error {
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/cms [post]
|
||||
func (h *Handler) Create(c echo.Context) error {
|
||||
form, err := response.Parse[CMSForm](c)
|
||||
form, err := ParseForm(c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
return handleParseError(c, err)
|
||||
}
|
||||
|
||||
// Create CMS
|
||||
cms, err := h.service.Create(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
if err == ErrCMSKeyExists {
|
||||
return response.Conflict(c, "CMS with this key already exists")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to create CMS")
|
||||
return handleServiceError(c, err, "Failed to create marketing page")
|
||||
}
|
||||
|
||||
return response.Created(c, cms, "CMS entry created successfully")
|
||||
return response.Created(c, cms, "Marketing page created successfully")
|
||||
}
|
||||
|
||||
// GetByID handles retrieving a CMS entry by ID (admin only)
|
||||
@@ -86,13 +82,10 @@ func (h *Handler) GetByID(c echo.Context) error {
|
||||
|
||||
cms, err := h.service.GetByID(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
if err.Error() == "cms not found" {
|
||||
return response.NotFound(c, "CMS entry not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to get CMS entry")
|
||||
return handleServiceError(c, err, "Failed to get marketing page")
|
||||
}
|
||||
|
||||
return response.Success(c, cms, "CMS entry retrieved successfully")
|
||||
return response.Success(c, cms, "Marketing page retrieved successfully")
|
||||
}
|
||||
|
||||
// Update handles updating a CMS entry (admin only)
|
||||
@@ -112,24 +105,17 @@ func (h *Handler) GetByID(c echo.Context) error {
|
||||
func (h *Handler) Update(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
form, err := response.Parse[CMSForm](c)
|
||||
form, err := ParseForm(c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
return handleParseError(c, err)
|
||||
}
|
||||
|
||||
// Update CMS
|
||||
cms, err := h.service.Update(c.Request().Context(), id, form)
|
||||
if err != nil {
|
||||
if err == ErrCMSKeyExists {
|
||||
return response.Conflict(c, "CMS with this key already exists")
|
||||
}
|
||||
if err.Error() == "cms not found" {
|
||||
return response.NotFound(c, "CMS entry not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update CMS entry")
|
||||
return handleServiceError(c, err, "Failed to update marketing page")
|
||||
}
|
||||
|
||||
return response.Success(c, cms, "CMS entry updated successfully")
|
||||
return response.Success(c, cms, "Marketing page updated successfully")
|
||||
}
|
||||
|
||||
// Delete handles deleting a CMS entry (admin only)
|
||||
@@ -148,13 +134,10 @@ func (h *Handler) Delete(c echo.Context) error {
|
||||
|
||||
err := h.service.Delete(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
if err.Error() == "cms not found" {
|
||||
return response.NotFound(c, "CMS entry not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to delete CMS entry")
|
||||
return handleServiceError(c, err, "Failed to delete marketing page")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "CMS entry deleted successfully")
|
||||
return response.Success(c, nil, "Marketing page deleted successfully")
|
||||
}
|
||||
|
||||
// Search handles searching CMS entries (admin only)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package cms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func handleParseError(c echo.Context, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "invalid request format") {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
return response.ValidationError(c, "Please fix the validation errors", FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
func handleServiceError(c echo.Context, err error, fallback string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, ErrCMSKeyExists) {
|
||||
return response.Conflict(c, "A marketing page with this key already exists")
|
||||
}
|
||||
if errors.Is(err, ErrCMSNotFound) {
|
||||
return response.NotFound(c, "Marketing page not found")
|
||||
}
|
||||
return response.InternalServerError(c, fallback)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
@@ -90,6 +91,9 @@ func (s *cmsService) GetByID(ctx context.Context, id string) (*CMSResponse, erro
|
||||
|
||||
cms, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrCMSNotFound) {
|
||||
return nil, ErrCMSNotFound
|
||||
}
|
||||
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
@@ -108,6 +112,9 @@ func (s *cmsService) GetByKey(ctx context.Context, key string) (*CMSResponse, er
|
||||
|
||||
cms, err := s.repo.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrCMSNotFound) {
|
||||
return nil, ErrCMSNotFound
|
||||
}
|
||||
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
|
||||
"key": key,
|
||||
"error": err.Error(),
|
||||
@@ -128,6 +135,9 @@ func (s *cmsService) Update(ctx context.Context, id string, form *CMSForm) (*CMS
|
||||
// Get existing CMS
|
||||
cms, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrCMSNotFound) {
|
||||
return nil, ErrCMSNotFound
|
||||
}
|
||||
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
@@ -175,6 +185,9 @@ func (s *cmsService) Delete(ctx context.Context, id string) error {
|
||||
})
|
||||
|
||||
if err := s.repo.Delete(ctx, id); err != nil {
|
||||
if errors.Is(err, ErrCMSNotFound) {
|
||||
return ErrCMSNotFound
|
||||
}
|
||||
s.logger.Error("Failed to delete CMS", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package cms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
objectIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{24}$`)
|
||||
filePathPattern = regexp.MustCompile(`^/[^/]*/v\d+/files/[a-fA-F0-9]{24}(/download)?$`)
|
||||
)
|
||||
|
||||
// ValidationService registers CMS-specific govalidator rules.
|
||||
type ValidationService struct{}
|
||||
|
||||
// NewValidationService registers custom validators used by CMS forms.
|
||||
func NewValidationService() ValidationService {
|
||||
govalidator.CustomTypeTagMap.Set("mediaRef", govalidator.CustomTypeValidator(func(i interface{}, _ interface{}) bool {
|
||||
value, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return isMediaReference(value)
|
||||
}))
|
||||
|
||||
return ValidationService{}
|
||||
}
|
||||
|
||||
func isMediaReference(value string) bool {
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
if govalidator.IsURL(value) {
|
||||
return true
|
||||
}
|
||||
if objectIDPattern.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
return filePathPattern.MatchString(value)
|
||||
}
|
||||
|
||||
// ValidateCMSForm validates a CMS create/update payload.
|
||||
func ValidateCMSForm(form *CMSForm) error {
|
||||
_, err := govalidator.ValidateStruct(form)
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseForm binds and validates a CMS form from the request.
|
||||
func ParseForm(c echo.Context) (*CMSForm, error) {
|
||||
var form CMSForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return nil, fmt.Errorf("invalid request format: %w", err)
|
||||
}
|
||||
if err := ValidateCMSForm(&form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &form, nil
|
||||
}
|
||||
|
||||
// FormatValidationErrors converts govalidator errors into user-facing messages.
|
||||
func FormatValidationErrors(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
errs, ok := err.(govalidator.Errors)
|
||||
if !ok {
|
||||
return humanizeValidationError(err.Error())
|
||||
}
|
||||
|
||||
messages := make([]string, 0, len(errs))
|
||||
for _, item := range errs {
|
||||
messages = append(messages, humanizeValidationError(item.Error()))
|
||||
}
|
||||
return strings.Join(messages, "; ")
|
||||
}
|
||||
|
||||
func humanizeValidationError(raw string) string {
|
||||
field, rule, ok := parseGovalidatorError(raw)
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
|
||||
label := fieldLabel(field)
|
||||
lowerRule := strings.ToLower(rule)
|
||||
|
||||
switch {
|
||||
case strings.Contains(lowerRule, "mediaref"):
|
||||
return label + " must be a valid URL or uploaded file"
|
||||
case strings.Contains(lowerRule, "url"):
|
||||
return label + " must be a valid URL"
|
||||
case strings.Contains(lowerRule, "email"):
|
||||
return label + " must be a valid email address"
|
||||
case strings.Contains(lowerRule, "length"):
|
||||
return label + " has an invalid length"
|
||||
case strings.Contains(lowerRule, "range"):
|
||||
return label + " is out of the allowed range"
|
||||
case strings.Contains(lowerRule, "in("):
|
||||
return label + " has an invalid value"
|
||||
default:
|
||||
return label + " is invalid"
|
||||
}
|
||||
}
|
||||
|
||||
func parseGovalidatorError(raw string) (field, rule string, ok bool) {
|
||||
parts := strings.SplitN(raw, ": ", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
|
||||
func fieldLabel(field string) string {
|
||||
field = strings.TrimPrefix(field, "CMSForm.")
|
||||
replacer := strings.NewReplacer(
|
||||
"Hero.", "Hero section: ",
|
||||
"Chart.", "Chart section: ",
|
||||
"Features.", "Features section: ",
|
||||
"Challenges.", "Challenges section: ",
|
||||
"Advantages.", "Advantages section: ",
|
||||
"Contact.", "Contact section: ",
|
||||
"Footer.", "Footer section: ",
|
||||
".", " ",
|
||||
)
|
||||
label := replacer.Replace(field)
|
||||
label = strings.ReplaceAll(label, "Cards 0", "card 1")
|
||||
label = strings.ReplaceAll(label, "Cards 1", "card 2")
|
||||
label = strings.ReplaceAll(label, "Cards 2", "card 3")
|
||||
label = strings.ReplaceAll(label, "Cards 3", "card 4")
|
||||
label = strings.ReplaceAll(label, "Cards 4", "card 5")
|
||||
return label
|
||||
}
|
||||
Reference in New Issue
Block a user