package handler import ( "encoding/json" "errors" "net/http" "strings" "github.com/prabhupadasays/spsays-bff/internal/middleware" "github.com/prabhupadasays/spsays-bff/internal/repository" "github.com/prabhupadasays/spsays-bff/internal/service" "github.com/prabhupadasays/spsays-bff/pkg/response" ) // UserHandler exposes profile-edit + avatar-presign endpoints. // All routes here sit behind middleware.RequireAuth. type UserHandler struct { svc *service.UserService } func NewUserHandler(svc *service.UserService) *UserHandler { return &UserHandler{svc: svc} } // updateProfileReq accepts partial updates. Use pointers so the JSON decoder // can tell "client explicitly sent null" from "field not present" — the // service treats nil-name as "leave alone" and ""-name as an error. type updateProfileReq struct { Name *string `json:"name"` AvatarURL *string `json:"avatar_url"` Handle *string `json:"handle"` } // UpdateProfile patches name, avatar, and/or handle. func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { uid := middleware.UserID(r.Context()) var req updateProfileReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body") return } u, err := h.svc.UpdateProfile(r.Context(), uid, req.Name, req.AvatarURL, req.Handle) if err != nil { if errors.Is(err, repository.ErrHandleTaken) { response.Error(w, http.StatusConflict, "that handle is already in use") return } response.Error(w, http.StatusBadRequest, err.Error()) return } response.JSON(w, http.StatusOK, u) } type presignReq struct { ContentType string `json:"content_type"` } type presignResp struct { UploadURL string `json:"upload_url"` PublicURL string `json:"public_url"` } // AvatarPresign returns a one-shot R2 PUT URL the browser uses to upload. // The frontend then PATCHes /users/me with the returned public_url to // persist the new avatar on the user row. func (h *UserHandler) AvatarPresign(w http.ResponseWriter, r *http.Request) { uid := middleware.UserID(r.Context()) var req presignReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body") return } ct := strings.TrimSpace(req.ContentType) if ct == "" { response.Error(w, http.StatusBadRequest, "content_type is required") return } upload, public, err := h.svc.AvatarPresign(r.Context(), uid, ct) if err != nil { // Differentiate "feature off" from "bad content type" so the frontend // can show a useful message. if strings.Contains(err.Error(), "not configured") { response.Error(w, http.StatusServiceUnavailable, err.Error()) return } response.Error(w, http.StatusBadRequest, err.Error()) return } response.JSON(w, http.StatusOK, presignResp{UploadURL: upload, PublicURL: public}) }