Files

188 lines
5.6 KiB
Go
Raw Permalink Normal View History

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
}