114 lines
2.6 KiB
Go
114 lines
2.6 KiB
Go
package domain
|
|
|
|
import (
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// UserID is a strongly-typed identifier for users.
|
|
type UserID string
|
|
|
|
// String returns the string representation of the ID.
|
|
func (id UserID) String() string {
|
|
return string(id)
|
|
}
|
|
|
|
// IsZero returns true if the ID is empty.
|
|
func (id UserID) IsZero() bool {
|
|
return id == ""
|
|
}
|
|
|
|
// User email constraints.
|
|
const (
|
|
MinEmailLen = 3
|
|
MaxEmailLen = 254
|
|
MinPasswordLen = 8
|
|
MaxPasswordLen = 72 // bcrypt limit
|
|
)
|
|
|
|
// User represents a user domain entity.
|
|
// This is a pure domain model with no external dependencies.
|
|
type User struct {
|
|
ID UserID
|
|
Email string
|
|
PasswordHash string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// NewUser creates a new User with validation and password hashing.
|
|
// Returns ErrInvalidEmail if the email is invalid.
|
|
// Returns ErrInvalidPassword if the password is invalid.
|
|
func NewUser(id UserID, email, password string) (*User, error) {
|
|
if err := validateEmail(email); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validatePassword(password); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
passwordHash, err := hashPassword(password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
return &User{
|
|
ID: id,
|
|
Email: email,
|
|
PasswordHash: passwordHash,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
// CheckPassword verifies if the provided password matches the stored hash.
|
|
func (u *User) CheckPassword(password string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
|
return err == nil
|
|
}
|
|
|
|
// validateEmail validates a user email address.
|
|
func validateEmail(email string) error {
|
|
length := utf8.RuneCountInString(email)
|
|
if length < MinEmailLen || length > MaxEmailLen {
|
|
return ErrInvalidEmail
|
|
}
|
|
// Basic email validation - contains @ with text before and after
|
|
hasAt := false
|
|
atPos := -1
|
|
for i, r := range email {
|
|
if r == '@' {
|
|
if hasAt {
|
|
return ErrInvalidEmail // Multiple @
|
|
}
|
|
hasAt = true
|
|
atPos = i
|
|
}
|
|
}
|
|
if !hasAt || atPos == 0 || atPos == len(email)-1 {
|
|
return ErrInvalidEmail
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validatePassword validates a password.
|
|
func validatePassword(password string) error {
|
|
length := utf8.RuneCountInString(password)
|
|
if length < MinPasswordLen || length > MaxPasswordLen {
|
|
return ErrInvalidPassword
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hashPassword creates a bcrypt hash of the password.
|
|
func hashPassword(password string) (string, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(hash), nil
|
|
}
|