From ea2edbb9e0a6be12ad3c266f63cd66f1a18a78ba Mon Sep 17 00:00:00 2001 From: lethdat Date: Thu, 22 Jan 2026 18:02:45 +0700 Subject: [PATCH] refactor: Update data models to use pointer fields for optional values and add atomic database operations for video views and user storage. --- internal/api/auth/auth.go | 27 ++++++++++++++----------- internal/api/payment/handler.go | 9 ++++++--- internal/api/video/handler.go | 36 ++++++++++++++++++++++++++++----- internal/middleware/auth.go | 4 ++-- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/internal/api/auth/auth.go b/internal/api/auth/auth.go index 4b85346..8d3d75b 100644 --- a/internal/api/auth/auth.go +++ b/internal/api/auth/auth.go @@ -60,17 +60,17 @@ func (h *handler) Login(c *gin.Context) { } // Verify password (if user has password, google users might not) - if user.Password == "" { + if user.Password == nil || *user.Password == "" { response.Error(c, http.StatusUnauthorized, "Please login with Google") return } - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(req.Password)); err != nil { response.Error(c, http.StatusUnauthorized, "Invalid credentials") return } - h.generateAndSetTokens(c, user.ID, user.Email, user.Role) + h.generateAndSetTokens(c, user.ID, user.Email, *user.Role) response.Success(c, gin.H{"user": user}) } @@ -111,12 +111,14 @@ func (h *handler) Register(c *gin.Context) { return } + password := string(hashedPassword) + role := "USER" newUser := &model.User{ ID: uuid.New().String(), Email: req.Email, - Password: string(hashedPassword), - Username: req.Username, - Role: "USER", + Password: &password, + Username: &req.Username, + Role: &role, } if err := u.WithContext(c.Request.Context()).Create(newUser); err != nil { @@ -226,23 +228,24 @@ func (h *handler) GoogleCallback(c *gin.Context) { u := query.User user, err := u.WithContext(c.Request.Context()).Where(u.Email.Eq(googleUser.Email)).First() if err != nil { + role := "USER" user = &model.User{ ID: uuid.New().String(), Email: googleUser.Email, - Username: googleUser.Name, - GoogleID: googleUser.ID, - Avatar: googleUser.Picture, - Role: "USER", + Username: &googleUser.Name, + GoogleID: &googleUser.ID, + Avatar: &googleUser.Picture, + Role: &role, } if err := u.WithContext(c.Request.Context()).Create(user); err != nil { response.Fail(c, "Failed to create user") return } - } else if user.GoogleID == "" { + } else if user.GoogleID == nil || *user.GoogleID == "" { u.WithContext(c.Request.Context()).Where(u.ID.Eq(user.ID)).Update(u.GoogleID, googleUser.ID) } - h.generateAndSetTokens(c, user.ID, user.Email, user.Role) + h.generateAndSetTokens(c, user.ID, user.Email, *user.Role) response.Success(c, gin.H{"user": user}) } diff --git a/internal/api/payment/handler.go b/internal/api/payment/handler.go index a2b834f..2440d6f 100644 --- a/internal/api/payment/handler.go +++ b/internal/api/payment/handler.go @@ -52,13 +52,16 @@ func (h *Handler) CreatePayment(c *gin.Context) { // In a real scenario, we would contact Stripe/PayPal here to create a session // For now, we just create a "PENDING" payment record. + status := "PENDING" + provider := "STRIPE" + payment := &model.Payment{ ID: uuid.New().String(), UserID: userID, - PlanID: req.PlanID, + PlanID: &req.PlanID, Amount: req.Amount, - Status: "PENDING", - Provider: "STRIPE", // Defaulting to Stripe for this example + Status: &status, + Provider: &provider, // Defaulting to Stripe for this example } p := query.Payment diff --git a/internal/api/video/handler.go b/internal/api/video/handler.go index 4997e0a..c4109c7 100644 --- a/internal/api/video/handler.go +++ b/internal/api/video/handler.go @@ -86,21 +86,41 @@ func (h *Handler) CreateVideo(c *gin.Context) { userID := c.GetString("userID") + status := "PUBLIC" + storageType := "S3" + video := &model.Video{ ID: uuid.New().String(), UserID: userID, + Name: req.Title, Title: req.Title, - Description: req.Description, + Description: &req.Description, URL: req.URL, Size: req.Size, Duration: req.Duration, Format: req.Format, - Status: "PUBLIC", - StorageType: "S3", + Status: &status, + StorageType: &storageType, } - v := query.Video - if err := v.WithContext(c.Request.Context()).Create(video); err != nil { + q := query.Q + err := q.Transaction(func(tx *query.Query) error { + if err := tx.Video.WithContext(c.Request.Context()).Create(video); err != nil { + return err + } + + // Atomic update: StorageUsed = StorageUsed + video.Size + // We use UpdateSimple with Add to ensure atomicity at database level: UPDATE users SET storage_used = storage_used + ? + if _, err := tx.User.WithContext(c.Request.Context()). + Where(tx.User.ID.Eq(userID)). + UpdateSimple(tx.User.StorageUsed.Add(video.Size)); err != nil { + return err + } + + return nil + }) + + if err != nil { h.logger.Error("Failed to create video record", "error", err) response.Error(c, http.StatusInternalServerError, "Failed to create video") return @@ -156,6 +176,12 @@ func (h *Handler) ListVideos(c *gin.Context) { func (h *Handler) GetVideo(c *gin.Context) { id := c.Param("id") v := query.Video + + // Atomically increment views: UPDATE videos SET views = views + 1 WHERE id = ? + // We intentionally ignore errors here (like record not found) because the subsequent fetch will handle 404s, + // and we don't want to fail the read if writing the view count fails for some transient reason. + v.WithContext(c.Request.Context()).Where(v.ID.Eq(id)).UpdateSimple(v.Views.Add(1)) + video, err := v.WithContext(c.Request.Context()).Where(v.ID.Eq(id)).First() if err != nil { response.Error(c, http.StatusNotFound, "Video not found") diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 886f467..f3b0140 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -119,7 +119,7 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc { // If we want new Access we can regenerate pair and update tokens. // Updating refresh token extends session (Slide expiration). - newTd, err := m.token.GenerateTokenPair(userID, userObj.Email, userObj.Role) + newTd, err := m.token.GenerateTokenPair(userID, userObj.Email, *userObj.Role) if err == nil { // Delete old refresh token from Redis? m.cache.Del(ctx, redisKey) @@ -140,7 +140,7 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc { return } - if strings.ToLower(user.Role) == "block" { + if user.Role != nil && strings.ToLower(*user.Role) == "block" { response.Error(c, http.StatusForbidden, "Forbidden: User is blocked") return }