feat: implement JWT refresh token mechanism and improve auth

This commit is contained in:
2025-09-28 11:46:20 +00:00
parent 59334da140
commit dfcd9db295
26 changed files with 860 additions and 587 deletions

View File

@@ -67,6 +67,7 @@ func main() {
api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST") api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST")
api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET")
api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET") api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET")
api.HandleFunc("/auth/refresh", authHandler.RefreshToken).Methods("POST")
api.HandleFunc("/search/multi", searchHandler.MultiSearch).Methods("GET") api.HandleFunc("/search/multi", searchHandler.MultiSearch).Methods("GET")
@@ -120,6 +121,8 @@ func main() {
protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET") protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET")
protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT") protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT")
protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE") protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE")
protected.HandleFunc("/auth/revoke-token", authHandler.RevokeRefreshToken).Methods("POST")
protected.HandleFunc("/auth/revoke-all-tokens", authHandler.RevokeAllRefreshTokens).Methods("POST")
protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET") protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET")
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST") protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST")

View File

@@ -2,24 +2,24 @@ package config
const ( const (
// Environment variable keys // Environment variable keys
EnvTMDBAccessToken = "TMDB_ACCESS_TOKEN" EnvTMDBAccessToken = "TMDB_ACCESS_TOKEN"
EnvJWTSecret = "JWT_SECRET" EnvJWTSecret = "JWT_SECRET"
EnvPort = "PORT" EnvPort = "PORT"
EnvBaseURL = "BASE_URL" EnvBaseURL = "BASE_URL"
EnvNodeEnv = "NODE_ENV" EnvNodeEnv = "NODE_ENV"
EnvGmailUser = "GMAIL_USER" EnvGmailUser = "GMAIL_USER"
EnvGmailPassword = "GMAIL_APP_PASSWORD" EnvGmailPassword = "GMAIL_APP_PASSWORD"
EnvLumexURL = "LUMEX_URL" EnvLumexURL = "LUMEX_URL"
EnvAllohaToken = "ALLOHA_TOKEN" EnvAllohaToken = "ALLOHA_TOKEN"
EnvRedAPIBaseURL = "REDAPI_BASE_URL" EnvRedAPIBaseURL = "REDAPI_BASE_URL"
EnvRedAPIKey = "REDAPI_KEY" EnvRedAPIKey = "REDAPI_KEY"
EnvMongoDBName = "MONGO_DB_NAME" EnvMongoDBName = "MONGO_DB_NAME"
EnvGoogleClientID = "GOOGLE_CLIENT_ID" EnvGoogleClientID = "GOOGLE_CLIENT_ID"
EnvGoogleClientSecret= "GOOGLE_CLIENT_SECRET" EnvGoogleClientSecret = "GOOGLE_CLIENT_SECRET"
EnvGoogleRedirectURL = "GOOGLE_REDIRECT_URL" EnvGoogleRedirectURL = "GOOGLE_REDIRECT_URL"
EnvFrontendURL = "FRONTEND_URL" EnvFrontendURL = "FRONTEND_URL"
EnvVibixHost = "VIBIX_HOST" EnvVibixHost = "VIBIX_HOST"
EnvVibixToken = "VIBIX_TOKEN" EnvVibixToken = "VIBIX_TOKEN"
// Default values // Default values
DefaultJWTSecret = "your-secret-key" DefaultJWTSecret = "your-secret-key"
@@ -28,7 +28,7 @@ const (
DefaultNodeEnv = "development" DefaultNodeEnv = "development"
DefaultRedAPIBase = "http://redapi.cfhttp.top" DefaultRedAPIBase = "http://redapi.cfhttp.top"
DefaultMongoDBName = "database" DefaultMongoDBName = "database"
DefaultVibixHost = "https://vibix.org" DefaultVibixHost = "https://vibix.org"
// Static constants // Static constants
TMDBImageBaseURL = "https://image.tmdb.org/t/p" TMDBImageBaseURL = "https://image.tmdb.org/t/p"

View File

@@ -3,8 +3,8 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"time"
"strings" "strings"
"time"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
@@ -46,7 +46,14 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return return
} }
response, err := h.authService.Login(req) // Получаем информацию о клиенте для refresh токена
userAgent := r.Header.Get("User-Agent")
ipAddress := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ipAddress = forwarded
}
response, err := h.authService.LoginWithTokens(req, userAgent, ipAddress)
if err != nil { if err != nil {
statusCode := http.StatusBadRequest statusCode := http.StatusBadRequest
if err.Error() == "Account not activated. Please verify your email." { if err.Error() == "Account not activated. Please verify your email." {
@@ -221,5 +228,82 @@ func (h *AuthHandler) ResendVerificationCode(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
// RefreshToken refreshes an access token using a refresh token
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
var req models.RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Получаем информацию о клиенте
userAgent := r.Header.Get("User-Agent")
ipAddress := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ipAddress = forwarded
}
tokenPair, err := h.authService.RefreshAccessToken(req.RefreshToken, userAgent, ipAddress)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: tokenPair,
Message: "Token refreshed successfully",
})
}
// RevokeRefreshToken revokes a specific refresh token
func (h *AuthHandler) RevokeRefreshToken(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
return
}
var req models.RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
err := h.authService.RevokeRefreshToken(userID, req.RefreshToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Message: "Refresh token revoked successfully",
})
}
// RevokeAllRefreshTokens revokes all refresh tokens for the current user
func (h *AuthHandler) RevokeAllRefreshTokens(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
return
}
err := h.authService.RevokeAllRefreshTokens(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Message: "All refresh tokens revoked successfully",
})
}
// helpers // helpers
func generateState() string { return uuidNew() } func generateState() string { return uuidNew() }

File diff suppressed because it is too large Load Diff

View File

@@ -189,8 +189,6 @@ func (h *MovieHandler) GetSimilar(w http.ResponseWriter, r *http.Request) {
}) })
} }
func (h *MovieHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) { func (h *MovieHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"]) id, err := strconv.Atoi(vars["id"])

View File

@@ -10,8 +10,8 @@ import (
"strings" "strings"
"time" "time"
"neomovies-api/pkg/config"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"neomovies-api/pkg/config"
) )
type PlayersHandler struct { type PlayersHandler struct {
@@ -75,7 +75,7 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
var allohaResponse struct { var allohaResponse struct {
Status string `json:"status"` Status string `json:"status"`
Data struct { Data struct {
Iframe string `json:"iframe"` Iframe string `json:"iframe"`
} `json:"data"` } `json:"data"`
} }

View File

@@ -85,7 +85,9 @@ func (h *ReactionsHandler) SetReaction(w http.ResponseWriter, r *http.Request) {
return return
} }
var request struct{ Type string `json:"type"` } var request struct {
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil { if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return

View File

@@ -7,21 +7,22 @@ import (
) )
type User struct { type User struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email" validate:"required,email"` Email string `json:"email" bson:"email" validate:"required,email"`
Password string `json:"-" bson:"password" validate:"required,min=6"` Password string `json:"-" bson:"password" validate:"required,min=6"`
Name string `json:"name" bson:"name" validate:"required"` Name string `json:"name" bson:"name" validate:"required"`
Avatar string `json:"avatar" bson:"avatar"` Avatar string `json:"avatar" bson:"avatar"`
Favorites []string `json:"favorites" bson:"favorites"` Favorites []string `json:"favorites" bson:"favorites"`
Verified bool `json:"verified" bson:"verified"` Verified bool `json:"verified" bson:"verified"`
VerificationCode string `json:"-" bson:"verificationCode,omitempty"` VerificationCode string `json:"-" bson:"verificationCode,omitempty"`
VerificationExpires time.Time `json:"-" bson:"verificationExpires,omitempty"` VerificationExpires time.Time `json:"-" bson:"verificationExpires,omitempty"`
IsAdmin bool `json:"isAdmin" bson:"isAdmin"` IsAdmin bool `json:"isAdmin" bson:"isAdmin"`
AdminVerified bool `json:"adminVerified" bson:"adminVerified"` AdminVerified bool `json:"adminVerified" bson:"adminVerified"`
CreatedAt time.Time `json:"created_at" bson:"createdAt"` CreatedAt time.Time `json:"created_at" bson:"createdAt"`
UpdatedAt time.Time `json:"updated_at" bson:"updatedAt"` UpdatedAt time.Time `json:"updated_at" bson:"updatedAt"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"` Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
GoogleID string `json:"googleId,omitempty" bson:"googleId,omitempty"` GoogleID string `json:"googleId,omitempty" bson:"googleId,omitempty"`
RefreshTokens []RefreshToken `json:"-" bson:"refreshTokens,omitempty"`
} }
type LoginRequest struct { type LoginRequest struct {
@@ -36,8 +37,9 @@ type RegisterRequest struct {
} }
type AuthResponse struct { type AuthResponse struct {
Token string `json:"token"` Token string `json:"token"`
User User `json:"user"` RefreshToken string `json:"refreshToken"`
User User `json:"user"`
} }
type VerifyEmailRequest struct { type VerifyEmailRequest struct {
@@ -48,3 +50,20 @@ type VerifyEmailRequest struct {
type ResendCodeRequest struct { type ResendCodeRequest struct {
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
} }
type RefreshToken struct {
Token string `json:"token" bson:"token"`
ExpiresAt time.Time `json:"expiresAt" bson:"expiresAt"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UserAgent string `json:"userAgent,omitempty" bson:"userAgent,omitempty"`
IPAddress string `json:"ipAddress,omitempty" bson:"ipAddress,omitempty"`
}
type TokenPair struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" validate:"required"`
}

View File

@@ -11,6 +11,7 @@ import (
"sync" "sync"
"time" "time"
"encoding/json"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
@@ -19,17 +20,16 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"encoding/json"
"neomovies-api/pkg/models" "neomovies-api/pkg/models"
) )
// AuthService contains the database connection, JWT secret, and email service. // AuthService contains the database connection, JWT secret, and email service.
type AuthService struct { type AuthService struct {
db *mongo.Database db *mongo.Database
jwtSecret string jwtSecret string
emailService *EmailService emailService *EmailService
baseURL string baseURL string
googleClientID string googleClientID string
googleClientSecret string googleClientSecret string
googleRedirectURL string googleRedirectURL string
@@ -38,18 +38,18 @@ type AuthService struct {
// Reaction represents a reaction entry in the database. // Reaction represents a reaction entry in the database.
type Reaction struct { type Reaction struct {
MediaID string `bson:"mediaId"` MediaID string `bson:"mediaId"`
Type string `bson:"type"` Type string `bson:"type"`
UserID primitive.ObjectID `bson:"userId"` UserID primitive.ObjectID `bson:"userId"`
} }
// NewAuthService creates and initializes a new AuthService. // NewAuthService creates and initializes a new AuthService.
func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService, baseURL string, googleClientID string, googleClientSecret string, googleRedirectURL string, frontendURL string) *AuthService { func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService, baseURL string, googleClientID string, googleClientSecret string, googleRedirectURL string, frontendURL string) *AuthService {
service := &AuthService{ service := &AuthService{
db: db, db: db,
jwtSecret: jwtSecret, jwtSecret: jwtSecret,
emailService: emailService, emailService: emailService,
baseURL: baseURL, baseURL: baseURL,
googleClientID: googleClientID, googleClientID: googleClientID,
googleClientSecret: googleClientSecret, googleClientSecret: googleClientSecret,
googleRedirectURL: googleRedirectURL, googleRedirectURL: googleRedirectURL,
@@ -81,11 +81,11 @@ func (s *AuthService) GetGoogleLoginURL(state string) (string, error) {
} }
type googleUserInfo struct { type googleUserInfo struct {
Sub string `json:"sub"` Sub string `json:"sub"`
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
Picture string `json:"picture"` Picture string `json:"picture"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
} }
// BuildFrontendRedirect builds frontend URL for redirect after OAuth; returns false if not configured // BuildFrontendRedirect builds frontend URL for redirect after OAuth; returns false if not configured
@@ -149,19 +149,19 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
if err == mongo.ErrNoDocuments { if err == mongo.ErrNoDocuments {
// Create new user // Create new user
user = models.User{ user = models.User{
ID: primitive.NewObjectID(), ID: primitive.NewObjectID(),
Email: gUser.Email, Email: gUser.Email,
Password: "", Password: "",
Name: gUser.Name, Name: gUser.Name,
Avatar: gUser.Picture, Avatar: gUser.Picture,
Favorites: []string{}, Favorites: []string{},
Verified: true, Verified: true,
IsAdmin: false, IsAdmin: false,
AdminVerified: false, AdminVerified: false,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
Provider: "google", Provider: "google",
GoogleID: gUser.Sub, GoogleID: gUser.Sub,
} }
if _, err := collection.InsertOne(ctx, user); err != nil { if _, err := collection.InsertOne(ctx, user); err != nil {
return nil, err return nil, err
@@ -171,13 +171,17 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
} else { } else {
// Existing user: ensure fields // Existing user: ensure fields
update := bson.M{ update := bson.M{
"verified": true, "verified": true,
"provider": "google", "provider": "google",
"googleId": gUser.Sub, "googleId": gUser.Sub,
"updatedAt": time.Now(), "updatedAt": time.Now(),
} }
if user.Name == "" && gUser.Name != "" { update["name"] = gUser.Name } if user.Name == "" && gUser.Name != "" {
if user.Avatar == "" && gUser.Picture != "" { update["avatar"] = gUser.Picture } update["name"] = gUser.Name
}
if user.Avatar == "" && gUser.Picture != "" {
update["avatar"] = gUser.Picture
}
_, _ = collection.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": update}) _, _ = collection.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": update})
} }
@@ -186,10 +190,16 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
// If we created user above, we already have user.ID set; else fetch updated // If we created user above, we already have user.ID set; else fetch updated
_ = collection.FindOne(ctx, bson.M{"email": gUser.Email}).Decode(&user) _ = collection.FindOne(ctx, bson.M{"email": gUser.Email}).Decode(&user)
} }
token, err := s.generateJWT(user.ID.Hex()) tokenPair, err := s.generateTokenPair(user.ID.Hex(), "", "")
if err != nil { return nil, err } if err != nil {
return nil, err
}
return &models.AuthResponse{ Token: token, User: user }, nil return &models.AuthResponse{
Token: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
User: user,
}, nil
} }
// generateVerificationCode creates a 6-digit verification code. // generateVerificationCode creates a 6-digit verification code.
@@ -216,18 +226,18 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
codeExpires := time.Now().Add(10 * time.Minute) codeExpires := time.Now().Add(10 * time.Minute)
user := models.User{ user := models.User{
ID: primitive.NewObjectID(), ID: primitive.NewObjectID(),
Email: req.Email, Email: req.Email,
Password: string(hashedPassword), Password: string(hashedPassword),
Name: req.Name, Name: req.Name,
Favorites: []string{}, Favorites: []string{},
Verified: false, Verified: false,
VerificationCode: code, VerificationCode: code,
VerificationExpires: codeExpires, VerificationExpires: codeExpires,
IsAdmin: false, IsAdmin: false,
AdminVerified: false, AdminVerified: false,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
_, err = collection.InsertOne(context.Background(), user) _, err = collection.InsertOne(context.Background(), user)
@@ -246,7 +256,7 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
} }
// Login authenticates a user. // Login authenticates a user.
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) { func (s *AuthService) LoginWithTokens(req models.LoginRequest, userAgent, ipAddress string) (*models.AuthResponse, error) {
collection := s.db.Collection("users") collection := s.db.Collection("users")
var user models.User var user models.User
@@ -264,17 +274,23 @@ func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, erro
return nil, errors.New("Invalid password") return nil, errors.New("Invalid password")
} }
token, err := s.generateJWT(user.ID.Hex()) tokenPair, err := s.generateTokenPair(user.ID.Hex(), userAgent, ipAddress)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &models.AuthResponse{ return &models.AuthResponse{
Token: token, Token: tokenPair.AccessToken,
User: user, RefreshToken: tokenPair.RefreshToken,
User: user,
}, nil }, nil
} }
// Login authenticates a user (legacy method for backward compatibility).
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
return s.LoginWithTokens(req, "", "")
}
// GetUserByID retrieves a user by their ID. // GetUserByID retrieves a user by their ID.
func (s *AuthService) GetUserByID(userID string) (*models.User, error) { func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
collection := s.db.Collection("users") collection := s.db.Collection("users")
@@ -320,7 +336,7 @@ func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, e
func (s *AuthService) generateJWT(userID string) (string, error) { func (s *AuthService) generateJWT(userID string) (string, error) {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"user_id": userID, "user_id": userID,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), "exp": time.Now().Add(time.Hour * 1).Unix(), // Сократил время жизни до 1 часа
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"jti": uuid.New().String(), "jti": uuid.New().String(),
} }
@@ -329,6 +345,158 @@ func (s *AuthService) generateJWT(userID string) (string, error) {
return token.SignedString([]byte(s.jwtSecret)) return token.SignedString([]byte(s.jwtSecret))
} }
// generateRefreshToken generates a new refresh token
func (s *AuthService) generateRefreshToken() string {
return uuid.New().String()
}
// generateTokenPair generates both access and refresh tokens
func (s *AuthService) generateTokenPair(userID, userAgent, ipAddress string) (*models.TokenPair, error) {
accessToken, err := s.generateJWT(userID)
if err != nil {
return nil, err
}
refreshToken := s.generateRefreshToken()
// Сохраняем refresh token в базе данных
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return nil, err
}
refreshTokenDoc := models.RefreshToken{
Token: refreshToken,
ExpiresAt: time.Now().Add(time.Hour * 24 * 30), // 30 дней
CreatedAt: time.Now(),
UserAgent: userAgent,
IPAddress: ipAddress,
}
// Удаляем старые истекшие токены и добавляем новый
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"expiresAt": bson.M{"$lt": time.Now()},
},
},
},
)
if err != nil {
return nil, err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$push": bson.M{
"refreshTokens": refreshTokenDoc,
},
"$set": bson.M{
"updatedAt": time.Now(),
},
},
)
if err != nil {
return nil, err
}
return &models.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
// RefreshAccessToken refreshes an access token using a refresh token
func (s *AuthService) RefreshAccessToken(refreshToken, userAgent, ipAddress string) (*models.TokenPair, error) {
collection := s.db.Collection("users")
// Найти пользователя с данным refresh токеном
var user models.User
err := collection.FindOne(
context.Background(),
bson.M{
"refreshTokens": bson.M{
"$elemMatch": bson.M{
"token": refreshToken,
"expiresAt": bson.M{"$gt": time.Now()},
},
},
},
).Decode(&user)
if err != nil {
return nil, errors.New("invalid or expired refresh token")
}
// Удалить использованный refresh token
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": user.ID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"token": refreshToken,
},
},
},
)
if err != nil {
return nil, err
}
// Создать новую пару токенов
return s.generateTokenPair(user.ID.Hex(), userAgent, ipAddress)
}
// RevokeRefreshToken revokes a specific refresh token
func (s *AuthService) RevokeRefreshToken(userID, refreshToken string) error {
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"token": refreshToken,
},
},
},
)
return err
}
// RevokeAllRefreshTokens revokes all refresh tokens for a user
func (s *AuthService) RevokeAllRefreshTokens(userID string) error {
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$set": bson.M{
"refreshTokens": []models.RefreshToken{},
"updatedAt": time.Now(),
},
},
)
return err
}
// VerifyEmail verifies a user's email with a code. // VerifyEmail verifies a user's email with a code.
func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) { func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) {
collection := s.db.Collection("users") collection := s.db.Collection("users")
@@ -439,7 +607,7 @@ func (s *AuthService) DeleteAccount(ctx context.Context, userID string) error {
go func(r Reaction) { go func(r Reaction) {
defer wg.Done() defer wg.Done()
url := fmt.Sprintf("%s/reactions/remove/%s/%s", s.baseURL, r.MediaID, r.Type) // Changed from cubAPIURL to baseURL url := fmt.Sprintf("%s/reactions/remove/%s/%s", s.baseURL, r.MediaID, r.Type) // Changed from cubAPIURL to baseURL
req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE" req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE"
if err != nil { if err != nil {
// Log the error but don't stop the process // Log the error but don't stop the process
fmt.Printf("failed to create request for cub.rip: %v\n", err) fmt.Printf("failed to create request for cub.rip: %v\n", err)

View File

@@ -48,8 +48,6 @@ func (s *MovieService) GetSimilar(id, page int, language string) (*models.TMDBRe
return s.tmdb.GetSimilarMovies(id, page, language) return s.tmdb.GetSimilarMovies(id, page, language)
} }
func (s *MovieService) GetExternalIDs(id int) (*models.ExternalIDs, error) { func (s *MovieService) GetExternalIDs(id int) (*models.ExternalIDs, error) {
return s.tmdb.GetMovieExternalIDs(id) return s.tmdb.GetMovieExternalIDs(id)
} }

View File

@@ -83,7 +83,9 @@ func (s *ReactionsService) GetMyReaction(userID, mediaType, mediaID string) (str
collection := s.db.Collection("reactions") collection := s.db.Collection("reactions")
ctx := context.Background() ctx := context.Background()
var result struct{ Type string `bson:"type"` } var result struct {
Type string `bson:"type"`
}
err := collection.FindOne(ctx, bson.M{ err := collection.FindOne(ctx, bson.M{
"userId": userID, "userId": userID,
"mediaType": mediaType, "mediaType": mediaType,

View File

@@ -207,7 +207,6 @@ func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID
return response, nil return response, nil
} }
// SearchMovies - поиск фильмов с дополнительной фильтрацией // SearchMovies - поиск фильмов с дополнительной фильтрацией
func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) { func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
params := map[string]string{ params := map[string]string{