2024-01-30 13:35:04 +05:30
|
|
|
package profiles
|
|
|
|
|
|
|
|
import (
|
2024-02-13 06:38:42 +05:30
|
|
|
"context"
|
2024-01-30 13:35:04 +05:30
|
|
|
"fmt"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
|
2024-02-01 12:42:34 +05:30
|
|
|
"ely.by/chrly/internal/db"
|
2024-01-30 13:35:04 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
type ProfilesRepository interface {
|
2024-02-13 06:38:42 +05:30
|
|
|
SaveProfile(ctx context.Context, profile *db.Profile) error
|
|
|
|
RemoveProfileByUuid(ctx context.Context, uuid string) error
|
2024-01-30 13:35:04 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func NewManager(pr ProfilesRepository) *Manager {
|
|
|
|
return &Manager{
|
|
|
|
ProfilesRepository: pr,
|
|
|
|
profileValidator: createProfileValidator(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Manager struct {
|
|
|
|
ProfilesRepository
|
|
|
|
profileValidator *validator.Validate
|
|
|
|
}
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
func (m *Manager) PersistProfile(ctx context.Context, profile *db.Profile) error {
|
2024-01-30 13:35:04 +05:30
|
|
|
validationErrors := m.profileValidator.Struct(profile)
|
|
|
|
if validationErrors != nil {
|
|
|
|
return mapValidationErrorsToCommonError(validationErrors.(validator.ValidationErrors))
|
|
|
|
}
|
|
|
|
|
|
|
|
profile.Uuid = cleanupUuid(profile.Uuid)
|
|
|
|
if profile.SkinUrl == "" || isClassicModel(profile.SkinModel) {
|
|
|
|
profile.SkinModel = ""
|
|
|
|
}
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
return m.ProfilesRepository.SaveProfile(ctx, profile)
|
2024-01-30 13:35:04 +05:30
|
|
|
}
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
func (m *Manager) RemoveProfileByUuid(ctx context.Context, uuid string) error {
|
|
|
|
return m.ProfilesRepository.RemoveProfileByUuid(ctx, cleanupUuid(uuid))
|
2024-01-30 13:35:04 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
type ValidationError struct {
|
|
|
|
Errors map[string][]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ValidationError) Error() string {
|
|
|
|
return "The profile is invalid and cannot be persisted"
|
|
|
|
}
|
|
|
|
|
|
|
|
func cleanupUuid(uuid string) string {
|
|
|
|
return strings.ReplaceAll(strings.ToLower(uuid), "-", "")
|
|
|
|
}
|
|
|
|
|
|
|
|
func createProfileValidator() *validator.Validate {
|
|
|
|
validate := validator.New()
|
|
|
|
|
|
|
|
regexUuidAny := regexp.MustCompile("(?i)^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}$")
|
|
|
|
_ = validate.RegisterValidation("uuid_any", func(fl validator.FieldLevel) bool {
|
|
|
|
return regexUuidAny.MatchString(fl.Field().String())
|
|
|
|
})
|
|
|
|
|
|
|
|
regexUsername := regexp.MustCompile(`^[-\w.!$%^&*()\[\]:;]+$`)
|
|
|
|
_ = validate.RegisterValidation("username", func(fl validator.FieldLevel) bool {
|
|
|
|
return regexUsername.MatchString(fl.Field().String())
|
|
|
|
})
|
|
|
|
|
|
|
|
validate.RegisterStructValidationMapRules(map[string]string{
|
|
|
|
"Username": "required,username,max=21",
|
|
|
|
"Uuid": "required,uuid_any",
|
|
|
|
"SkinUrl": "omitempty,url",
|
|
|
|
"SkinModel": "omitempty,max=20",
|
|
|
|
"CapeUrl": "omitempty,url",
|
|
|
|
"MojangTextures": "omitempty,base64",
|
|
|
|
"MojangSignature": "required_with=MojangTextures,omitempty,base64",
|
|
|
|
}, db.Profile{})
|
|
|
|
|
|
|
|
return validate
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapValidationErrorsToCommonError(err validator.ValidationErrors) *ValidationError {
|
|
|
|
resultErr := &ValidationError{make(map[string][]string)}
|
|
|
|
for _, e := range err {
|
|
|
|
// Manager can return multiple errors per field, but the current validation implementation
|
|
|
|
// returns only one error per field
|
|
|
|
resultErr.Errors[e.Field()] = []string{formatValidationErr(e)}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultErr
|
|
|
|
}
|
|
|
|
|
|
|
|
// The go-playground/validator lib already contains tools for translated errors output.
|
|
|
|
// However, the implementation is very heavy and becomes even more so when you need to add messages for custom validators.
|
|
|
|
// So for simplicity, I've extracted validation error formatting into this simple implementation
|
|
|
|
func formatValidationErr(err validator.FieldError) string {
|
|
|
|
switch err.Tag() {
|
|
|
|
case "required", "required_with":
|
|
|
|
return fmt.Sprintf("%s is a required field", err.Field())
|
|
|
|
case "username":
|
|
|
|
return fmt.Sprintf("%s must be a valid username", err.Field())
|
|
|
|
case "max":
|
|
|
|
return fmt.Sprintf("%s must be a maximum of %s in length", err.Field(), err.Param())
|
|
|
|
case "uuid_any":
|
|
|
|
return fmt.Sprintf("%s must be a valid UUID", err.Field())
|
|
|
|
case "url":
|
|
|
|
return fmt.Sprintf("%s must be a valid URL", err.Field())
|
|
|
|
case "base64":
|
|
|
|
return fmt.Sprintf("%s must be a valid Base64 string", err.Field())
|
|
|
|
default:
|
|
|
|
return fmt.Sprintf(`Field validation for "%s" failed on the "%s" tag`, err.Field(), err.Tag())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isClassicModel(model string) bool {
|
|
|
|
return model == "" || model == "classic" || model == "default" || model == "steve"
|
|
|
|
}
|