Files
rideaware-api/internal/user/service.go
2025-11-22 23:54:49 -06:00

172 lines
3.5 KiB
Go

package user
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"os"
"regexp"
"time"
"rideaware/internal/config"
"rideaware/internal/email"
"rideaware/pkg/database"
)
type Service struct {
repo *Repository
email *email.Service
}
func NewService() *Service {
return &Service{
repo: NewRepository(),
email: email.NewService(),
}
}
func (s *Service) CreateUser(username, password, email, firstName, lastName string) (*User, error) {
if username == "" || password == "" {
return nil, errors.New("username and password are required")
}
if email != "" {
if !isValidEmail(email) {
return nil, errors.New("invalid email format")
}
}
exists, err := s.repo.UserExists(username, email)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("username or email already exists")
}
user := &User{
Username: username,
Email: email,
}
if err := user.SetPassword(password); err != nil {
return nil, err
}
if err := s.repo.CreateUser(user); err != nil {
return nil, err
}
_ = s.email.SendWelcomeEmail(email, username)
return user, nil
}
func (s *Service) VerifyUser(username, password string) (*User, error) {
user, err := s.repo.GetUserByUsername(username)
if err != nil {
return nil, errors.New("invalid username or password")
}
if !user.CheckPassword(password) {
return nil, errors.New("invalid username or password")
}
return user, nil
}
func (s *Service) RequestPasswordReset(email string) error {
user, err := s.repo.GetUserByEmail(email)
if err != nil {
return nil
}
token, err := generateSecureToken(32)
if err != nil {
return err
}
resetToken := &PasswordReset{
UserID: user.ID,
Token: token,
ExpiresAt: time.Now().Add(config.JWT.ResetTokenDuration),
}
if err := database.DB.Create(resetToken).Error; err != nil {
return err
}
appURL := os.Getenv("APP_URL")
if appURL == "" {
appURL = "https://dev.rideaware.org"
}
resetLink := fmt.Sprintf("%s/reset-password?token=%s", appURL, token)
return s.email.SendPasswordResetEmail(user.Email, user.Username, resetLink)
}
func (s *Service) ResetPassword(token, newPassword string) error {
if len(newPassword) < 8 {
return errors.New("password must be at least 8 characters long")
}
var resetToken PasswordReset
if err := database.DB.Where("token = ?", token).First(&resetToken).Error; err != nil {
return errors.New("invalid or expired reset token")
}
if !resetToken.IsValid() {
return errors.New("reset token has expired")
}
user, err := s.repo.GetUserByID(resetToken.UserID)
if err != nil {
return err
}
if err := user.SetPassword(newPassword); err != nil {
return err
}
now := time.Now()
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Model(user).Update("password", user.Password).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Model(&resetToken).Update("used_at", now).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
func (s *Service) GetUserByID(userID uint) (*User, error) {
return s.repo.GetUserByID(userID)
}
func (s *Service) UpdateUser(user *User) error {
return s.repo.UpdateUser(user)
}
func isValidEmail(email string) bool {
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return regex.MatchString(email)
}
func generateSecureToken(length int) (string, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}