2025-12-03 19:29:37 +00:00
|
|
|
package utils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/big"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"unicode"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// GenerateID génère un ID unique
|
|
|
|
|
func GenerateID() string {
|
|
|
|
|
b := make([]byte, 16)
|
|
|
|
|
rand.Read(b)
|
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateUUID génère un UUID v4
|
|
|
|
|
func GenerateUUID() string {
|
|
|
|
|
b := make([]byte, 16)
|
|
|
|
|
rand.Read(b)
|
|
|
|
|
|
|
|
|
|
// Version 4
|
|
|
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
|
|
|
// Variant bits
|
|
|
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateRandomString génère une chaîne aléatoire de longueur donnée
|
|
|
|
|
func GenerateRandomString(length int) string {
|
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
|
b := make([]byte, length)
|
|
|
|
|
for i := range b {
|
|
|
|
|
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
|
|
|
b[i] = charset[num.Int64()]
|
|
|
|
|
}
|
|
|
|
|
return string(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateRandomBytes génère des bytes aléatoires
|
|
|
|
|
func GenerateRandomBytes(length int) ([]byte, error) {
|
|
|
|
|
b := make([]byte, length)
|
|
|
|
|
_, err := rand.Read(b)
|
|
|
|
|
return b, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HashPassword hash un mot de passe avec bcrypt
|
|
|
|
|
func HashPassword(password string) (string, error) {
|
2026-03-13 23:44:46 +00:00
|
|
|
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), 12) // SECURITY(REM-030): Aligned with password_service.go bcryptCost=12
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to hash password: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return string(hashedBytes), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VerifyPassword vérifie un mot de passe contre son hash
|
|
|
|
|
func VerifyPassword(hashedPassword, password string) error {
|
|
|
|
|
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckPasswordHash est un alias pour VerifyPassword (compatibilité)
|
|
|
|
|
func CheckPasswordHash(password, hashedPassword string) error {
|
|
|
|
|
return VerifyPassword(hashedPassword, password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HashSHA256 hash une chaîne avec SHA256
|
|
|
|
|
func HashSHA256(input string) string {
|
|
|
|
|
hash := sha256.Sum256([]byte(input))
|
|
|
|
|
return hex.EncodeToString(hash[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ValidateEmail valide le format d'un email
|
|
|
|
|
func ValidateEmail(email string) bool {
|
|
|
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
|
return emailRegex.MatchString(email)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ValidatePasswordStrength is now in password_validator.go
|
|
|
|
|
// T0197: Moved to password_validator.go for better organization
|
|
|
|
|
|
|
|
|
|
// ValidateUsername valide le format d'un nom d'utilisateur
|
|
|
|
|
func ValidateUsername(username string) (bool, []string) {
|
|
|
|
|
var errors []string
|
|
|
|
|
|
|
|
|
|
if len(username) < 3 {
|
|
|
|
|
errors = append(errors, "Username must be at least 3 characters long")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(username) > 30 {
|
|
|
|
|
errors = append(errors, "Username must be less than 30 characters")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
|
|
|
if !usernameRegex.MatchString(username) {
|
|
|
|
|
errors = append(errors, "Username can only contain letters, numbers, underscores, and hyphens")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SanitizeString nettoie une chaîne de caractères
|
|
|
|
|
func SanitizeString(input string) string {
|
|
|
|
|
// Supprimer les caractères de contrôle
|
|
|
|
|
cleaned := strings.Map(func(r rune) rune {
|
|
|
|
|
if r < 32 || r == 127 {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
return r
|
|
|
|
|
}, input)
|
|
|
|
|
|
|
|
|
|
// Supprimer les espaces en début et fin
|
|
|
|
|
cleaned = strings.TrimSpace(cleaned)
|
|
|
|
|
|
|
|
|
|
// Limiter la longueur
|
|
|
|
|
if len(cleaned) > 1000 {
|
|
|
|
|
cleaned = cleaned[:1000]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cleaned
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 11:15:25 +00:00
|
|
|
// SanitizeHTML is now in sanitizer.go with enhanced functionality
|
|
|
|
|
// BE-SEC-009: Moved to sanitizer.go for better organization and security
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
// TruncateString tronque une chaîne à la longueur spécifiée
|
|
|
|
|
func TruncateString(input string, maxLength int) string {
|
|
|
|
|
if len(input) <= maxLength {
|
|
|
|
|
return input
|
|
|
|
|
}
|
|
|
|
|
return input[:maxLength] + "..."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ContainsString vérifie si une chaîne contient une sous-chaîne (insensible à la casse)
|
|
|
|
|
func ContainsString(s, substr string) bool {
|
|
|
|
|
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsEmpty vérifie si une chaîne est vide ou ne contient que des espaces
|
|
|
|
|
func IsEmpty(s string) bool {
|
|
|
|
|
return strings.TrimSpace(s) == ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsNotEmpty vérifie si une chaîne n'est pas vide
|
|
|
|
|
func IsNotEmpty(s string) bool {
|
|
|
|
|
return !IsEmpty(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormatDuration formate une durée en chaîne lisible
|
|
|
|
|
func FormatDuration(d time.Duration) string {
|
|
|
|
|
if d < time.Minute {
|
|
|
|
|
return fmt.Sprintf("%.0fs", d.Seconds())
|
|
|
|
|
}
|
|
|
|
|
if d < time.Hour {
|
|
|
|
|
return fmt.Sprintf("%.0fm", d.Minutes())
|
|
|
|
|
}
|
|
|
|
|
if d < 24*time.Hour {
|
|
|
|
|
return fmt.Sprintf("%.1fh", d.Hours())
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%.1fd", d.Hours()/24)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormatFileSize formate une taille de fichier en chaîne lisible
|
|
|
|
|
func FormatFileSize(bytes int64) string {
|
|
|
|
|
const unit = 1024
|
|
|
|
|
if bytes < unit {
|
|
|
|
|
return fmt.Sprintf("%d B", bytes)
|
|
|
|
|
}
|
|
|
|
|
div, exp := int64(unit), 0
|
|
|
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
|
|
|
div *= unit
|
|
|
|
|
exp++
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormatNumber formate un nombre avec des séparateurs de milliers
|
|
|
|
|
func FormatNumber(n int64) string {
|
|
|
|
|
str := fmt.Sprintf("%d", n)
|
|
|
|
|
if len(str) <= 3 {
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result strings.Builder
|
|
|
|
|
for i, char := range str {
|
|
|
|
|
if i > 0 && (len(str)-i)%3 == 0 {
|
|
|
|
|
result.WriteString(",")
|
|
|
|
|
}
|
|
|
|
|
result.WriteRune(char)
|
|
|
|
|
}
|
|
|
|
|
return result.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseDuration parse une durée depuis une chaîne
|
|
|
|
|
func ParseDuration(s string) (time.Duration, error) {
|
|
|
|
|
// Supprimer les espaces
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
|
|
|
|
|
// Ajouter 's' si pas d'unité spécifiée
|
|
|
|
|
if !strings.ContainsAny(s, "smhd") {
|
|
|
|
|
s += "s"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return time.ParseDuration(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsValidURL vérifie si une chaîne est une URL valide
|
|
|
|
|
func IsValidURL(url string) bool {
|
|
|
|
|
urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$`)
|
|
|
|
|
return urlRegex.MatchString(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExtractDomain extrait le domaine d'une URL
|
|
|
|
|
func ExtractDomain(url string) string {
|
|
|
|
|
// Supprimer le protocole
|
|
|
|
|
if strings.HasPrefix(url, "http://") {
|
|
|
|
|
url = url[7:]
|
|
|
|
|
} else if strings.HasPrefix(url, "https://") {
|
|
|
|
|
url = url[8:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Supprimer le chemin
|
|
|
|
|
if idx := strings.Index(url, "/"); idx != -1 {
|
|
|
|
|
url = url[:idx]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateSlug génère un slug à partir d'une chaîne
|
|
|
|
|
func GenerateSlug(input string) string {
|
|
|
|
|
// Convertir en minuscules
|
|
|
|
|
slug := strings.ToLower(input)
|
|
|
|
|
|
|
|
|
|
// Remplacer les espaces par des tirets
|
|
|
|
|
slug = strings.ReplaceAll(slug, " ", "-")
|
|
|
|
|
|
|
|
|
|
// Supprimer les caractères non alphanumériques sauf les tirets
|
|
|
|
|
slug = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(slug, "")
|
|
|
|
|
|
|
|
|
|
// Supprimer les tirets multiples
|
|
|
|
|
slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-")
|
|
|
|
|
|
|
|
|
|
// Supprimer les tirets en début et fin
|
|
|
|
|
slug = strings.Trim(slug, "-")
|
|
|
|
|
|
|
|
|
|
return slug
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ContainsOnlyDigits vérifie si une chaîne ne contient que des chiffres
|
|
|
|
|
func ContainsOnlyDigits(s string) bool {
|
|
|
|
|
for _, char := range s {
|
|
|
|
|
if !unicode.IsDigit(char) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ContainsOnlyLetters vérifie si une chaîne ne contient que des lettres
|
|
|
|
|
func ContainsOnlyLetters(s string) bool {
|
|
|
|
|
for _, char := range s {
|
|
|
|
|
if !unicode.IsLetter(char) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ContainsOnlyAlphanumeric vérifie si une chaîne ne contient que des caractères alphanumériques
|
|
|
|
|
func ContainsOnlyAlphanumeric(s string) bool {
|
|
|
|
|
for _, char := range s {
|
|
|
|
|
if !unicode.IsLetter(char) && !unicode.IsDigit(char) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveDuplicates supprime les doublons d'une slice de chaînes
|
|
|
|
|
func RemoveDuplicates(slice []string) []string {
|
|
|
|
|
keys := make(map[string]bool)
|
|
|
|
|
var result []string
|
|
|
|
|
|
|
|
|
|
for _, item := range slice {
|
|
|
|
|
if !keys[item] {
|
|
|
|
|
keys[item] = true
|
|
|
|
|
result = append(result, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Contains vérifie si une slice contient un élément
|
|
|
|
|
func Contains(slice []string, item string) bool {
|
|
|
|
|
for _, s := range slice {
|
|
|
|
|
if s == item {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IndexOf retourne l'index d'un élément dans une slice
|
|
|
|
|
func IndexOf(slice []string, item string) int {
|
|
|
|
|
for i, s := range slice {
|
|
|
|
|
if s == item {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reverse inverse l'ordre d'une slice
|
|
|
|
|
func Reverse(slice []string) []string {
|
|
|
|
|
for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
|
slice[i], slice[j] = slice[j], slice[i]
|
|
|
|
|
}
|
|
|
|
|
return slice
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Chunk divise une slice en chunks de taille donnée
|
|
|
|
|
func Chunk(slice []string, chunkSize int) [][]string {
|
|
|
|
|
var chunks [][]string
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(slice); i += chunkSize {
|
|
|
|
|
end := i + chunkSize
|
|
|
|
|
if end > len(slice) {
|
|
|
|
|
end = len(slice)
|
|
|
|
|
}
|
|
|
|
|
chunks = append(chunks, slice[i:end])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chunks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter filtre une slice selon une condition
|
|
|
|
|
func Filter(slice []string, predicate func(string) bool) []string {
|
|
|
|
|
var result []string
|
|
|
|
|
|
|
|
|
|
for _, item := range slice {
|
|
|
|
|
if predicate(item) {
|
|
|
|
|
result = append(result, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map applique une fonction à chaque élément d'une slice
|
|
|
|
|
func Map(slice []string, mapper func(string) string) []string {
|
|
|
|
|
var result []string
|
|
|
|
|
|
|
|
|
|
for _, item := range slice {
|
|
|
|
|
result = append(result, mapper(item))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reduce réduit une slice à une seule valeur
|
|
|
|
|
func Reduce(slice []string, initial string, reducer func(string, string) string) string {
|
|
|
|
|
result := initial
|
|
|
|
|
|
|
|
|
|
for _, item := range slice {
|
|
|
|
|
result = reducer(result, item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|