Initial commit: dating app backend (Go/Gin/GORM/Postgres)
Phone+OTP auth, staged onboarding, admin approval workflow, and a full discovery/swipe/match/chat system with photo upload and approximate distance (haversine) based on user location. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user