mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
first test
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GetEncryptionKey returns the encryption key from environment
|
||||
func GetEncryptionKey() ([]byte, error) {
|
||||
key := os.Getenv("ENCRYPTION_KEY")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ENCRYPTION_KEY environment variable not set")
|
||||
}
|
||||
|
||||
// Hash the key to ensure it's exactly 32 bytes for AES-256
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext using AES-GCM
|
||||
func Encrypt(plaintext string) (string, error) {
|
||||
key, err := GetEncryptionKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts ciphertext using AES-GCM
|
||||
func Decrypt(ciphertext string) (string, error) {
|
||||
key, err := GetEncryptionKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// EncryptFile encrypts file content and returns the encrypted data
|
||||
func EncryptFile(content []byte) ([]byte, error) {
|
||||
key, err := GetEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepend nonce to encrypted content
|
||||
encrypted := gcm.Seal(nonce, nonce, content, nil)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// DecryptFile decrypts file content
|
||||
func DecryptFile(encryptedContent []byte) ([]byte, error) {
|
||||
key, err := GetEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encryptedContent) < nonceSize {
|
||||
return nil, fmt.Errorf("encrypted content too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := encryptedContent[:nonceSize], encryptedContent[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// IsEncrypted checks if content appears to be encrypted (base64 encoded)
|
||||
func IsEncrypted(content string) bool {
|
||||
// Simple check: try to base64 decode and see if it looks like encrypted content
|
||||
_, err := base64.StdEncoding.DecodeString(content)
|
||||
return err == nil && len(content) > 32 // Encrypted content should be longer than 32 chars
|
||||
}
|
||||
|
||||
// GenerateEncryptionKey generates a new random encryption key
|
||||
func GenerateEncryptionKey() (string, error) {
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// APIResponse represents a standard API response
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
// PaginatedResponse represents a paginated API response
|
||||
type PaginatedResponse struct {
|
||||
APIResponse
|
||||
Pagination PaginationInfo `json:"pagination"`
|
||||
}
|
||||
|
||||
// PaginationInfo contains pagination metadata
|
||||
type PaginationInfo struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
// Success sends a successful response
|
||||
func Success(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := "Operation successful"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Error sends an error response
|
||||
func Error(c *gin.Context, statusCode int, err error, message ...string) {
|
||||
msg := "An error occurred"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := APIResponse{
|
||||
Success: false,
|
||||
Message: msg,
|
||||
Error: err.Error(),
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(statusCode, response)
|
||||
}
|
||||
|
||||
// ValidationError sends a validation error response
|
||||
func ValidationError(c *gin.Context, errors interface{}) {
|
||||
response := APIResponse{
|
||||
Success: false,
|
||||
Message: "Validation failed",
|
||||
Error: "Invalid input data",
|
||||
Data: errors,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
}
|
||||
|
||||
// Paginated sends a paginated response
|
||||
func Paginated(c *gin.Context, data interface{}, pagination PaginationInfo, message ...string) {
|
||||
msg := "Data retrieved successfully"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := PaginatedResponse{
|
||||
APIResponse: APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
},
|
||||
Pagination: pagination,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CalculatePagination calculates pagination information
|
||||
func CalculatePagination(page, perPage int, total int64) PaginationInfo {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 {
|
||||
perPage = 10
|
||||
}
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(perPage) - 1) / int64(perPage))
|
||||
hasNext := page < totalPages
|
||||
hasPrev := page > 1
|
||||
|
||||
return PaginationInfo{
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
HasNext: hasNext,
|
||||
HasPrev: hasPrev,
|
||||
}
|
||||
}
|
||||
|
||||
// Created sends a created response
|
||||
func Created(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := "Resource created successfully"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// Updated sends an updated response
|
||||
func Updated(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := "Resource updated successfully"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Deleted sends a deleted response
|
||||
func Deleted(c *gin.Context, message ...string) {
|
||||
msg := "Resource deleted successfully"
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
response := APIResponse{
|
||||
Success: true,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
RequestID: c.GetString("RequestID"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateSecureSecret generates a cryptographically secure random secret
|
||||
func GenerateSecureSecret(byteLength int) (string, error) {
|
||||
bytes := make([]byte, byteLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// GenerateSecureKey generates a hex-encoded encryption key
|
||||
func GenerateSecureKey(bitLength int) (string, error) {
|
||||
byteLength := bitLength / 8
|
||||
bytes := make([]byte, byteLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// GetOrCreateJWTSecret retrieves JWT secret from file or generates a new one
|
||||
func GetOrCreateJWTSecret() (string, error) {
|
||||
secretFile := "jwt_secret.key"
|
||||
|
||||
// Try to read existing secret
|
||||
if secret, err := readSecretFromFile(secretFile); err == nil {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// Generate new secret
|
||||
secret, err := GenerateSecureSecret(32) // 32 bytes = 256 bits
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate JWT secret: %w", err)
|
||||
}
|
||||
|
||||
// Save to file
|
||||
if err := saveSecretToFile(secretFile, secret); err != nil {
|
||||
return "", fmt.Errorf("failed to save JWT secret: %w", err)
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// GetOrCreateEncryptionKey retrieves encryption key from file or generates a new one
|
||||
func GetOrCreateEncryptionKey() (string, error) {
|
||||
keyFile := "encryption.key"
|
||||
|
||||
// Try to read existing key
|
||||
if key, err := readSecretFromFile(keyFile); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Generate new key
|
||||
key, err := GenerateSecureKey(256) // 256 bits
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate encryption key: %w", err)
|
||||
}
|
||||
|
||||
// Save to file
|
||||
if err := saveSecretToFile(keyFile, key); err != nil {
|
||||
return "", fmt.Errorf("failed to save encryption key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// readSecretFromFile reads a secret from a file
|
||||
func readSecretFromFile(filename string) (string, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
secret := string(data)
|
||||
if secret == "" {
|
||||
return "", fmt.Errorf("empty secret file")
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// saveSecretToFile saves a secret to a file with secure permissions
|
||||
func saveSecretToFile(filename, secret string) error {
|
||||
// Create the file with restricted permissions (only readable by owner)
|
||||
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSecretStrength checks if a secret meets minimum security requirements
|
||||
func ValidateSecretStrength(secret string, minLength int) error {
|
||||
if len(secret) < minLength {
|
||||
return fmt.Errorf("secret too short: minimum %d characters required", minLength)
|
||||
}
|
||||
|
||||
// Check entropy (basic check)
|
||||
entropy := calculateEntropy(secret)
|
||||
if entropy < 3.0 { // Minimum entropy threshold
|
||||
return fmt.Errorf("secret has low entropy: %.2f (minimum 3.0)", entropy)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateEntropy calculates the Shannon entropy of a string
|
||||
func calculateEntropy(s string) float64 {
|
||||
if len(s) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Count character frequencies
|
||||
freq := make(map[rune]int)
|
||||
for _, char := range s {
|
||||
freq[char]++
|
||||
}
|
||||
|
||||
// Calculate entropy
|
||||
entropy := 0.0
|
||||
length := float64(len(s))
|
||||
|
||||
for _, count := range freq {
|
||||
if count > 0 {
|
||||
p := float64(count) / length
|
||||
entropy -= p * log2(p)
|
||||
}
|
||||
}
|
||||
|
||||
return entropy
|
||||
}
|
||||
|
||||
// log2 calculates base-2 logarithm
|
||||
func log2(x float64) float64 {
|
||||
const ln2 = 0.6931471805599453 // ln(2)
|
||||
return 1.0 / ln2 * logNatural(x)
|
||||
}
|
||||
|
||||
// logNatural calculates natural logarithm using approximation
|
||||
func logNatural(x float64) float64 {
|
||||
if x <= 0 {
|
||||
return 0
|
||||
}
|
||||
if x == 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Simple approximation for ln(x)
|
||||
// For production, use math.Log from the standard library
|
||||
n := 0.0
|
||||
for x > 1.0 {
|
||||
x /= 2.718281828459045 // e
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// RotateSecret generates a new secret and updates the file
|
||||
func RotateSecret(filename string) (string, error) {
|
||||
// Generate new secret
|
||||
var newSecret string
|
||||
var err error
|
||||
|
||||
if filename == "jwt_secret.key" {
|
||||
newSecret, err = GenerateSecureSecret(32)
|
||||
} else if filename == "encryption.key" {
|
||||
newSecret, err = GenerateSecureKey(256)
|
||||
} else {
|
||||
return "", fmt.Errorf("unknown secret file type: %s", filename)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate new secret: %w", err)
|
||||
}
|
||||
|
||||
// Backup old secret if it exists
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
backupFile := fmt.Sprintf("%s.backup.%d", filename, time.Now().Unix())
|
||||
if err := os.Rename(filename, backupFile); err != nil {
|
||||
return "", fmt.Errorf("failed to backup old secret: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save new secret
|
||||
if err := saveSecretToFile(filename, newSecret); err != nil {
|
||||
return "", fmt.Errorf("failed to save new secret: %w", err)
|
||||
}
|
||||
|
||||
return newSecret, nil
|
||||
}
|
||||
|
||||
// GetSecretFilePath returns the full path to a secret file
|
||||
func GetSecretFilePath(filename string) string {
|
||||
// Store secrets in a secure directory
|
||||
secretDir := os.Getenv("SECRET_DIR")
|
||||
if secretDir == "" {
|
||||
secretDir = "./secrets"
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
||||
// Fallback to current directory
|
||||
return filename
|
||||
}
|
||||
|
||||
return filepath.Join(secretDir, filename)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Validator provides common validation functions
|
||||
type Validator struct {
|
||||
errors map[string]string
|
||||
}
|
||||
|
||||
// NewValidator creates a new validator instance
|
||||
func NewValidator() *Validator {
|
||||
return &Validator{
|
||||
errors: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Required checks if a field is not empty
|
||||
func (v *Validator) Required(field, value string) *Validator {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
v.errors[field] = fmt.Sprintf("%s is required", field)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// MinLength checks if a field meets minimum length
|
||||
func (v *Validator) MinLength(field, value string, min int) *Validator {
|
||||
if utf8.RuneCountInString(value) < min {
|
||||
v.errors[field] = fmt.Sprintf("%s must be at least %d characters", field, min)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// MaxLength checks if a field exceeds maximum length
|
||||
func (v *Validator) MaxLength(field, value string, max int) *Validator {
|
||||
if utf8.RuneCountInString(value) > max {
|
||||
v.errors[field] = fmt.Sprintf("%s must be at most %d characters", field, max)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Email checks if a field is a valid email
|
||||
func (v *Validator) Email(field, value string) *Validator {
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(value) {
|
||||
v.errors[field] = fmt.Sprintf("%s must be a valid email address", field)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// URL checks if a field is a valid URL
|
||||
func (v *Validator) URL(field, value string) *Validator {
|
||||
urlRegex := regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`)
|
||||
if !urlRegex.MatchString(value) {
|
||||
v.errors[field] = fmt.Sprintf("%s must be a valid URL", field)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Match checks if a field matches a regex pattern
|
||||
func (v *Validator) Match(field, value, pattern, message string) *Validator {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
if !regex.MatchString(value) {
|
||||
v.errors[field] = message
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// In checks if a field value is in the allowed values
|
||||
func (v *Validator) In(field, value string, allowed []string) *Validator {
|
||||
for _, allowedValue := range allowed {
|
||||
if value == allowedValue {
|
||||
return v
|
||||
}
|
||||
}
|
||||
v.errors[field] = fmt.Sprintf("%s must be one of: %s", field, strings.Join(allowed, ", "))
|
||||
return v
|
||||
}
|
||||
|
||||
// HasErrors returns true if there are validation errors
|
||||
func (v *Validator) HasErrors() bool {
|
||||
return len(v.errors) > 0
|
||||
}
|
||||
|
||||
// GetErrors returns all validation errors
|
||||
func (v *Validator) GetErrors() map[string]string {
|
||||
return v.errors
|
||||
}
|
||||
|
||||
// GetError returns a specific field error
|
||||
func (v *Validator) GetError(field string) string {
|
||||
return v.errors[field]
|
||||
}
|
||||
|
||||
// Clear clears all validation errors
|
||||
func (v *Validator) Clear() *Validator {
|
||||
v.errors = make(map[string]string)
|
||||
return v
|
||||
}
|
||||
|
||||
// ValidatePassword checks password strength
|
||||
func (v *Validator) ValidatePassword(field, password string) *Validator {
|
||||
if len(password) < 8 {
|
||||
v.errors[field] = "Password must be at least 8 characters long"
|
||||
return v
|
||||
}
|
||||
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
||||
hasSpecial := regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password)
|
||||
|
||||
errorCount := 0
|
||||
if !hasUpper {
|
||||
errorCount++
|
||||
}
|
||||
if !hasLower {
|
||||
errorCount++
|
||||
}
|
||||
if !hasNumber {
|
||||
errorCount++
|
||||
}
|
||||
if !hasSpecial {
|
||||
errorCount++
|
||||
}
|
||||
|
||||
if errorCount > 1 {
|
||||
v.errors[field] = "Password must contain at least 3 of: uppercase, lowercase, number, special character"
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user