mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 18:08:51 +05:00
342 lines
8.8 KiB
Go
342 lines
8.8 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"neomovies-api/pkg/models"
|
|
)
|
|
|
|
// AuthService contains the database connection, JWT secret, and email service.
|
|
type AuthService struct {
|
|
db *mongo.Database
|
|
jwtSecret string
|
|
emailService *EmailService
|
|
cubAPIURL string
|
|
}
|
|
|
|
// Reaction represents a reaction entry in the database.
|
|
type Reaction struct {
|
|
MediaID string `bson:"mediaId"`
|
|
Type string `bson:"type"`
|
|
UserID primitive.ObjectID `bson:"userId"`
|
|
}
|
|
|
|
// NewAuthService creates and initializes a new AuthService.
|
|
func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService, cubAPIURL string) *AuthService {
|
|
service := &AuthService{
|
|
db: db,
|
|
jwtSecret: jwtSecret,
|
|
emailService: emailService,
|
|
cubAPIURL: cubAPIURL,
|
|
}
|
|
|
|
return service
|
|
}
|
|
|
|
// generateVerificationCode creates a 6-digit verification code.
|
|
func (s *AuthService) generateVerificationCode() string {
|
|
return fmt.Sprintf("%06d", rand.Intn(900000)+100000)
|
|
}
|
|
|
|
// Register registers a new user.
|
|
func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface{}, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
var existingUser models.User
|
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&existingUser)
|
|
if err == nil {
|
|
return nil, errors.New("email already registered")
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
code := s.generateVerificationCode()
|
|
codeExpires := time.Now().Add(10 * time.Minute)
|
|
|
|
user := models.User{
|
|
ID: primitive.NewObjectID(),
|
|
Email: req.Email,
|
|
Password: string(hashedPassword),
|
|
Name: req.Name,
|
|
Favorites: []string{},
|
|
Verified: false,
|
|
VerificationCode: code,
|
|
VerificationExpires: codeExpires,
|
|
IsAdmin: false,
|
|
AdminVerified: false,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
_, err = collection.InsertOne(context.Background(), user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.emailService != nil {
|
|
go s.emailService.SendVerificationEmail(user.Email, code)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"success": true,
|
|
"message": "Registered. Check email for verification code.",
|
|
}, nil
|
|
}
|
|
|
|
// Login authenticates a user.
|
|
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
var user models.User
|
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
|
|
if err != nil {
|
|
return nil, errors.New("User not found")
|
|
}
|
|
|
|
if !user.Verified {
|
|
return nil, errors.New("Account not activated. Please verify your email.")
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
|
|
if err != nil {
|
|
return nil, errors.New("Invalid password")
|
|
}
|
|
|
|
token, err := s.generateJWT(user.ID.Hex())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &models.AuthResponse{
|
|
Token: token,
|
|
User: user,
|
|
}, nil
|
|
}
|
|
|
|
// GetUserByID retrieves a user by their ID.
|
|
func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
objectID, err := primitive.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var user models.User
|
|
err = collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// UpdateUser updates a user's information.
|
|
func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
objectID, err := primitive.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
updates["updated_at"] = time.Now()
|
|
|
|
_, err = collection.UpdateOne(
|
|
context.Background(),
|
|
bson.M{"_id": objectID},
|
|
bson.M{"$set": updates},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetUserByID(userID)
|
|
}
|
|
|
|
// generateJWT generates a new JWT for a given user ID.
|
|
func (s *AuthService) generateJWT(userID string) (string, error) {
|
|
claims := jwt.MapClaims{
|
|
"user_id": userID,
|
|
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"jti": uuid.New().String(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString([]byte(s.jwtSecret))
|
|
}
|
|
|
|
// VerifyEmail verifies a user's email with a code.
|
|
func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
var user models.User
|
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
if user.Verified {
|
|
return map[string]interface{}{
|
|
"success": true,
|
|
"message": "Email already verified",
|
|
}, nil
|
|
}
|
|
|
|
if user.VerificationCode != req.Code || user.VerificationExpires.Before(time.Now()) {
|
|
return nil, errors.New("invalid or expired verification code")
|
|
}
|
|
|
|
_, err = collection.UpdateOne(
|
|
context.Background(),
|
|
bson.M{"email": req.Email},
|
|
bson.M{
|
|
"$set": bson.M{"verified": true},
|
|
"$unset": bson.M{
|
|
"verificationCode": "",
|
|
"verificationExpires": "",
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"success": true,
|
|
"message": "Email verified successfully",
|
|
}, nil
|
|
}
|
|
|
|
// ResendVerificationCode sends a new verification email.
|
|
func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[string]interface{}, error) {
|
|
collection := s.db.Collection("users")
|
|
|
|
var user models.User
|
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
if user.Verified {
|
|
return nil, errors.New("email already verified")
|
|
}
|
|
|
|
code := s.generateVerificationCode()
|
|
codeExpires := time.Now().Add(10 * time.Minute)
|
|
|
|
_, err = collection.UpdateOne(
|
|
context.Background(),
|
|
bson.M{"email": req.Email},
|
|
bson.M{
|
|
"$set": bson.M{
|
|
"verificationCode": code,
|
|
"verificationExpires": codeExpires,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.emailService != nil {
|
|
go s.emailService.SendVerificationEmail(user.Email, code)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"success": true,
|
|
"message": "Verification code sent to your email",
|
|
}, nil
|
|
}
|
|
|
|
// DeleteAccount deletes a user and all associated data.
|
|
func (s *AuthService) DeleteAccount(ctx context.Context, userID string) error {
|
|
objectID, err := primitive.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid user ID format: %w", err)
|
|
}
|
|
|
|
// Step 1: Find user reactions and remove them from cub.rip
|
|
if s.cubAPIURL != "" {
|
|
reactionsCollection := s.db.Collection("reactions")
|
|
var userReactions []Reaction
|
|
cursor, err := reactionsCollection.Find(ctx, bson.M{"userId": objectID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find user reactions: %w", err)
|
|
}
|
|
if err = cursor.All(ctx, &userReactions); err != nil {
|
|
return fmt.Errorf("failed to decode user reactions: %w", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
for _, reaction := range userReactions {
|
|
wg.Add(1)
|
|
go func(r Reaction) {
|
|
defer wg.Done()
|
|
url := fmt.Sprintf("%s/reactions/remove/%s/%s", s.cubAPIURL, r.MediaID, r.Type)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE"
|
|
if err != nil {
|
|
// Log the error but don't stop the process
|
|
fmt.Printf("failed to create request for cub.rip: %v\n", err)
|
|
return
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
fmt.Printf("failed to send request to cub.rip: %v\n", err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("cub.rip API responded with status %d: %s\n", resp.StatusCode, body)
|
|
}
|
|
}(reaction)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// Step 2: Delete all user-related data from the database
|
|
usersCollection := s.db.Collection("users")
|
|
favoritesCollection := s.db.Collection("favorites")
|
|
reactionsCollection := s.db.Collection("reactions")
|
|
|
|
_, err = usersCollection.DeleteOne(ctx, bson.M{"_id": objectID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user: %w", err)
|
|
}
|
|
|
|
_, err = favoritesCollection.DeleteMany(ctx, bson.M{"userId": objectID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user favorites: %w", err)
|
|
}
|
|
|
|
_, err = reactionsCollection.DeleteMany(ctx, bson.M{"userId": objectID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user reactions: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|