284 lines
8.0 KiB
Go
284 lines
8.0 KiB
Go
// internal/controllers/upload_controller.go
|
|
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"lost-and-found/internal/models"
|
|
"lost-and-found/internal/utils"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ✅ KRITERIA BACKEND: File Handling (5 Poin)
|
|
// - Validasi MIME type
|
|
// - Validasi Max size
|
|
// - Multiple file support
|
|
// - Secure filename handling
|
|
|
|
type UploadController struct {
|
|
imageHandler *utils.ImageHandler
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewUploadController(db *gorm.DB) *UploadController {
|
|
return &UploadController{
|
|
imageHandler: utils.NewImageHandler("./uploads"),
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// UploadItemImage uploads image untuk item
|
|
// POST /api/upload/item-image
|
|
func (c *UploadController) UploadItemImage(ctx *gin.Context) {
|
|
userObj, _ := ctx.Get("user")
|
|
user := userObj.(*models.User)
|
|
|
|
// ✅ VALIDASI: Get file from request
|
|
file, err := ctx.FormFile("image")
|
|
if err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "No file uploaded", err.Error())
|
|
return
|
|
}
|
|
|
|
// ✅ VALIDASI: Check MIME type (dilakukan di ImageHandler)
|
|
// ✅ VALIDASI: Check file size (dilakukan di ImageHandler)
|
|
|
|
// Upload dengan processing (resize, optimize)
|
|
relativePath, err := c.imageHandler.UploadImage(file, "items")
|
|
if err != nil {
|
|
// Error dari ImageHandler sudah mencakup validasi MIME type & size
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Upload failed", err.Error())
|
|
return
|
|
}
|
|
|
|
// Generate URL
|
|
imageURL := fmt.Sprintf("/uploads/%s", relativePath)
|
|
|
|
// Log audit
|
|
c.logAudit(user.ID, "upload", "item_image", nil,
|
|
fmt.Sprintf("Image uploaded: %s", file.Filename),
|
|
ctx.ClientIP(), ctx.Request.UserAgent())
|
|
|
|
utils.SuccessResponse(ctx, http.StatusOK, "Image uploaded successfully", gin.H{
|
|
"url": imageURL,
|
|
"filename": filepath.Base(relativePath),
|
|
"size": file.Size,
|
|
})
|
|
}
|
|
|
|
// UploadClaimProof uploads bukti untuk claim
|
|
// POST /api/upload/claim-proof
|
|
func (c *UploadController) UploadClaimProof(ctx *gin.Context) {
|
|
userObj, _ := ctx.Get("user")
|
|
user := userObj.(*models.User)
|
|
|
|
file, err := ctx.FormFile("proof")
|
|
if err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "No file uploaded", err.Error())
|
|
return
|
|
}
|
|
|
|
// ✅ VALIDASI TAMBAHAN: Check file extension
|
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
|
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf"}
|
|
|
|
isAllowed := false
|
|
for _, allowedExt := range allowedExts {
|
|
if ext == allowedExt {
|
|
isAllowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isAllowed {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest,
|
|
"Invalid file extension",
|
|
fmt.Sprintf("Allowed: %v", allowedExts))
|
|
return
|
|
}
|
|
|
|
// Upload simple (tanpa resize untuk PDF)
|
|
var relativePath string
|
|
if ext == ".pdf" {
|
|
relativePath, err = c.imageHandler.UploadImageSimple(file, "proofs")
|
|
} else {
|
|
relativePath, err = c.imageHandler.UploadImage(file, "proofs")
|
|
}
|
|
|
|
if err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Upload failed", err.Error())
|
|
return
|
|
}
|
|
|
|
proofURL := fmt.Sprintf("/uploads/%s", relativePath)
|
|
|
|
c.logAudit(user.ID, "upload", "claim_proof", nil,
|
|
fmt.Sprintf("Proof uploaded: %s", file.Filename),
|
|
ctx.ClientIP(), ctx.Request.UserAgent())
|
|
|
|
utils.SuccessResponse(ctx, http.StatusOK, "Proof uploaded successfully", gin.H{
|
|
"url": proofURL,
|
|
"filename": filepath.Base(relativePath),
|
|
"size": file.Size,
|
|
"type": ext,
|
|
})
|
|
}
|
|
|
|
// UploadMultipleImages uploads multiple images sekaligus
|
|
// POST /api/upload/multiple
|
|
func (c *UploadController) UploadMultipleImages(ctx *gin.Context) {
|
|
userObj, _ := ctx.Get("user")
|
|
user := userObj.(*models.User)
|
|
|
|
// ✅ VALIDASI: Parse multipart form
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Invalid form data", err.Error())
|
|
return
|
|
}
|
|
|
|
files := form.File["images"]
|
|
if len(files) == 0 {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "No files uploaded", "")
|
|
return
|
|
}
|
|
|
|
// ✅ VALIDASI: Max 5 files per request
|
|
const maxFiles = 5
|
|
if len(files) > maxFiles {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest,
|
|
fmt.Sprintf("Too many files. Max %d files allowed", maxFiles), "")
|
|
return
|
|
}
|
|
|
|
var uploadedFiles []gin.H
|
|
var failedFiles []gin.H
|
|
|
|
for _, file := range files {
|
|
// Upload each file
|
|
relativePath, err := c.imageHandler.UploadImage(file, "items")
|
|
|
|
if err != nil {
|
|
failedFiles = append(failedFiles, gin.H{
|
|
"filename": file.Filename,
|
|
"error": err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
uploadedFiles = append(uploadedFiles, gin.H{
|
|
"filename": filepath.Base(relativePath),
|
|
"url": fmt.Sprintf("/uploads/%s", relativePath),
|
|
"size": file.Size,
|
|
})
|
|
}
|
|
|
|
c.logAudit(user.ID, "upload", "multiple_images", nil,
|
|
fmt.Sprintf("Uploaded %d files, %d failed", len(uploadedFiles), len(failedFiles)),
|
|
ctx.ClientIP(), ctx.Request.UserAgent())
|
|
|
|
utils.SuccessResponse(ctx, http.StatusOK, "Upload completed", gin.H{
|
|
"uploaded": uploadedFiles,
|
|
"failed": failedFiles,
|
|
"total": len(files),
|
|
"success": len(uploadedFiles),
|
|
})
|
|
}
|
|
|
|
// DeleteImage deletes uploaded image
|
|
// DELETE /api/upload/delete
|
|
func (c *UploadController) DeleteImage(ctx *gin.Context) {
|
|
userObj, _ := ctx.Get("user")
|
|
user := userObj.(*models.User)
|
|
|
|
var req struct {
|
|
ImageURL string `json:"image_url" binding:"required"`
|
|
}
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Invalid request", err.Error())
|
|
return
|
|
}
|
|
|
|
// Extract relative path from URL
|
|
relativePath := strings.TrimPrefix(req.ImageURL, "/uploads/")
|
|
|
|
// ✅ SECURITY: Validate path (prevent directory traversal)
|
|
if strings.Contains(relativePath, "..") {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Invalid path", "Directory traversal detected")
|
|
return
|
|
}
|
|
|
|
// Delete file
|
|
if err := c.imageHandler.DeleteImage(relativePath); err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Delete failed", err.Error())
|
|
return
|
|
}
|
|
|
|
c.logAudit(user.ID, "delete", "image", nil,
|
|
fmt.Sprintf("Image deleted: %s", relativePath),
|
|
ctx.ClientIP(), ctx.Request.UserAgent())
|
|
|
|
utils.SuccessResponse(ctx, http.StatusOK, "Image deleted successfully", nil)
|
|
}
|
|
|
|
// GetImageInfo gets metadata tentang uploaded image
|
|
// GET /api/upload/info?url=/uploads/items/image.jpg
|
|
func (c *UploadController) GetImageInfo(ctx *gin.Context) {
|
|
imageURL := ctx.Query("url")
|
|
if imageURL == "" {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Image URL required", "")
|
|
return
|
|
}
|
|
|
|
relativePath := strings.TrimPrefix(imageURL, "/uploads/")
|
|
|
|
// Security check
|
|
if strings.Contains(relativePath, "..") {
|
|
utils.ErrorResponse(ctx, http.StatusBadRequest, "Invalid path", "")
|
|
return
|
|
}
|
|
|
|
fullPath := filepath.Join("./uploads", relativePath)
|
|
|
|
// Check if file exists
|
|
fileInfo, err := filepath.Abs(fullPath)
|
|
if err != nil {
|
|
utils.ErrorResponse(ctx, http.StatusNotFound, "File not found", err.Error())
|
|
return
|
|
}
|
|
|
|
utils.SuccessResponse(ctx, http.StatusOK, "Image info retrieved", gin.H{
|
|
"path": fileInfo,
|
|
"url": imageURL,
|
|
"filename": filepath.Base(relativePath),
|
|
"folder": filepath.Dir(relativePath),
|
|
})
|
|
}
|
|
|
|
// logAudit helper untuk logging
|
|
func (c *UploadController) logAudit(userID uint, action, entityType string, entityID *uint, details, ip, userAgent string) {
|
|
auditLog := &models.AuditLog{
|
|
UserID: &userID,
|
|
Action: action,
|
|
EntityType: entityType,
|
|
EntityID: entityID,
|
|
Details: details,
|
|
IPAddress: ip,
|
|
UserAgent: userAgent,
|
|
}
|
|
c.db.Create(auditLog)
|
|
}
|
|
|
|
// ✅ VALIDASI INFO
|
|
// Validasi yang diterapkan:
|
|
// 1. MIME type check (di ImageHandler.isAllowedType)
|
|
// 2. File size check (di ImageHandler dengan maxSize = 10MB)
|
|
// 3. File extension check (allowedExts)
|
|
// 4. Max files per request check (5 files)
|
|
// 5. Directory traversal prevention (.. check)
|
|
// 6. Image resize untuk optimize storage (di ImageHandler) |