140 lines
4.7 KiB
Go
140 lines
4.7 KiB
Go
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)
|
|
}
|
|
|
|
// GetReadingPrefs returns the caller's saved reading prefs blob (always
|
|
// well-formed JSON; an empty object when the user has never written).
|
|
// The frontend treats the response as the source of truth on first
|
|
// sign-in for this device.
|
|
func (h *UserHandler) GetReadingPrefs(w http.ResponseWriter, r *http.Request) {
|
|
uid := middleware.UserID(r.Context())
|
|
raw, err := h.svc.GetReadingPrefs(r.Context(), uid)
|
|
if err != nil {
|
|
response.Error(w, http.StatusInternalServerError, "could not load reading prefs")
|
|
return
|
|
}
|
|
// Round-trip the JSONB as the data field directly. We use a raw
|
|
// json.RawMessage so the body is `{"data": {...}}` not a re-encoded
|
|
// string. The envelope wrapper keeps shape parity with the rest of
|
|
// the API.
|
|
response.JSON(w, http.StatusOK, json.RawMessage(raw))
|
|
}
|
|
|
|
// UpdateReadingPrefs replaces the caller's reading-prefs blob with the
|
|
// request body (which must itself be a JSON object — we re-marshal a
|
|
// decoded map to enforce that without imposing a fixed schema on a
|
|
// payload the frontend owns).
|
|
func (h *UserHandler) UpdateReadingPrefs(w http.ResponseWriter, r *http.Request) {
|
|
uid := middleware.UserID(r.Context())
|
|
|
|
// Decode into map[string]any to guarantee the body is an object (not
|
|
// an array, number, or string). Re-marshal before persisting so the
|
|
// stored JSON is canonicalised and any duplicate keys are dropped.
|
|
var body map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body (expect an object)")
|
|
return
|
|
}
|
|
prefsJSON, err := json.Marshal(body)
|
|
if err != nil {
|
|
response.Error(w, http.StatusBadRequest, "could not re-encode prefs")
|
|
return
|
|
}
|
|
if err := h.svc.UpdateReadingPrefs(r.Context(), uid, prefsJSON); err != nil {
|
|
response.Error(w, http.StatusInternalServerError, "could not save reading prefs")
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, json.RawMessage(prefsJSON))
|
|
}
|
|
|
|
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})
|
|
}
|