Files
neomovies-api/pkg/services/auth.go

342 lines
8.8 KiB
Go
Raw Normal View History

2025-08-07 13:47:42 +00:00
package services
import (
"context"
"errors"
"fmt"
2025-08-07 18:25:43 +00:00
"io"
2025-08-07 13:47:42 +00:00
"math/rand"
2025-08-07 18:25:43 +00:00
"net/http"
"sync"
2025-08-07 13:47:42 +00:00
"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"
)
2025-08-07 18:25:43 +00:00
// AuthService contains the database connection, JWT secret, and email service.
2025-08-07 13:47:42 +00:00
type AuthService struct {
db *mongo.Database
jwtSecret string
emailService *EmailService
2025-08-07 18:25:43 +00:00
cubAPIURL string
2025-08-07 13:47:42 +00:00
}
2025-08-07 18:25:43 +00:00
// 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 {
2025-08-07 13:47:42 +00:00
service := &AuthService{
db: db,
jwtSecret: jwtSecret,
emailService: emailService,
2025-08-07 18:25:43 +00:00
cubAPIURL: cubAPIURL,
2025-08-07 13:47:42 +00:00
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return service
}
2025-08-07 18:25:43 +00:00
// generateVerificationCode creates a 6-digit verification code.
2025-08-07 13:47:42 +00:00
func (s *AuthService) generateVerificationCode() string {
return fmt.Sprintf("%06d", rand.Intn(900000)+100000)
}
2025-08-07 18:25:43 +00:00
// Register registers a new user.
2025-08-07 13:47:42 +00:00
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()
2025-08-07 18:25:43 +00:00
codeExpires := time.Now().Add(10 * time.Minute)
2025-08-07 13:47:42 +00:00
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
}
2025-08-07 18:25:43 +00:00
// Login authenticates a user.
2025-08-07 13:47:42 +00:00
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
collection := s.db.Collection("users")
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
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
}
2025-08-07 18:25:43 +00:00
// GetUserByID retrieves a user by their ID.
2025-08-07 13:47:42 +00:00
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
}
2025-08-07 18:25:43 +00:00
// UpdateUser updates a user's information.
2025-08-07 13:47:42 +00:00
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)
}
2025-08-07 18:25:43 +00:00
// generateJWT generates a new JWT for a given user ID.
2025-08-07 13:47:42 +00:00
func (s *AuthService) generateJWT(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
2025-08-07 18:25:43 +00:00
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
2025-08-07 13:47:42 +00:00
"iat": time.Now().Unix(),
"jti": uuid.New().String(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.jwtSecret))
}
2025-08-07 18:25:43 +00:00
// VerifyEmail verifies a user's email with a code.
2025-08-07 13:47:42 +00:00
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
}
2025-08-07 18:25:43 +00:00
// ResendVerificationCode sends a new verification email.
2025-08-07 13:47:42 +00:00
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
2025-08-07 18:25:43 +00:00
}
// 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
}