package domain import ( "fmt" "regexp" "time" "git.nakama.town/fmartingr/hako/internal/auth/store" "git.nakama.town/fmartingr/hako/internal/dependencies" "git.nakama.town/fmartingr/hako/internal/model" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) // AuthDomain handles business logic for authentication type AuthDomain struct { deps model.Dependencies } // NewAuthDomain creates a new AuthDomain func NewAuthDomain(deps model.Dependencies) model.AuthDomain { return &AuthDomain{deps: deps} } // getDeps returns the concrete dependencies implementation func (d *AuthDomain) getDeps() *dependencies.Dependencies { return d.deps.(*dependencies.Dependencies) } // Login authenticates a user and returns a JWT token func (d *AuthDomain) Login(email, password string) (*model.User, *model.Token, error) { deps := d.getDeps() // Find user by email storeUser, err := deps.UserStore.FindByEmail(email) if err != nil { return nil, nil, fmt.Errorf("invalid credentials") } // Convert to domain user user := convertUser(storeUser) // Verify password if !d.VerifyPassword(user, password) { return nil, nil, fmt.Errorf("invalid credentials") } // Get JWT service from dependencies jwtService := deps.GetJWTService() if jwtService == nil { return nil, nil, fmt.Errorf("JWT service not available") } // Generate JWT token with user's role tokenString, expiresAt, err := jwtService.GenerateTokenWithRole(user.ID, user.Email, user.Role) if err != nil { return nil, nil, fmt.Errorf("failed to generate token: %w", err) } token := &model.Token{ Token: tokenString, ExpiresAt: expiresAt, } return user, token, nil } // VerifyPassword verifies a password against a user's password hash func (d *AuthDomain) VerifyPassword(user *model.User, password string) bool { err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) return err == nil } // CreateUser creates a new user with hashed password and specified role func (d *AuthDomain) CreateUser(email, password, role string) (*model.User, error) { deps := d.getDeps() if !d.isValidEmail(email) { return nil, fmt.Errorf("invalid email format") } if len(password) < 8 { return nil, fmt.Errorf("password must be at least 8 characters") } if role == "" { role = "user" } if role != "user" && role != "admin" { return nil, fmt.Errorf("role must be 'user' or 'admin'") } // Check if user already exists _, err := deps.UserStore.FindByEmail(email) if err == nil { return nil, fmt.Errorf("user with this email already exists") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } user := model.NewUserWithRole(email, string(hashedPassword), role) storeUser := convertUserToStore(user) if err := deps.UserStore.Create(storeUser); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } return user, nil } // ListUsers returns all users func (d *AuthDomain) ListUsers() ([]*model.User, error) { deps := d.getDeps() storeUsers, err := deps.UserStore.List() if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } users := make([]*model.User, len(storeUsers)) for i, su := range storeUsers { users[i] = convertUser(su) } return users, nil } // GetUser returns a user by ID func (d *AuthDomain) GetUser(id string) (*model.User, error) { deps := d.getDeps() uid, err := uuid.Parse(id) if err != nil { return nil, fmt.Errorf("invalid user ID") } storeUser, err := deps.UserStore.FindByID(uid) if err != nil { return nil, fmt.Errorf("user not found") } return convertUser(storeUser), nil } // UpdateUser updates a user's email and role func (d *AuthDomain) UpdateUser(id, email, role string) (*model.User, error) { deps := d.getDeps() uid, err := uuid.Parse(id) if err != nil { return nil, fmt.Errorf("invalid user ID") } if !d.isValidEmail(email) { return nil, fmt.Errorf("invalid email format") } if role != "user" && role != "admin" { return nil, fmt.Errorf("role must be 'user' or 'admin'") } // Fetch existing user storeUser, err := deps.UserStore.FindByID(uid) if err != nil { return nil, fmt.Errorf("user not found") } // Check email uniqueness (allow keeping the same email) if email != storeUser.Email { existing, err := deps.UserStore.FindByEmail(email) if err == nil && existing.ID != uid { return nil, fmt.Errorf("email already in use") } } storeUser.Email = email storeUser.Role = role storeUser.UpdatedAt = time.Now() if err := deps.UserStore.Update(storeUser); err != nil { return nil, fmt.Errorf("failed to update user: %w", err) } return convertUser(storeUser), nil } // UpdateUserPassword changes a user's password func (d *AuthDomain) UpdateUserPassword(id, password string) error { deps := d.getDeps() uid, err := uuid.Parse(id) if err != nil { return fmt.Errorf("invalid user ID") } if len(password) < 8 { return fmt.Errorf("password must be at least 8 characters") } storeUser, err := deps.UserStore.FindByID(uid) if err != nil { return fmt.Errorf("user not found") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("failed to hash password: %w", err) } storeUser.PasswordHash = string(hashedPassword) storeUser.UpdatedAt = time.Now() if err := deps.UserStore.Update(storeUser); err != nil { return fmt.Errorf("failed to update password: %w", err) } return nil } // DeleteUser deletes a user by ID func (d *AuthDomain) DeleteUser(id string) error { deps := d.getDeps() uid, err := uuid.Parse(id) if err != nil { return fmt.Errorf("invalid user ID") } if err := deps.UserStore.Delete(uid); err != nil { return fmt.Errorf("failed to delete user: %w", err) } return nil } // isValidEmail validates email format func (d *AuthDomain) isValidEmail(email string) bool { return emailRegex.MatchString(email) } // Helper conversion functions func convertUser(su *store.User) *model.User { if su == nil { return nil } // Default role to "user" if not set role := su.Role if role == "" { role = "user" } return &model.User{ ID: su.ID, Email: su.Email, PasswordHash: su.PasswordHash, Role: role, CreatedAt: su.CreatedAt, UpdatedAt: su.UpdatedAt, } } func convertUserToStore(du *model.User) *store.User { if du == nil { return nil } return &store.User{ ID: du.ID, Email: du.Email, PasswordHash: du.PasswordHash, Role: du.Role, CreatedAt: du.CreatedAt, UpdatedAt: du.UpdatedAt, } }