gohorsejobs/backend/internal/utils/document_validator.go
Tiago Yamamoto 9ee9f6855c feat: implementar múltiplas features
Backend:
- Password reset flow (forgot/reset endpoints, tokens table)
- Profile management (PUT /users/me, skills, experience, education)
- Tickets system (CRUD, messages, stats)
- Activity logs (list, stats)
- Document validator (CNPJ, CPF, EIN support)
- Input sanitizer (XSS prevention)
- Full-text search em vagas (plainto_tsquery)
- Filtros avançados (location, salary, workMode)
- Ordenação (date, salary, relevance)

Frontend:
- Forgot/Reset password pages
- Candidate profile edit page
- Sanitize utilities (sanitize.ts)

Backoffice:
- TicketsModule proxy
- ActivityLogsModule proxy
- Dockerfile otimizado (multi-stage, non-root, healthcheck)

Migrations:
- 013: Profile fields to users
- 014: Password reset tokens
- 015: Tickets table
- 016: Activity logs table
2025-12-27 11:19:47 -03:00

189 lines
5.8 KiB
Go

package utils
import (
"regexp"
"strings"
)
// DocumentValidator provides flexible document validation for global use
type DocumentValidator struct {
// Country code to use for validation (empty = accept all formats)
CountryCode string
}
// NewDocumentValidator creates a new validator
func NewDocumentValidator(countryCode string) *DocumentValidator {
return &DocumentValidator{CountryCode: strings.ToUpper(countryCode)}
}
// ValidationResult represents the result of document validation
type ValidationResult struct {
Valid bool
Message string
Clean string // Cleaned document number
}
// ValidateDocument validates a document based on country
// For a global portal, this supports multiple formats
func (v *DocumentValidator) ValidateDocument(doc string, docType string) ValidationResult {
// Remove all non-alphanumeric characters for cleaning
clean := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(doc, "")
if clean == "" {
return ValidationResult{Valid: true, Message: "Documento opcional não fornecido", Clean: ""}
}
switch v.CountryCode {
case "BR":
return v.validateBrazil(clean, docType)
case "JP":
return v.validateJapan(clean, docType)
case "US":
return v.validateUSA(clean, docType)
default:
// For global/unknown countries, accept any alphanumeric
if len(clean) < 5 || len(clean) > 30 {
return ValidationResult{Valid: false, Message: "Documento deve ter entre 5 e 30 caracteres", Clean: clean}
}
return ValidationResult{Valid: true, Message: "Documento aceito", Clean: clean}
}
}
// validateBrazil validates Brazilian documents (CNPJ/CPF)
func (v *DocumentValidator) validateBrazil(doc string, docType string) ValidationResult {
switch strings.ToUpper(docType) {
case "CNPJ":
if len(doc) != 14 {
return ValidationResult{Valid: false, Message: "CNPJ deve ter 14 dígitos", Clean: doc}
}
if !validateCNPJ(doc) {
return ValidationResult{Valid: false, Message: "CNPJ inválido", Clean: doc}
}
return ValidationResult{Valid: true, Message: "CNPJ válido", Clean: doc}
case "CPF":
if len(doc) != 11 {
return ValidationResult{Valid: false, Message: "CPF deve ter 11 dígitos", Clean: doc}
}
if !validateCPF(doc) {
return ValidationResult{Valid: false, Message: "CPF inválido", Clean: doc}
}
return ValidationResult{Valid: true, Message: "CPF válido", Clean: doc}
default:
// Unknown Brazilian document type, accept if reasonable length
if len(doc) < 11 || len(doc) > 14 {
return ValidationResult{Valid: false, Message: "Documento brasileiro deve ter entre 11 e 14 dígitos", Clean: doc}
}
return ValidationResult{Valid: true, Message: "Documento aceito", Clean: doc}
}
}
// validateCNPJ validates Brazilian CNPJ using checksum algorithm
func validateCNPJ(cnpj string) bool {
if len(cnpj) != 14 {
return false
}
// Check for known invalid patterns
if cnpj == "00000000000000" || cnpj == "11111111111111" || cnpj == "22222222222222" ||
cnpj == "33333333333333" || cnpj == "44444444444444" || cnpj == "55555555555555" ||
cnpj == "66666666666666" || cnpj == "77777777777777" || cnpj == "88888888888888" ||
cnpj == "99999999999999" {
return false
}
// Calculate first check digit
weights1 := []int{5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2}
sum := 0
for i, w := range weights1 {
sum += int(cnpj[i]-'0') * w
}
remainder := sum % 11
checkDigit1 := 0
if remainder >= 2 {
checkDigit1 = 11 - remainder
}
if int(cnpj[12]-'0') != checkDigit1 {
return false
}
// Calculate second check digit
weights2 := []int{6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2}
sum = 0
for i, w := range weights2 {
sum += int(cnpj[i]-'0') * w
}
remainder = sum % 11
checkDigit2 := 0
if remainder >= 2 {
checkDigit2 = 11 - remainder
}
return int(cnpj[13]-'0') == checkDigit2
}
// validateCPF validates Brazilian CPF using checksum algorithm
func validateCPF(cpf string) bool {
if len(cpf) != 11 {
return false
}
// Check for known invalid patterns
if cpf == "00000000000" || cpf == "11111111111" || cpf == "22222222222" ||
cpf == "33333333333" || cpf == "44444444444" || cpf == "55555555555" ||
cpf == "66666666666" || cpf == "77777777777" || cpf == "88888888888" ||
cpf == "99999999999" {
return false
}
// Calculate first check digit
sum := 0
for i := 0; i < 9; i++ {
sum += int(cpf[i]-'0') * (10 - i)
}
remainder := sum % 11
checkDigit1 := 0
if remainder >= 2 {
checkDigit1 = 11 - remainder
}
if int(cpf[9]-'0') != checkDigit1 {
return false
}
// Calculate second check digit
sum = 0
for i := 0; i < 10; i++ {
sum += int(cpf[i]-'0') * (11 - i)
}
remainder = sum % 11
checkDigit2 := 0
if remainder >= 2 {
checkDigit2 = 11 - remainder
}
return int(cpf[10]-'0') == checkDigit2
}
// validateJapan validates Japanese corporate numbers
func (v *DocumentValidator) validateJapan(doc string, docType string) ValidationResult {
// Japanese Corporate Number (法人番号) is 13 digits
if len(doc) == 13 {
return ValidationResult{Valid: true, Message: "法人番号 válido", Clean: doc}
}
// Accept other formats loosely
if len(doc) >= 5 && len(doc) <= 20 {
return ValidationResult{Valid: true, Message: "Documento aceito", Clean: doc}
}
return ValidationResult{Valid: false, Message: "Documento japonês inválido", Clean: doc}
}
// validateUSA validates US documents (EIN)
func (v *DocumentValidator) validateUSA(doc string, docType string) ValidationResult {
// EIN is 9 digits
if strings.ToUpper(docType) == "EIN" {
if len(doc) != 9 {
return ValidationResult{Valid: false, Message: "EIN must be 9 digits", Clean: doc}
}
return ValidationResult{Valid: true, Message: "EIN válido", Clean: doc}
}
// Accept other formats loosely
if len(doc) >= 5 && len(doc) <= 20 {
return ValidationResult{Valid: true, Message: "Document accepted", Clean: doc}
}
return ValidationResult{Valid: false, Message: "Invalid US document", Clean: doc}
}