278 lines
8.5 KiB
Go
278 lines
8.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"veza-backend-api/internal/core/social"
|
|
"veza-backend-api/internal/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// SocialHandler gère les opérations sociales
|
|
type SocialHandler struct {
|
|
service social.SocialService
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewSocialHandler crée une nouvelle instance de SocialHandler
|
|
func NewSocialHandler(service social.SocialService, logger *zap.Logger) *SocialHandler {
|
|
return &SocialHandler{
|
|
service: service,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// NewSocialHandlerWithInterface crée une nouvelle instance de SocialHandler avec une interface (pour les tests)
|
|
func NewSocialHandlerWithInterface(service social.SocialService, logger *zap.Logger) *SocialHandler {
|
|
return &SocialHandler{
|
|
service: service,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// CreatePostRequest DTO pour la création de post
|
|
// GO-013: Validation améliorée avec tags go-validator
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type CreatePostRequest struct {
|
|
Content string `json:"content" binding:"required,min=1,max=5000" validate:"required,min=1,max=5000"`
|
|
Attachments map[string]string `json:"attachments" validate:"omitempty"` // track_id, playlist_id (UUID strings)
|
|
}
|
|
|
|
// CreatePost crée un post
|
|
// GO-013: Utilise validator centralisé pour validation améliorée
|
|
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
|
|
func (h *SocialHandler) CreatePost(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
var req CreatePostRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks
|
|
req.Content = utils.SanitizeText(req.Content, 5000)
|
|
|
|
attachments := make(map[string]uuid.UUID)
|
|
for k, v := range req.Attachments {
|
|
if id, err := uuid.Parse(v); err == nil {
|
|
attachments[k] = id
|
|
}
|
|
}
|
|
|
|
post, err := h.service.CreatePost(c.Request.Context(), userID, req.Content, attachments)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, post)
|
|
}
|
|
|
|
// ToggleLikeRequest DTO pour liker
|
|
// GO-013: Validation améliorée avec tags go-validator
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type ToggleLikeRequest struct {
|
|
TargetID string `json:"target_id" binding:"required,uuid" validate:"required,uuid"`
|
|
TargetType string `json:"target_type" binding:"required,oneof=post track playlist" validate:"required,oneof=post track playlist"`
|
|
}
|
|
|
|
// ToggleLike like ou unlike un objet
|
|
// GO-013: Utilise validator centralisé pour validation améliorée
|
|
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
|
|
func (h *SocialHandler) ToggleLike(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
var req ToggleLikeRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// UUID validation déjà fait par binding tag, mais on garde le parse pour compatibilité
|
|
targetID, err := uuid.Parse(req.TargetID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid target_id format"})
|
|
return
|
|
}
|
|
|
|
liked, err := h.service.ToggleLike(c.Request.Context(), userID, targetID, req.TargetType)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle like"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"liked": liked})
|
|
}
|
|
|
|
// AddCommentRequest DTO pour commenter
|
|
// GO-013: Validation améliorée avec tags go-validator
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type AddCommentRequest struct {
|
|
TargetID string `json:"target_id" binding:"required,uuid" validate:"required,uuid"`
|
|
TargetType string `json:"target_type" binding:"required,oneof=post track playlist" validate:"required,oneof=post track playlist"`
|
|
Content string `json:"content" binding:"required,min=1,max=2000" validate:"required,min=1,max=2000"`
|
|
}
|
|
|
|
// AddComment ajoute un commentaire
|
|
// GO-013: Utilise validator centralisé pour validation améliorée
|
|
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
|
|
func (h *SocialHandler) AddComment(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
var req AddCommentRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// UUID validation déjà fait par binding tag, mais on garde le parse pour compatibilité
|
|
targetID, err := uuid.Parse(req.TargetID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid target_id format"})
|
|
return
|
|
}
|
|
|
|
comment, err := h.service.AddComment(c.Request.Context(), userID, targetID, req.TargetType, req.Content)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add comment"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, comment)
|
|
}
|
|
|
|
// GetFeed récupère le feed global (S1.2, S1.4, S1.6: cursor, limit, type filter)
|
|
func (h *SocialHandler) GetFeed(c *gin.Context) {
|
|
feedType := c.DefaultQuery("type", "all") // all | following | groups
|
|
if feedType != "all" && feedType != "following" && feedType != "groups" {
|
|
feedType = "all"
|
|
}
|
|
|
|
limitParam := c.DefaultQuery("limit", "20")
|
|
limit := 20
|
|
if l, err := strconv.Atoi(limitParam); err == nil && l > 0 {
|
|
limit = l
|
|
if limit > 50 {
|
|
limit = 50
|
|
}
|
|
}
|
|
// cursor: timestamp or ID for pagination (S1.4) - for now we use offset via cursor
|
|
cursor := c.Query("cursor")
|
|
offset := 0
|
|
if cursor != "" {
|
|
// Simple cursor: if it's a number, use as offset; else ignore
|
|
if o, err := strconv.Atoi(cursor); err == nil && o >= 0 {
|
|
offset = o
|
|
}
|
|
}
|
|
|
|
var userID *uuid.UUID
|
|
if feedType == "following" {
|
|
if uid, ok := GetUserIDUUID(c); ok {
|
|
userID = &uid
|
|
} else {
|
|
feedType = "all" // Fallback if not authenticated
|
|
}
|
|
}
|
|
feed, err := h.service.GetGlobalFeed(c.Request.Context(), limit, offset, feedType, userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get feed"})
|
|
return
|
|
}
|
|
nextCursor := ""
|
|
if len(feed) >= limit {
|
|
nextCursor = strconv.Itoa(offset + limit)
|
|
}
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"items": feed,
|
|
"next_cursor": nextCursor,
|
|
})
|
|
}
|
|
|
|
// GetExplore returns trending hashtags and suggested users (S1.5)
|
|
func (h *SocialHandler) GetExplore(c *gin.Context) {
|
|
limitParam := c.DefaultQuery("limit", "10")
|
|
limit := 10
|
|
if l, err := strconv.Atoi(limitParam); err == nil && l > 0 {
|
|
limit = l
|
|
if limit > 50 {
|
|
limit = 50
|
|
}
|
|
}
|
|
tags, err := h.service.GetTrendingHashtags(c.Request.Context(), limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get explore data"})
|
|
return
|
|
}
|
|
// suggested_users: placeholder - can add users not followed with most followers
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"trending": tags,
|
|
"suggested_users": []interface{}{},
|
|
})
|
|
}
|
|
|
|
// GetTrending returns trending hashtags from recent posts (v0.203 Lot L)
|
|
func (h *SocialHandler) GetTrending(c *gin.Context) {
|
|
limitParam := c.DefaultQuery("limit", "10")
|
|
limit := 10
|
|
if l, err := strconv.Atoi(limitParam); err == nil && l > 0 {
|
|
limit = l
|
|
if limit > 50 {
|
|
limit = 50
|
|
}
|
|
}
|
|
|
|
tags, err := h.service.GetTrendingHashtags(c.Request.Context(), limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get trending"})
|
|
return
|
|
}
|
|
RespondSuccess(c, http.StatusOK, gin.H{"tags": tags})
|
|
}
|
|
|
|
// GetPostsByUser récupère les posts d'un utilisateur spécifique
|
|
func (h *SocialHandler) GetPostsByUser(c *gin.Context) {
|
|
userIDStr := c.Param("user_id")
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
|
return
|
|
}
|
|
|
|
pageParam := c.DefaultQuery("page", "1")
|
|
limitParam := c.DefaultQuery("limit", "20")
|
|
|
|
page, err := strconv.Atoi(pageParam)
|
|
if err != nil || page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
limit, err := strconv.Atoi(limitParam)
|
|
if err != nil || limit < 1 {
|
|
limit = 20
|
|
}
|
|
|
|
offset := (page - 1) * limit
|
|
|
|
posts, err := h.service.GetPostsByUser(c.Request.Context(), userID, limit, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get posts"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, posts)
|
|
}
|