refactor: Update data models to use pointer fields for optional values and add atomic database operations for video views and user storage.

This commit is contained in:
2026-01-22 18:02:45 +07:00
parent acd0be8fa1
commit ea2edbb9e0
4 changed files with 54 additions and 22 deletions

View File

@@ -60,17 +60,17 @@ func (h *handler) Login(c *gin.Context) {
} }
// Verify password (if user has password, google users might not) // 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") response.Error(c, http.StatusUnauthorized, "Please login with Google")
return 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") response.Error(c, http.StatusUnauthorized, "Invalid credentials")
return 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}) response.Success(c, gin.H{"user": user})
} }
@@ -111,12 +111,14 @@ func (h *handler) Register(c *gin.Context) {
return return
} }
password := string(hashedPassword)
role := "USER"
newUser := &model.User{ newUser := &model.User{
ID: uuid.New().String(), ID: uuid.New().String(),
Email: req.Email, Email: req.Email,
Password: string(hashedPassword), Password: &password,
Username: req.Username, Username: &req.Username,
Role: "USER", Role: &role,
} }
if err := u.WithContext(c.Request.Context()).Create(newUser); err != nil { 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 u := query.User
user, err := u.WithContext(c.Request.Context()).Where(u.Email.Eq(googleUser.Email)).First() user, err := u.WithContext(c.Request.Context()).Where(u.Email.Eq(googleUser.Email)).First()
if err != nil { if err != nil {
role := "USER"
user = &model.User{ user = &model.User{
ID: uuid.New().String(), ID: uuid.New().String(),
Email: googleUser.Email, Email: googleUser.Email,
Username: googleUser.Name, Username: &googleUser.Name,
GoogleID: googleUser.ID, GoogleID: &googleUser.ID,
Avatar: googleUser.Picture, Avatar: &googleUser.Picture,
Role: "USER", Role: &role,
} }
if err := u.WithContext(c.Request.Context()).Create(user); err != nil { if err := u.WithContext(c.Request.Context()).Create(user); err != nil {
response.Fail(c, "Failed to create user") response.Fail(c, "Failed to create user")
return 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) 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}) response.Success(c, gin.H{"user": user})
} }

View File

@@ -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 // In a real scenario, we would contact Stripe/PayPal here to create a session
// For now, we just create a "PENDING" payment record. // For now, we just create a "PENDING" payment record.
status := "PENDING"
provider := "STRIPE"
payment := &model.Payment{ payment := &model.Payment{
ID: uuid.New().String(), ID: uuid.New().String(),
UserID: userID, UserID: userID,
PlanID: req.PlanID, PlanID: &req.PlanID,
Amount: req.Amount, Amount: req.Amount,
Status: "PENDING", Status: &status,
Provider: "STRIPE", // Defaulting to Stripe for this example Provider: &provider, // Defaulting to Stripe for this example
} }
p := query.Payment p := query.Payment

View File

@@ -86,21 +86,41 @@ func (h *Handler) CreateVideo(c *gin.Context) {
userID := c.GetString("userID") userID := c.GetString("userID")
status := "PUBLIC"
storageType := "S3"
video := &model.Video{ video := &model.Video{
ID: uuid.New().String(), ID: uuid.New().String(),
UserID: userID, UserID: userID,
Name: req.Title,
Title: req.Title, Title: req.Title,
Description: req.Description, Description: &req.Description,
URL: req.URL, URL: req.URL,
Size: req.Size, Size: req.Size,
Duration: req.Duration, Duration: req.Duration,
Format: req.Format, Format: req.Format,
Status: "PUBLIC", Status: &status,
StorageType: "S3", StorageType: &storageType,
} }
v := query.Video q := query.Q
if err := v.WithContext(c.Request.Context()).Create(video); err != nil { 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) h.logger.Error("Failed to create video record", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create video") response.Error(c, http.StatusInternalServerError, "Failed to create video")
return return
@@ -156,6 +176,12 @@ func (h *Handler) ListVideos(c *gin.Context) {
func (h *Handler) GetVideo(c *gin.Context) { func (h *Handler) GetVideo(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
v := query.Video 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() video, err := v.WithContext(c.Request.Context()).Where(v.ID.Eq(id)).First()
if err != nil { if err != nil {
response.Error(c, http.StatusNotFound, "Video not found") response.Error(c, http.StatusNotFound, "Video not found")

View File

@@ -119,7 +119,7 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
// If we want new Access we can regenerate pair and update tokens. // If we want new Access we can regenerate pair and update tokens.
// Updating refresh token extends session (Slide expiration). // 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 { if err == nil {
// Delete old refresh token from Redis? // Delete old refresh token from Redis?
m.cache.Del(ctx, redisKey) m.cache.Del(ctx, redisKey)
@@ -140,7 +140,7 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return 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") response.Error(c, http.StatusForbidden, "Forbidden: User is blocked")
return return
} }