add service
All checks were successful
Deploy Smart Search Backend / deploy (push) Successful in 1m47s

This commit is contained in:
vallyenfail
2026-01-20 19:02:06 +03:00
parent f8db0fd9e6
commit 8b9554720d
15 changed files with 2109 additions and 38 deletions

View File

@@ -6,11 +6,12 @@ import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
)
type Crypto struct {
@@ -31,10 +32,19 @@ func (c *Crypto) EmailHash(email string) string {
return hex.EncodeToString(h.Sum(nil))
}
const bcryptCost = 12
func PasswordHash(password string) string {
h := sha512.New()
h.Write([]byte(password))
return hex.EncodeToString(h.Sum(nil))
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return ""
}
return string(hash)
}
func PasswordVerify(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func (c *Crypto) getKey() []byte {

View File

@@ -15,6 +15,13 @@ const (
UnsupportedFileFormat = "UNSUPPORTED_FILE_FORMAT"
FileProcessingError = "FILE_PROCESSING_ERROR"
ValidationInvalidEmail = "VALIDATION_INVALID_EMAIL"
ValidationInvalidPassword = "VALIDATION_INVALID_PASSWORD"
ValidationInvalidPhone = "VALIDATION_INVALID_PHONE"
ValidationInvalidName = "VALIDATION_INVALID_NAME"
ValidationFileTooLarge = "VALIDATION_FILE_TOO_LARGE"
ValidationRequestTooLong = "VALIDATION_REQUEST_TOO_LONG"
DatabaseError = "DATABASE_ERROR"
EncryptionError = "ENCRYPTION_ERROR"
AIAPIError = "AI_API_ERROR"

View File

@@ -0,0 +1,156 @@
package validation
import (
"net/mail"
"regexp"
"strings"
"unicode"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
)
const (
MinPasswordLength = 8
MaxPasswordLength = 128
MaxEmailLength = 254
MaxNameLength = 100
MaxPhoneLength = 20
MaxRequestTxtLen = 50000
MaxFileSizeBytes = 10 * 1024 * 1024
)
var (
phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{6,14}$`)
)
func ValidateEmail(email string) error {
if email == "" {
return errors.NewBusinessError(errors.ValidationInvalidEmail, "email is required")
}
if len(email) > MaxEmailLength {
return errors.NewBusinessError(errors.ValidationInvalidEmail, "email is too long")
}
_, err := mail.ParseAddress(email)
if err != nil {
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
}
parts := strings.Split(email, "@")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
}
if strings.Contains(parts[1], "..") {
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
}
return nil
}
func ValidatePassword(password string) error {
if password == "" {
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password is required")
}
if len(password) < MinPasswordLength {
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password must be at least 8 characters")
}
if len(password) > MaxPasswordLength {
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password is too long")
}
var hasUpper, hasLower, hasDigit bool
for _, c := range password {
switch {
case unicode.IsUpper(c):
hasUpper = true
case unicode.IsLower(c):
hasLower = true
case unicode.IsDigit(c):
hasDigit = true
}
}
if !hasUpper || !hasLower || !hasDigit {
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password must contain uppercase, lowercase and digit")
}
return nil
}
func ValidatePhone(phone string) error {
if phone == "" {
return errors.NewBusinessError(errors.ValidationInvalidPhone, "phone is required")
}
if len(phone) > MaxPhoneLength {
return errors.NewBusinessError(errors.ValidationInvalidPhone, "phone is too long")
}
cleaned := strings.ReplaceAll(phone, " ", "")
cleaned = strings.ReplaceAll(cleaned, "-", "")
cleaned = strings.ReplaceAll(cleaned, "(", "")
cleaned = strings.ReplaceAll(cleaned, ")", "")
if !phoneRegex.MatchString(cleaned) {
return errors.NewBusinessError(errors.ValidationInvalidPhone, "invalid phone format")
}
return nil
}
func ValidateName(name string) error {
if name == "" {
return errors.NewBusinessError(errors.ValidationInvalidName, "name is required")
}
if len(name) > MaxNameLength {
return errors.NewBusinessError(errors.ValidationInvalidName, "name is too long")
}
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return errors.NewBusinessError(errors.ValidationInvalidName, "name cannot be only whitespace")
}
return nil
}
func ValidateRequestTxt(txt string) error {
if len(txt) > MaxRequestTxtLen {
return errors.NewBusinessError(errors.ValidationRequestTooLong, "request text exceeds 50000 characters limit")
}
return nil
}
func ValidateFileSize(size int) error {
if size > MaxFileSizeBytes {
return errors.NewBusinessError(errors.ValidationFileTooLarge, "file size exceeds 10MB limit")
}
return nil
}
func ValidateRegistration(email, password, name, phone string) error {
if err := ValidateEmail(email); err != nil {
return err
}
if err := ValidatePassword(password); err != nil {
return err
}
if err := ValidateName(name); err != nil {
return err
}
if err := ValidatePhone(phone); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,222 @@
package validation
import (
"strings"
"testing"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
wantCode string
}{
{"valid email", "test@example.com", false, ""},
{"valid with subdomain", "test@sub.example.com", false, ""},
{"valid with plus", "test+tag@example.com", false, ""},
{"valid with dots", "test.name@example.com", false, ""},
{"empty", "", true, errors.ValidationInvalidEmail},
{"no at sign", "testexample.com", true, errors.ValidationInvalidEmail},
{"no domain", "test@", true, errors.ValidationInvalidEmail},
{"no local part", "@example.com", true, errors.ValidationInvalidEmail},
{"double dots in domain", "test@example..com", true, errors.ValidationInvalidEmail},
{"too long", strings.Repeat("a", 255) + "@example.com", true, errors.ValidationInvalidEmail},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidatePassword(t *testing.T) {
tests := []struct {
name string
password string
wantErr bool
wantCode string
}{
{"valid password", "Abcd1234", false, ""},
{"valid with special chars", "Abcd1234!", false, ""},
{"empty", "", true, errors.ValidationInvalidPassword},
{"too short", "Ab1", true, errors.ValidationInvalidPassword},
{"no uppercase", "abcd1234", true, errors.ValidationInvalidPassword},
{"no lowercase", "ABCD1234", true, errors.ValidationInvalidPassword},
{"no digit", "Abcdefgh", true, errors.ValidationInvalidPassword},
{"only digits", "12345678", true, errors.ValidationInvalidPassword},
{"only lowercase", "abcdefgh", true, errors.ValidationInvalidPassword},
{"only uppercase", "ABCDEFGH", true, errors.ValidationInvalidPassword},
{"too long", strings.Repeat("Aa1", 50), true, errors.ValidationInvalidPassword},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePassword(tt.password)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidatePhone(t *testing.T) {
tests := []struct {
name string
phone string
wantErr bool
wantCode string
}{
{"valid international", "+1234567890", false, ""},
{"valid with country code", "+79123456789", false, ""},
{"valid without plus", "1234567890", false, ""},
{"empty", "", true, errors.ValidationInvalidPhone},
{"too short", "123", true, errors.ValidationInvalidPhone},
{"letters", "abcdefgh", true, errors.ValidationInvalidPhone},
{"too long", "+123456789012345678901", true, errors.ValidationInvalidPhone},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePhone(tt.phone)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateName(t *testing.T) {
tests := []struct {
name string
value string
wantErr bool
wantCode string
}{
{"valid name", "John Doe", false, ""},
{"valid cyrillic", "Иван Иванов", false, ""},
{"empty", "", true, errors.ValidationInvalidName},
{"only whitespace", " ", true, errors.ValidationInvalidName},
{"too long", strings.Repeat("a", 101), true, errors.ValidationInvalidName},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateName(tt.value)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateRequestTxt(t *testing.T) {
tests := []struct {
name string
txt string
wantErr bool
wantCode string
}{
{"valid short", "Test request", false, ""},
{"empty is valid", "", false, ""},
{"max length", strings.Repeat("a", MaxRequestTxtLen), false, ""},
{"too long", strings.Repeat("a", MaxRequestTxtLen+1), true, errors.ValidationRequestTooLong},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateRequestTxt(tt.txt)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateFileSize(t *testing.T) {
tests := []struct {
name string
size int
wantErr bool
wantCode string
}{
{"zero", 0, false, ""},
{"small file", 1024, false, ""},
{"max size", MaxFileSizeBytes, false, ""},
{"too large", MaxFileSizeBytes + 1, true, errors.ValidationFileTooLarge},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateFileSize(tt.size)
if tt.wantErr {
assert.Error(t, err)
if appErr, ok := err.(*errors.AppError); ok {
assert.Equal(t, tt.wantCode, appErr.Code)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateRegistration(t *testing.T) {
tests := []struct {
name string
email string
password string
userName string
phone string
wantErr bool
}{
{"valid", "test@example.com", "Abcd1234", "John Doe", "+1234567890", false},
{"invalid email", "invalid", "Abcd1234", "John Doe", "+1234567890", true},
{"invalid password", "test@example.com", "weak", "John Doe", "+1234567890", true},
{"invalid name", "test@example.com", "Abcd1234", "", "+1234567890", true},
{"invalid phone", "test@example.com", "Abcd1234", "John Doe", "abc", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateRegistration(tt.email, tt.password, tt.userName, tt.phone)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}