slack-auth-1770277926/services/auth-api/internal/domain/user.go
rdev-worker fd9bf961bb
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature auth-system --requirements 'User model with email/...
2026-02-05 07:59:55 +00:00

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
}