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
189 lines
5.8 KiB
Go
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}
|
|
}
|