Files
agent-desk-ng/internal/services/system_config_support_validator.go

171 lines
6.0 KiB
Go

package services
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/i18nx"
"github.com/mlogclub/simple/common/strs"
)
type supportNavigationMenuValidator struct{}
func (supportNavigationMenuValidator) Validate(raw json.RawMessage) (json.RawMessage, []response.ConfigFieldError, error) {
var input []request.SupportNavigationMenuItemRequest
if err := json.Unmarshal(raw, &input); err != nil {
return nil, []response.ConfigFieldError{configFieldError("", "invalid_json", "error.supportConfig.navigationInvalidJSON")}, nil
}
items, fieldErrors := normalizeSupportNavigationMenu(input)
if len(fieldErrors) > 0 {
return nil, fieldErrors, nil
}
normalized, err := json.Marshal(items)
if err != nil {
return nil, nil, err
}
return normalized, nil, nil
}
func normalizeSupportNavigationMenu(input []request.SupportNavigationMenuItemRequest) ([]response.SupportNavigationMenuItemResponse, []response.ConfigFieldError) {
if len(input) == 0 {
return nil, []response.ConfigFieldError{configFieldError("navigationMenu", "required", "error.supportConfig.navigationRequired")}
}
if len(input) > 20 {
return nil, []response.ConfigFieldError{configFieldError("navigationMenu", "too_many", "error.supportConfig.navigationTooMany")}
}
seenIDs := make(map[string]int)
items, visibleCount, fieldErrors := normalizeSupportNavigationItems(input, "navigationMenu", 1, seenIDs)
if len(fieldErrors) > 0 {
return nil, fieldErrors
}
if visibleCount == 0 {
return nil, []response.ConfigFieldError{configFieldError("navigationMenu", "visible_required", "error.supportConfig.navigationVisibleRequired")}
}
return items, nil
}
func normalizeSupportNavigationItems(input []request.SupportNavigationMenuItemRequest, path string, depth int, seenIDs map[string]int) ([]response.SupportNavigationMenuItemResponse, int, []response.ConfigFieldError) {
items := make([]response.SupportNavigationMenuItemResponse, 0, len(input))
visibleCount := 0
for idx, raw := range input {
itemPath := fmt.Sprintf("%s[%d]", path, idx)
title := strings.TrimSpace(raw.Title)
if title == "" {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".title", "required", "error.supportConfig.navigationTitleRequired")}
}
if len([]rune(title)) > 64 {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".title", "too_long", "error.supportConfig.navigationTitleTooLong")}
}
link := strings.TrimSpace(raw.URL)
if link == "" {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".url", "required", "error.supportConfig.navigationURLRequired")}
}
if !isAllowedSupportNavigationURL(link) {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".url", "invalid_url", "error.supportConfig.navigationURLInvalid")}
}
id := normalizeSupportNavigationMenuID(raw.ID)
if id == "" {
id = "nav-" + strings.ReplaceAll(strs.UUID(), "-", "")[:12]
}
if count := seenIDs[id]; count > 0 {
id = id + "-" + strings.ReplaceAll(strs.UUID(), "-", "")[:6]
}
seenIDs[id]++
visible := true
if raw.Visible != nil {
visible = *raw.Visible
}
children := []response.SupportNavigationMenuItemResponse(nil)
if len(raw.Children) > 0 {
if depth >= 2 {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".children", "too_deep", "error.supportConfig.navigationTooDeep")}
}
if len(raw.Children) > 20 {
return nil, 0, []response.ConfigFieldError{configFieldError(itemPath+".children", "too_many", "error.supportConfig.navigationChildrenTooMany")}
}
nextChildren, nextVisibleCount, fieldErrors := normalizeSupportNavigationItems(raw.Children, itemPath+".children", depth+1, seenIDs)
if len(fieldErrors) > 0 {
return nil, 0, fieldErrors
}
children = nextChildren
if nextVisibleCount > 0 && visible {
visibleCount += nextVisibleCount
}
}
if visible {
visibleCount++
}
items = append(items, response.SupportNavigationMenuItemResponse{
ID: id,
Title: title,
URL: link,
OpenInNewWindow: raw.OpenInNewWindow,
Visible: visible,
SortNo: (idx + 1) * 10,
Children: children,
})
}
return items, visibleCount, nil
}
func isAllowedSupportNavigationURL(value string) bool {
if strings.HasPrefix(value, "/") {
return !strings.HasPrefix(value, "//")
}
parsed, err := url.Parse(value)
if err != nil {
return false
}
return parsed.Scheme == "http" || parsed.Scheme == "https"
}
func normalizeSupportNavigationMenuID(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
if value == "" {
return ""
}
var builder strings.Builder
for _, r := range value {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
builder.WriteRune(r)
}
}
return strings.Trim(builder.String(), "-_")
}
func configFieldError(path, code, key string) response.ConfigFieldError {
return response.ConfigFieldError{
Path: path,
Code: code,
MessageKey: key,
Message: i18nx.Get(key),
}
}
func localizeConfigFieldErrors(errors []response.ConfigFieldError, locale string) []response.ConfigFieldError {
if len(errors) == 0 {
return nil
}
ret := make([]response.ConfigFieldError, 0, len(errors))
for _, item := range errors {
if item.MessageKey != "" {
item.Message = i18nx.Getf(locale, item.MessageKey)
}
ret = append(ret, item)
}
return ret
}
func defaultSupportNavigationMenu() []response.SupportNavigationMenuItemResponse {
return []response.SupportNavigationMenuItemResponse{
{ID: "home", Title: i18nx.Get("systemConfig.support.navigationMenu.default.home"), URL: "/support", SortNo: 10, Visible: true},
{ID: "docs", Title: i18nx.Get("systemConfig.support.navigationMenu.default.docs"), URL: "/support/docs", SortNo: 20, Visible: true},
{ID: "community", Title: i18nx.Get("systemConfig.support.navigationMenu.default.community"), URL: "/support/community/posts", SortNo: 30, Visible: true},
}
}