veza/veza-backend-api/internal/handlers/avatar_handler.go

167 lines
5.3 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package handlers
import (
fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings Security fixes implemented: CRITICAL: - CRIT-001: IDOR on chat rooms — added IsRoomMember check before returning room data or message history (returns 404, not 403) - CRIT-002: play_count/like_count exposed publicly — changed JSON tags to "-" so they are never serialized in API responses HIGH: - HIGH-001: TOCTOU race on marketplace downloads — transaction + SELECT FOR UPDATE on GetDownloadURL - HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256) - HIGH-003: context.Background() bypass in user repository — full context propagation from handlers → services → repository (29 files) - HIGH-004: Race condition on promo codes — SELECT FOR UPDATE - HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE - HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default - HIGH-007: RGPD hard delete incomplete — added cleanup for sessions, settings, follows, notifications, audit_logs anonymization - HIGH-008: RTMP callback auth weak — fail-closed when unconfigured, header-only (no query param), constant-time compare - HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn and verifies IsHost before processing - HIGH-010: Moderator self-strike — added issuedBy != userID check MEDIUM: - MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand - MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256) Updated REMEDIATION_MATRIX: 14 findings marked ✅ CORRIGÉ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:40:53 +00:00
"context"
"mime/multipart"
"net/http"
2025-12-03 19:29:37 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/services"
)
// ImageServiceInterface defines the interface for image operations
// This allows for easier testing with mocks
type ImageServiceInterface interface {
ProcessAvatar(fileHeader *multipart.FileHeader) ([]byte, error)
GenerateS3Key(userID uuid.UUID) string
UploadToS3(data []byte, key string) (string, error)
DeleteFromS3(avatarURL string) error
}
// UserServiceInterfaceForAvatar defines the interface for user operations needed by avatar handler
// This allows for easier testing with mocks
type UserServiceInterfaceForAvatar interface {
fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings Security fixes implemented: CRITICAL: - CRIT-001: IDOR on chat rooms — added IsRoomMember check before returning room data or message history (returns 404, not 403) - CRIT-002: play_count/like_count exposed publicly — changed JSON tags to "-" so they are never serialized in API responses HIGH: - HIGH-001: TOCTOU race on marketplace downloads — transaction + SELECT FOR UPDATE on GetDownloadURL - HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256) - HIGH-003: context.Background() bypass in user repository — full context propagation from handlers → services → repository (29 files) - HIGH-004: Race condition on promo codes — SELECT FOR UPDATE - HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE - HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default - HIGH-007: RGPD hard delete incomplete — added cleanup for sessions, settings, follows, notifications, audit_logs anonymization - HIGH-008: RTMP callback auth weak — fail-closed when unconfigured, header-only (no query param), constant-time compare - HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn and verifies IsHost before processing - HIGH-010: Moderator self-strike — added issuedBy != userID check MEDIUM: - MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand - MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256) Updated REMEDIATION_MATRIX: 14 findings marked ✅ CORRIGÉ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:40:53 +00:00
GetByID(ctx context.Context, userID uuid.UUID) (*models.User, error)
UpdateAvatarURL(ctx context.Context, userID uuid.UUID, avatarURL string) error
}
2025-12-03 19:29:37 +00:00
// AvatarHandler handles avatar-related operations
type AvatarHandler struct {
imageService ImageServiceInterface
userService UserServiceInterfaceForAvatar
2025-12-03 19:29:37 +00:00
}
// NewAvatarHandler creates a new AvatarHandler instance
func NewAvatarHandler(imageService *services.ImageService, userService *services.UserService) *AvatarHandler {
return &AvatarHandler{
imageService: imageService,
userService: userService,
}
}
// NewAvatarHandlerWithInterface creates a new AvatarHandler with interfaces (for testing)
func NewAvatarHandlerWithInterface(imageService ImageServiceInterface, userService UserServiceInterfaceForAvatar) *AvatarHandler {
return &AvatarHandler{
imageService: imageService,
userService: userService,
}
}
2025-12-03 19:29:37 +00:00
// UploadAvatar handles avatar upload
// POST /api/v1/users/:userId/avatar
// BE-API-021: Implement avatar upload endpoint
2025-12-03 19:29:37 +00:00
// T0221: Validates user_id, file format/size, processes image, uploads to S3, and updates DB
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
userIDStr := c.Param("userId")
if userIDStr == "" {
userIDStr = c.Param("id")
}
2025-12-03 19:29:37 +00:00
userID, err := uuid.Parse(userIDStr)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
2025-12-03 19:29:37 +00:00
return
}
// Vérifier que l'utilisateur est authentifié
authenticatedUserID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Vérifier que l'utilisateur ne peut modifier que son propre avatar
2025-12-03 19:29:37 +00:00
if userID != authenticatedUserID {
RespondWithAppError(c, apperrors.NewForbiddenError("cannot update other user's avatar"))
2025-12-03 19:29:37 +00:00
return
}
// Récupérer le fichier
2025-12-03 19:29:37 +00:00
fileHeader, err := c.FormFile("avatar")
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("no file provided"))
2025-12-03 19:29:37 +00:00
return
}
// Valider et traiter l'image
2025-12-03 19:29:37 +00:00
resizedImage, err := h.imageService.ProcessAvatar(fileHeader)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
2025-12-03 19:29:37 +00:00
return
}
// Générer la clé S3
2025-12-03 19:29:37 +00:00
s3Key := h.imageService.GenerateS3Key(userID)
// Upload vers S3 (ou stockage local pour l'instant)
2025-12-03 19:29:37 +00:00
avatarURL, err := h.imageService.UploadToS3(resizedImage, s3Key)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to upload avatar", err))
2025-12-03 19:29:37 +00:00
return
}
// Mettre à jour l'URL de l'avatar dans la DB
fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings Security fixes implemented: CRITICAL: - CRIT-001: IDOR on chat rooms — added IsRoomMember check before returning room data or message history (returns 404, not 403) - CRIT-002: play_count/like_count exposed publicly — changed JSON tags to "-" so they are never serialized in API responses HIGH: - HIGH-001: TOCTOU race on marketplace downloads — transaction + SELECT FOR UPDATE on GetDownloadURL - HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256) - HIGH-003: context.Background() bypass in user repository — full context propagation from handlers → services → repository (29 files) - HIGH-004: Race condition on promo codes — SELECT FOR UPDATE - HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE - HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default - HIGH-007: RGPD hard delete incomplete — added cleanup for sessions, settings, follows, notifications, audit_logs anonymization - HIGH-008: RTMP callback auth weak — fail-closed when unconfigured, header-only (no query param), constant-time compare - HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn and verifies IsHost before processing - HIGH-010: Moderator self-strike — added issuedBy != userID check MEDIUM: - MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand - MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256) Updated REMEDIATION_MATRIX: 14 findings marked ✅ CORRIGÉ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:40:53 +00:00
if err := h.userService.UpdateAvatarURL(c.Request.Context(), userID, avatarURL); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update avatar", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"avatar_url": avatarURL})
2025-12-03 19:29:37 +00:00
}
// DeleteAvatar handles avatar deletion
// DELETE /api/v1/users/:userId/avatar
// BE-API-022: Implement avatar delete endpoint
2025-12-03 19:29:37 +00:00
// T0222: Validates user_id, deletes file from S3, and sets avatar_url to NULL in DB
func (h *AvatarHandler) DeleteAvatar(c *gin.Context) {
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
userIDStr := c.Param("userId")
if userIDStr == "" {
userIDStr = c.Param("id")
}
2025-12-03 19:29:37 +00:00
userID, err := uuid.Parse(userIDStr)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
2025-12-03 19:29:37 +00:00
return
}
// Vérifier que l'utilisateur est authentifié
authenticatedUserID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Vérifier que l'utilisateur ne peut supprimer que son propre avatar
2025-12-03 19:29:37 +00:00
if userID != authenticatedUserID {
RespondWithAppError(c, apperrors.NewForbiddenError("cannot delete other user's avatar"))
2025-12-03 19:29:37 +00:00
return
}
// Récupérer l'utilisateur actuel pour obtenir l'URL de l'avatar
fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings Security fixes implemented: CRITICAL: - CRIT-001: IDOR on chat rooms — added IsRoomMember check before returning room data or message history (returns 404, not 403) - CRIT-002: play_count/like_count exposed publicly — changed JSON tags to "-" so they are never serialized in API responses HIGH: - HIGH-001: TOCTOU race on marketplace downloads — transaction + SELECT FOR UPDATE on GetDownloadURL - HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256) - HIGH-003: context.Background() bypass in user repository — full context propagation from handlers → services → repository (29 files) - HIGH-004: Race condition on promo codes — SELECT FOR UPDATE - HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE - HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default - HIGH-007: RGPD hard delete incomplete — added cleanup for sessions, settings, follows, notifications, audit_logs anonymization - HIGH-008: RTMP callback auth weak — fail-closed when unconfigured, header-only (no query param), constant-time compare - HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn and verifies IsHost before processing - HIGH-010: Moderator self-strike — added issuedBy != userID check MEDIUM: - MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand - MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256) Updated REMEDIATION_MATRIX: 14 findings marked ✅ CORRIGÉ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:40:53 +00:00
user, err := h.userService.GetByID(c.Request.Context(), userID)
2025-12-03 19:29:37 +00:00
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
2025-12-03 19:29:37 +00:00
return
}
// Supprimer le fichier de S3 (ou stockage local) s'il existe
2025-12-03 19:29:37 +00:00
if user.Avatar != "" {
if err := h.imageService.DeleteFromS3(user.Avatar); err != nil {
// Logger l'erreur mais continuer (le fichier peut déjà être supprimé)
// En production, vous pourriez vouloir utiliser un logger ici
2025-12-03 19:29:37 +00:00
_ = err
}
}
// Mettre l'URL de l'avatar à une chaîne vide (NULL dans la DB)
fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings Security fixes implemented: CRITICAL: - CRIT-001: IDOR on chat rooms — added IsRoomMember check before returning room data or message history (returns 404, not 403) - CRIT-002: play_count/like_count exposed publicly — changed JSON tags to "-" so they are never serialized in API responses HIGH: - HIGH-001: TOCTOU race on marketplace downloads — transaction + SELECT FOR UPDATE on GetDownloadURL - HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256) - HIGH-003: context.Background() bypass in user repository — full context propagation from handlers → services → repository (29 files) - HIGH-004: Race condition on promo codes — SELECT FOR UPDATE - HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE - HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default - HIGH-007: RGPD hard delete incomplete — added cleanup for sessions, settings, follows, notifications, audit_logs anonymization - HIGH-008: RTMP callback auth weak — fail-closed when unconfigured, header-only (no query param), constant-time compare - HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn and verifies IsHost before processing - HIGH-010: Moderator self-strike — added issuedBy != userID check MEDIUM: - MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand - MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256) Updated REMEDIATION_MATRIX: 14 findings marked ✅ CORRIGÉ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:40:53 +00:00
if err := h.userService.UpdateAvatarURL(c.Request.Context(), userID, ""); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete avatar", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "avatar deleted"})
2025-12-03 19:29:37 +00:00
}