package auth import ( "errors" "fmt" "log" "strconv" "tailly_back_v2/internal/domain" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) var ( ErrInvalidToken = errors.New("invalid token") ) type TokenAuth struct { accessTokenSecret string refreshTokenSecret string accessTokenExpiry time.Duration refreshTokenExpiry time.Duration } func NewTokenAuth(accessSecret, refreshSecret string, accessExpiry, refreshExpiry time.Duration) *TokenAuth { return &TokenAuth{ accessTokenSecret: accessSecret, refreshTokenSecret: refreshSecret, accessTokenExpiry: accessExpiry, refreshTokenExpiry: refreshExpiry, } } // GenerateTokens создает пару access и refresh токенов func (a *TokenAuth) GenerateTokens(userID int, existingRefreshToken ...string) (*domain.Tokens, error) { accessExpires := time.Now().UTC().Add(a.accessTokenExpiry) refreshExpires := time.Now().UTC().Add(a.refreshTokenExpiry) // Генерируем новый access token accessToken, err := a.generateAccessToken(userID) if err != nil { return nil, err } // Используем существующий refresh token, если он передан и валиден var refreshToken string if len(existingRefreshToken) > 0 && existingRefreshToken[0] != "" { // Проверяем, что существующий токен еще действителен if _, err := a.ValidateRefreshToken(existingRefreshToken[0]); err == nil { refreshToken = existingRefreshToken[0] } else { log.Printf("Existing refresh token is invalid, generating new one: %v", err) refreshToken, err = a.generateRefreshToken(userID) if err != nil { return nil, err } } } else { // Генерируем новый refresh token refreshToken, err = a.generateRefreshToken(userID) if err != nil { return nil, err } } return &domain.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, AccessTokenExpires: accessExpires, RefreshTokenExpires: refreshExpires, }, nil } // generateAccessToken создает access токен (короткоживущий) func (a *TokenAuth) generateAccessToken(userID int) (string, error) { claims := jwt.RegisteredClaims{ Subject: fmt.Sprintf("%d", userID), ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.accessTokenExpiry)), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(a.accessTokenSecret)) } // generateRefreshToken создает refresh токен (долгоживущий) func (a *TokenAuth) generateRefreshToken(userID int) (string, error) { claims := jwt.RegisteredClaims{ Subject: fmt.Sprintf("%d", userID), ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.refreshTokenExpiry)), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(a.refreshTokenSecret)) } // ValidateAccessToken проверяет access токен и возвращает userID func (a *TokenAuth) ValidateAccessToken(tokenString string) (int, error) { token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(a.accessTokenSecret), nil }) if err != nil { return 0, err } if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid { var userID int _, err := fmt.Sscanf(claims.Subject, "%d", &userID) if err != nil { return 0, ErrInvalidToken } return userID, nil } return 0, ErrInvalidToken } // ValidateRefreshToken проверяет refresh токен и возвращает userID func (a *TokenAuth) ValidateRefreshToken(tokenString string) (int, error) { token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(a.refreshTokenSecret), nil }) if err != nil { return 0, fmt.Errorf("token validation failed: %w", err) } if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid { userID, err := strconv.Atoi(claims.Subject) if err != nil { return 0, errors.New("invalid user ID in token") } return userID, nil } return 0, errors.New("invalid token claims") } // HashPassword хеширует пароль func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } // CheckPasswordHash проверяет пароль с хешем func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil }