gohorsejobs/backend/internal/utils/sanitizer.go
Tiago Yamamoto 3a26af3df5 fix: global document and phone handling — remove Brazil-specific formatting
Frontend (jobs/new):
- Replace isValidCNPJ (checksum algorithm) with isValidDocument: accepts
  any tax document with 5–30 alphanumeric chars (CNPJ, EIN, VAT, etc.)
- Add cleanPhone(): strips formatting chars (dashes, spaces, parens) and
  keeps only digits + optional leading '+'; replaces cleanDigits+prepend
- Phone sent as '+5511999998888' if user typed '+55...', or '11999998888'
  if no country code was provided — no '+' blindly prepended anymore
- Company document sent stripped of all non-alphanumeric before API call
- Update label placeholder from '00.000.000/0000-00' to 'CNPJ, EIN, VAT...'
- Rename error key invalidCnpj → invalidDocument in all 3 locales (pt, en, es)

Backend (create_company use case):
- Add SanitizePhone() to utils/sanitizer.go: strips all non-digit chars
  except a leading '+'; '(11) 99999-8888' → '11999998888'
- Apply SanitizePhone to input.Phone before persisting to DB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 12:46:54 -06:00

111 lines
3 KiB
Go

package utils
import (
"html"
"regexp"
"strings"
"unicode/utf8"
)
// Sanitizer provides input sanitization utilities
type Sanitizer struct {
// Max lengths for common fields
MaxNameLength int
MaxDescriptionLength int
MaxEmailLength int
}
// DefaultSanitizer returns a sanitizer with default settings
func DefaultSanitizer() *Sanitizer {
return &Sanitizer{
MaxNameLength: 255,
MaxDescriptionLength: 10000,
MaxEmailLength: 320,
}
}
// SanitizeString escapes HTML and trims whitespace
func (s *Sanitizer) SanitizeString(input string) string {
if input == "" {
return ""
}
// Trim whitespace
result := strings.TrimSpace(input)
// Escape HTML entities to prevent XSS
result = html.EscapeString(result)
return result
}
// SanitizeName sanitizes a name field
func (s *Sanitizer) SanitizeName(input string) string {
sanitized := s.SanitizeString(input)
if utf8.RuneCountInString(sanitized) > s.MaxNameLength {
runes := []rune(sanitized)
sanitized = string(runes[:s.MaxNameLength])
}
return sanitized
}
// SanitizeEmail sanitizes and validates email format
func (s *Sanitizer) SanitizeEmail(input string) string {
sanitized := strings.TrimSpace(strings.ToLower(input))
if utf8.RuneCountInString(sanitized) > s.MaxEmailLength {
return ""
}
return sanitized
}
// SanitizeDescription sanitizes long text fields
func (s *Sanitizer) SanitizeDescription(input string) string {
sanitized := s.SanitizeString(input)
if utf8.RuneCountInString(sanitized) > s.MaxDescriptionLength {
runes := []rune(sanitized)
sanitized = string(runes[:s.MaxDescriptionLength])
}
return sanitized
}
// SanitizeSlug creates a URL-safe slug
func (s *Sanitizer) SanitizeSlug(input string) string {
// Convert to lowercase
result := strings.ToLower(strings.TrimSpace(input))
// Replace spaces with hyphens
result = strings.ReplaceAll(result, " ", "-")
// Remove non-alphanumeric characters except hyphens
reg := regexp.MustCompile(`[^a-z0-9-]`)
result = reg.ReplaceAllString(result, "")
// Remove multiple consecutive hyphens
reg = regexp.MustCompile(`-+`)
result = reg.ReplaceAllString(result, "-")
// Trim hyphens from ends
result = strings.Trim(result, "-")
return result
}
// StripHTML removes all HTML tags from input
func StripHTML(input string) string {
reg := regexp.MustCompile(`<[^>]*>`)
return reg.ReplaceAllString(input, "")
}
// SanitizePhone cleans a phone number, keeping only digits and an optional
// leading '+' (international dialing prefix). Removes all formatting characters
// such as spaces, dashes, parentheses, and dots.
//
// Examples:
//
// "+55 (11) 99999-8888" → "+5511999998888"
// "(11) 99999-8888" → "11999998888"
// "+1-800-555-0100" → "+18005550100"
func SanitizePhone(phone string) string {
phone = strings.TrimSpace(phone)
if phone == "" {
return ""
}
hasPlus := strings.HasPrefix(phone, "+")
digitsOnly := regexp.MustCompile(`\D`).ReplaceAllString(phone, "")
if hasPlus {
return "+" + digitsOnly
}
return digitsOnly
}