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 }