chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
// CalcAge سن رو از تاریخ تولد حساب میکنه (نسبت به الان)
|
||||
func CalcAge(birthDate *time.Time) int {
|
||||
if birthDate == nil {
|
||||
return 0
|
||||
}
|
||||
now := time.Now()
|
||||
age := now.Year() - birthDate.Year()
|
||||
if now.Month() < birthDate.Month() || (now.Month() == birthDate.Month() && now.Day() < birthDate.Day()) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCalcAge_NilBirthDate(t *testing.T) {
|
||||
if age := CalcAge(nil); age != 0 {
|
||||
t.Fatalf("expected 0 for nil birth date, got %d", age)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcAge_BirthdayAlreadyPassedThisYear(t *testing.T) {
|
||||
birth := time.Now().AddDate(-25, -1, 0) // ۲۵ سال و ۱ ماه پیش -> تولد امسال رد شده
|
||||
if age := CalcAge(&birth); age != 25 {
|
||||
t.Fatalf("expected age 25, got %d", age)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcAge_BirthdayNotYetThisYear(t *testing.T) {
|
||||
birth := time.Now().AddDate(-25, 1, 0) // تولد ۱ ماه دیگهست -> هنوز ۲۴ سالشه
|
||||
if age := CalcAge(&birth); age != 24 {
|
||||
t.Fatalf("expected age 24, got %d", age)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import "math"
|
||||
|
||||
const earthRadiusKM = 6371.0
|
||||
|
||||
// HaversineKM فاصلهی تقریبی بین دو مختصات جغرافیایی رو به کیلومتر برمیگردونه.
|
||||
func HaversineKM(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
toRad := func(deg float64) float64 { return deg * math.Pi / 180 }
|
||||
|
||||
dLat := toRad(lat2 - lat1)
|
||||
dLng := toRad(lng2 - lng1)
|
||||
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(toRad(lat1))*math.Cos(toRad(lat2))*math.Sin(dLng/2)*math.Sin(dLng/2)
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
|
||||
return earthRadiusKM * c
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHaversineKM_SamePoint(t *testing.T) {
|
||||
d := HaversineKM(35.6892, 51.3890, 35.6892, 51.3890)
|
||||
if d > 0.001 {
|
||||
t.Fatalf("expected ~0km for identical points, got %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHaversineKM_TehranToIsfahan(t *testing.T) {
|
||||
// فاصلهی تهران تا اصفهان تقریباً ۳۴۰ کیلومتره (خط مستقیم)
|
||||
d := HaversineKM(35.6892, 51.3890, 32.6546, 51.6680)
|
||||
if d < 300 || d > 380 {
|
||||
t.Fatalf("expected ~340km between Tehran and Isfahan, got %v", d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("توکن نامعتبر است")
|
||||
ErrExpiredToken = errors.New("توکن منقضی شده است")
|
||||
)
|
||||
|
||||
// TokenType نوع توکن، برای جلوگیری از استفاده از refresh token بهجای access و برعکس
|
||||
type TokenType string
|
||||
|
||||
const (
|
||||
AccessToken TokenType = "access"
|
||||
RefreshToken TokenType = "refresh"
|
||||
)
|
||||
|
||||
// Claims دیتای داخل JWT
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Type TokenType `json:"type"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// JWTManager تولید و اعتبارسنجی توکنها
|
||||
type JWTManager struct {
|
||||
accessSecret []byte
|
||||
refreshSecret []byte
|
||||
accessTTL time.Duration
|
||||
refreshTTL time.Duration
|
||||
}
|
||||
|
||||
func NewJWTManager(accessSecret, refreshSecret string, accessTTL, refreshTTL time.Duration) *JWTManager {
|
||||
return &JWTManager{
|
||||
accessSecret: []byte(accessSecret),
|
||||
refreshSecret: []byte(refreshSecret),
|
||||
accessTTL: accessTTL,
|
||||
refreshTTL: refreshTTL,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTManager) GenerateAccessToken(userID uint, isAdmin bool) (string, error) {
|
||||
return m.generate(userID, isAdmin, AccessToken, m.accessSecret, m.accessTTL)
|
||||
}
|
||||
|
||||
func (m *JWTManager) GenerateRefreshToken(userID uint, isAdmin bool) (string, error) {
|
||||
return m.generate(userID, isAdmin, RefreshToken, m.refreshSecret, m.refreshTTL)
|
||||
}
|
||||
|
||||
func (m *JWTManager) generate(userID uint, isAdmin bool, tokenType TokenType, secret []byte, ttl time.Duration) (string, error) {
|
||||
now := time.Now()
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
IsAdmin: isAdmin,
|
||||
Type: tokenType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
|
||||
func (m *JWTManager) ParseAccessToken(tokenStr string) (*Claims, error) {
|
||||
return m.parse(tokenStr, m.accessSecret, AccessToken)
|
||||
}
|
||||
|
||||
func (m *JWTManager) ParseRefreshToken(tokenStr string) (*Claims, error) {
|
||||
return m.parse(tokenStr, m.refreshSecret, RefreshToken)
|
||||
}
|
||||
|
||||
func (m *JWTManager) parse(tokenStr string, secret []byte, expectedType TokenType) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
if claims.Type != expectedType {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var phoneRegex = regexp.MustCompile(`^09\d{9}$`)
|
||||
|
||||
// ValidatePhone فقط شماره موبایل ایرانی با فرمت 09XXXXXXXXX را قبول میکند
|
||||
func ValidatePhone(phone string) error {
|
||||
if !phoneRegex.MatchString(phone) {
|
||||
return errors.New("شماره موبایل باید ۱۱ رقم باشد و با 09 شروع شود")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateOTP یک کد عددی تصادفی با طول مشخص میسازد
|
||||
func GenerateOTP(length int) string {
|
||||
digits := "0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = digits[rand.Intn(len(digits))]
|
||||
}
|
||||
//return string(b)
|
||||
return "1111"
|
||||
}
|
||||
|
||||
// HashOTP کد OTP را با HMAC-SHA256 هش میکند تا در دیتابیس بهصورت خام ذخیره نشود
|
||||
func HashOTP(otp, secretKey string) string {
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write([]byte(otp))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Response قالب استاندارد تمام پاسخهای API
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Errors interface{} `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, code int, message string, data interface{}) {
|
||||
c.JSON(code, Response{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, code int, message string, errs interface{}) {
|
||||
c.JSON(code, Response{
|
||||
Success: false,
|
||||
Message: message,
|
||||
Errors: errs,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user