package service import ( "errors" "time" "gorm.io/gorm" "dating-app/internal/models" "dating-app/internal/repository" "dating-app/internal/utils" ) var ( ErrPhoneBlocked = errors.New("حساب کاربری مسدود شده است") ErrOTPStillValid = errors.New("کد تایید قبلی هنوز معتبر است") ErrOTPNotRequested = errors.New("ابتدا باید کد تایید را درخواست کنید") ErrOTPExpired = errors.New("کد تایید منقضی شده است") ErrOTPInvalid = errors.New("کد وارد شده نامعتبر است") ErrSMSSendFailed = errors.New("ارسال پیامک با خطا مواجه شد؛ لطفاً چند لحظه دیگر دوباره تلاش کنید") ErrUserNotFound = errors.New("کاربر یافت نشد") ) // OTPStillValidError برای برگردوندن تعداد ثانیه‌ی باقی‌مانده تا انقضای کد قبلی type OTPStillValidError struct { RemainingSeconds int } func (e *OTPStillValidError) Error() string { return "کد تایید قبلی هنوز معتبر است" } type AuthService struct { userRepo *repository.UserRepository otpSender OTPSender jwtManager *utils.JWTManager otpExpiry time.Duration otpLength int otpHashSecret string } func NewAuthService( userRepo *repository.UserRepository, otpSender OTPSender, jwtManager *utils.JWTManager, otpExpirySeconds int, otpLength int, otpHashSecret string, ) *AuthService { return &AuthService{ userRepo: userRepo, otpSender: otpSender, jwtManager: jwtManager, otpExpiry: time.Duration(otpExpirySeconds) * time.Second, otpLength: otpLength, otpHashSecret: otpHashSecret, } } // RequestOTP یک کد تایید جدید تولید، هش و ذخیره می‌کند و از طریق OTPSender ارسال می‌کند. func (s *AuthService) RequestOTP(phone string) error { if err := utils.ValidatePhone(phone); err != nil { return err } user, _, err := s.userRepo.GetOrCreateByPhone(phone) if err != nil { return err } if user.State == models.UserStateBlocked { return ErrPhoneBlocked } // اگر کد قبلی هنوز معتبره، اجازه‌ی ارسال دوباره نده if user.OTPCode != nil && user.OTPExpiry != nil && time.Now().Before(*user.OTPExpiry) { remaining := int(time.Until(*user.OTPExpiry).Seconds()) return &OTPStillValidError{RemainingSeconds: remaining} } otp := utils.GenerateOTP(s.otpLength) hashed := utils.HashOTP(otp, s.otpHashSecret) expiry := time.Now().Add(s.otpExpiry) if err := s.userRepo.UpdateFields(user, map[string]interface{}{ "otp_code": hashed, "otp_expiry": expiry, }); err != nil { return err } // ارسال پیامک به‌صورت sync؛ اگه واقعاً شکست خورد، صادقانه به کاربر خبر بده // و کد ذخیره‌شده رو پاک کن تا کاربر بلافاصله بتونه دوباره تلاش کنه // (وگرنه تا پایان OTPExpiry با پیام «کد قبلی هنوز معتبره» قفل می‌مونه). if ok := s.otpSender.Send(phone, otp); !ok { _ = s.userRepo.UpdateFields(user, map[string]interface{}{ "otp_code": nil, "otp_expiry": nil, }) return ErrSMSSendFailed } return nil } // VerifyOTP کد را بررسی می‌کند و در صورت صحت، کاربر را فعال کرده و توکن صادر می‌کند. func (s *AuthService) VerifyOTP(phone, otp string) (*models.User, string, string, bool, error) { user, err := s.userRepo.FindByPhone(phone) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, "", "", false, ErrUserNotFound } return nil, "", "", false, err } if user.OTPCode == nil || user.OTPExpiry == nil { return nil, "", "", false, ErrOTPNotRequested } if time.Now().After(*user.OTPExpiry) { _ = s.userRepo.UpdateFields(user, map[string]interface{}{ "otp_code": nil, "otp_expiry": nil, }) return nil, "", "", false, ErrOTPExpired } if *user.OTPCode != utils.HashOTP(otp, s.otpHashSecret) { return nil, "", "", false, ErrOTPInvalid } isNewUser := user.State == models.UserStateInactive now := time.Now() updates := map[string]interface{}{ "otp_code": nil, "otp_expiry": nil, "state": models.UserStateActive, "last_login": now, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return nil, "", "", false, err } // رفرش شیء user در حافظه برای برگردوندن مقادیر به‌روز user.State = models.UserStateActive user.LastLogin = &now user.OTPCode = nil user.OTPExpiry = nil access, err := s.jwtManager.GenerateAccessToken(user.ID, user.IsAdmin) if err != nil { return nil, "", "", false, err } refresh, err := s.jwtManager.GenerateRefreshToken(user.ID, user.IsAdmin) if err != nil { return nil, "", "", false, err } return user, access, refresh, isNewUser, nil } // RefreshTokens با یک refresh token معتبر، یک جفت توکن جدید صادر می‌کند. func (s *AuthService) RefreshTokens(refreshToken string) (string, string, error) { claims, err := s.jwtManager.ParseRefreshToken(refreshToken) if err != nil { return "", "", err } user, err := s.userRepo.FindByID(claims.UserID) if err != nil { return "", "", ErrUserNotFound } if user.State == models.UserStateBlocked || !user.IsActive { return "", "", ErrPhoneBlocked } access, err := s.jwtManager.GenerateAccessToken(user.ID, user.IsAdmin) if err != nil { return "", "", err } newRefresh, err := s.jwtManager.GenerateRefreshToken(user.ID, user.IsAdmin) if err != nil { return "", "", err } return access, newRefresh, nil }