- POST /api/v1/me/photo (multipart, field 'photo')
- MinIO upload via storage.UploadAvatar (avatars/{userID}/{uuid}-{ts})
- /storage/* backend route reads from MinIO + serves with proper headers
- nginx unchanged: /storage/* proxied to backend like everything else
- Bumped to v0.5.0
77 lines
2.5 KiB
Go
77 lines
2.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/buhapp/backend/internal/audit"
|
|
"github.com/buhapp/backend/internal/auth"
|
|
"github.com/buhapp/backend/internal/storage"
|
|
"github.com/buhapp/backend/internal/users"
|
|
)
|
|
|
|
type PhotoHandlers struct {
|
|
Users *users.Repo
|
|
Storage *storage.Storage
|
|
Audit *audit.Repo
|
|
}
|
|
|
|
func NewPhotoHandlers(u *users.Repo, s *storage.Storage, au *audit.Repo) *PhotoHandlers {
|
|
return &PhotoHandlers{Users: u, Storage: s, Audit: au}
|
|
}
|
|
|
|
const maxPhotoSize = 8 * 1024 * 1024 // 8 MB
|
|
|
|
var allowedPhotoTypes = map[string]bool{
|
|
"image/jpeg": true,
|
|
"image/jpg": true,
|
|
"image/png": true,
|
|
"image/webp": true,
|
|
}
|
|
|
|
// UploadPhoto — POST /me/photo (multipart, поле "photo")
|
|
func (h *PhotoHandlers) UploadPhoto(c *fiber.Ctx) error {
|
|
uid, err := auth.UserID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
uuidUser, err := uuid.Parse(uid)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "bad user id"})
|
|
}
|
|
if h.Storage == nil {
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "storage not configured"})
|
|
}
|
|
file, err := c.FormFile("photo")
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "photo field required"})
|
|
}
|
|
if file.Size > maxPhotoSize {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "file too large (max 8MB)"})
|
|
}
|
|
contentType := file.Header.Get("Content-Type")
|
|
if !allowedPhotoTypes[contentType] {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid image type"})
|
|
}
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "open failed"})
|
|
}
|
|
defer src.Close()
|
|
|
|
url, err := h.Storage.UploadAvatar(c.UserContext(), uuidUser, contentType, src, file.Size)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "upload failed: " + err.Error()})
|
|
}
|
|
// обновим photo_url в users
|
|
sql := `UPDATE users SET photo_url=$1, updated_at=NOW() WHERE id=$2`
|
|
if _, err := h.Users.UpdateRaw(c.UserContext(), sql, url, uuidUser); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db update failed"})
|
|
}
|
|
_ = h.Audit.Log(c.UserContext(), &audit.Event{
|
|
UserID: &uuidUser, Action: "user.photo_upload",
|
|
IP: c.IP(), UserAgent: c.Get("User-Agent"),
|
|
})
|
|
return c.JSON(fiber.Map{"ok": true, "photo_url": url})
|
|
}
|