backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.
The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
146 lines
4 KiB
Go
146 lines
4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"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 {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid room ID"))
|
|
return
|
|
}
|
|
|
|
if !h.permissions.CanRead(c.Request.Context(), userID, roomID) {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("Not allowed to access this conversation"))
|
|
return
|
|
}
|
|
|
|
fileHeader, err := c.FormFile("file")
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("No file provided"))
|
|
return
|
|
}
|
|
|
|
if fileHeader.Size > chatAttachmentMaxSize {
|
|
RespondWithAppError(c, apperrors.NewValidationError("File too large. Max 50MB"))
|
|
return
|
|
}
|
|
|
|
if fileHeader.Size == 0 {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Empty file"))
|
|
return
|
|
}
|
|
|
|
fileType := h.validator.GetFileTypeFromPath(fileHeader.Filename)
|
|
if fileType == "unknown" {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Unsupported file type. Allowed: mp3, wav, ogg, jpg, png, pdf"))
|
|
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))
|
|
RespondWithAppError(c, apperrors.NewValidationError("File validation failed: "+err.Error()))
|
|
return
|
|
}
|
|
|
|
if !validationResult.Valid {
|
|
RespondWithAppError(c, apperrors.NewValidationError(validationResult.Error))
|
|
return
|
|
}
|
|
|
|
if validationResult.Quarantined {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("File rejected: virus detected"))
|
|
return
|
|
}
|
|
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to read file", err))
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to read file", err))
|
|
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))
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to upload file", err))
|
|
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,
|
|
})
|
|
}
|