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

147 lines
4 KiB
Go
Raw Normal View History

2026-03-06 17:52:08 +00:00
package handlers
import (
"fmt"
"io"
"net/http"
"path/filepath"
2026-03-06 23:54:35 +00:00
apperrors "veza-backend-api/internal/errors"
2026-03-06 17:52:08 +00:00
"veza-backend-api/internal/services"
chatws "veza-backend-api/internal/websocket/chat"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
const chatAttachmentMaxSize = 50 * 1024 * 1024 // 50MB
// ChatAttachmentHandler handles chat file uploads (v0.9.7)
type ChatAttachmentHandler struct {
s3Service *services.S3StorageService
permissions *chatws.PermissionService
validator UploadValidatorInterface
logger *zap.Logger
urlExpirySec int64
}
// NewChatAttachmentHandler creates a new ChatAttachmentHandler
func NewChatAttachmentHandler(
s3Service *services.S3StorageService,
permissions *chatws.PermissionService,
validator UploadValidatorInterface,
logger *zap.Logger,
) *ChatAttachmentHandler {
if logger == nil {
logger = zap.NewNop()
}
return &ChatAttachmentHandler{
s3Service: s3Service,
permissions: permissions,
validator: validator,
logger: logger,
urlExpirySec: 86400, // 24h
}
}
// UploadChatAttachment handles POST /api/v1/chat/rooms/:roomId/attachments
// Accepts: audio (mp3, wav, ogg), image (jpg, png), PDF. Max 50MB.
func (h *ChatAttachmentHandler) UploadChatAttachment(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
roomID, err := uuid.Parse(c.Param("roomId"))
if err != nil {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("Invalid room ID"))
2026-03-06 17:52:08 +00:00
return
}
if !h.permissions.CanRead(c.Request.Context(), userID, roomID) {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("Not allowed to access this conversation"))
2026-03-06 17:52:08 +00:00
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("No file provided"))
2026-03-06 17:52:08 +00:00
return
}
if fileHeader.Size > chatAttachmentMaxSize {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("File too large. Max 50MB"))
2026-03-06 17:52:08 +00:00
return
}
if fileHeader.Size == 0 {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("Empty file"))
2026-03-06 17:52:08 +00:00
return
}
fileType := h.validator.GetFileTypeFromPath(fileHeader.Filename)
if fileType == "unknown" {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("Unsupported file type. Allowed: mp3, wav, ogg, jpg, png, pdf"))
2026-03-06 17:52:08 +00:00
return
}
validationResult, err := h.validator.ValidateFile(c.Request.Context(), fileHeader, fileType)
if err != nil {
h.logger.Warn("Chat attachment validation failed", zap.Error(err), zap.String("filename", fileHeader.Filename))
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError("File validation failed: "+err.Error()))
2026-03-06 17:52:08 +00:00
return
}
if !validationResult.Valid {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewValidationError(validationResult.Error))
2026-03-06 17:52:08 +00:00
return
}
if validationResult.Quarantined {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("File rejected: virus detected"))
2026-03-06 17:52:08 +00:00
return
}
file, err := fileHeader.Open()
if err != nil {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to read file", err))
2026-03-06 17:52:08 +00:00
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to read file", err))
2026-03-06 17:52:08 +00:00
return
}
ext := filepath.Ext(fileHeader.Filename)
if ext == "" {
ext = ".bin"
}
key := fmt.Sprintf("chat/%s/%s/%s%s", roomID, userID, uuid.New().String(), ext)
_, err = h.s3Service.UploadFile(c.Request.Context(), data, key, fileHeader.Header.Get("Content-Type"))
if err != nil {
h.logger.Error("Failed to upload chat attachment", zap.Error(err), zap.String("key", key))
2026-03-06 23:54:35 +00:00
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to upload file", err))
2026-03-06 17:52:08 +00:00
return
}
// Return presigned URL for download (24h)
signedURL, err := h.s3Service.GetPresignedURL(c.Request.Context(), key)
if err != nil {
// Fallback to public URL if presigned fails (e.g. public bucket)
signedURL = h.s3Service.GetPublicURL(key)
}
RespondSuccess(c, http.StatusCreated, gin.H{
"url": signedURL,
"file_id": uuid.New().String(),
"file_name": fileHeader.Filename,
"file_size": fileHeader.Size,
"file_type": fileType,
})
}