- POST /api/v1/reviews (one per chat_id, UNIQUE) - GET /api/v1/users/:id/reviews (public list) - GET /api/v1/users/:id/stats (rating_avg, count) - migration 0004: reviews (rating 1-5, anonymous flag), user_stats aggregate - only chat participants can review; reviewed_id is other party
324 lines
8.6 KiB
Go
324 lines
8.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"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/config"
|
|
"github.com/buhapp/backend/internal/consent"
|
|
"github.com/buhapp/backend/internal/locations"
|
|
"github.com/buhapp/backend/internal/preferences"
|
|
"github.com/buhapp/backend/internal/users"
|
|
)
|
|
|
|
type Handlers struct {
|
|
Cfg *config.Config
|
|
Users *users.Repo
|
|
Prefs *preferences.Repo
|
|
Locs *locations.Repo
|
|
Consent *consent.Repo
|
|
Audit *audit.Repo
|
|
Auth *auth.Service
|
|
}
|
|
|
|
func New(cfg *config.Config, u *users.Repo, p *preferences.Repo, l *locations.Repo, c *consent.Repo, a *audit.Repo, au *auth.Service) *Handlers {
|
|
return &Handlers{Cfg: cfg, Users: u, Prefs: p, Locs: l, Consent: c, Audit: a, Auth: au}
|
|
}
|
|
|
|
func (h *Handlers) GetMe(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
u, err := h.Users.GetByID(c.UserContext(), uid)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
if u == nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
|
}
|
|
|
|
p, _ := h.Prefs.Get(c.UserContext(), uid)
|
|
return c.JSON(fiber.Map{
|
|
"user": u,
|
|
"prefs": p,
|
|
})
|
|
}
|
|
|
|
type updateMeRequest struct {
|
|
Name *string `json:"name"`
|
|
City *string `json:"city"`
|
|
Bio *string `json:"bio"`
|
|
Gender *string `json:"gender"`
|
|
PhotoURL *string `json:"photo_url"`
|
|
}
|
|
|
|
func (h *Handlers) UpdateMe(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
var req updateMeRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
|
}
|
|
|
|
// Соберём UPDATE динамически
|
|
sets := []string{}
|
|
args := []interface{}{}
|
|
idx := 1
|
|
|
|
add := func(col string, val interface{}) {
|
|
sets = append(sets, col+"=$"+itoa(idx))
|
|
args = append(args, val)
|
|
idx++
|
|
}
|
|
if req.Name != nil {
|
|
n := strings.TrimSpace(*req.Name)
|
|
if n == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name cannot be empty"})
|
|
}
|
|
add("name", n)
|
|
}
|
|
if req.City != nil {
|
|
add("city", strings.TrimSpace(*req.City))
|
|
}
|
|
if req.Bio != nil {
|
|
if len(*req.Bio) > 1000 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bio too long (max 1000)"})
|
|
}
|
|
add("bio", strings.TrimSpace(*req.Bio))
|
|
}
|
|
if req.Gender != nil {
|
|
g := *req.Gender
|
|
if g != "m" && g != "f" && g != "o" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "gender must be m, f, o"})
|
|
}
|
|
add("gender", g)
|
|
}
|
|
if req.PhotoURL != nil {
|
|
add("photo_url", strings.TrimSpace(*req.PhotoURL))
|
|
}
|
|
|
|
if len(sets) == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nothing to update"})
|
|
}
|
|
|
|
args = append(args, uid)
|
|
sql := "UPDATE users SET "
|
|
for i, s := range sets {
|
|
if i > 0 {
|
|
sql += ", "
|
|
}
|
|
sql += s
|
|
}
|
|
sql += ", updated_at=NOW() WHERE id=$" + itoa(idx)
|
|
|
|
if _, err := h.Users.UpdateRaw(c.UserContext(), sql, args...); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
|
|
_ = h.Audit.Log(c.UserContext(), &audit.Event{
|
|
UserID: &uid, Action: "user.update",
|
|
IP: c.IP(), UserAgent: c.Get("User-Agent"),
|
|
})
|
|
|
|
u, _ := h.Users.GetByID(c.UserContext(), uid)
|
|
return c.JSON(u)
|
|
}
|
|
|
|
type prefsRequest struct {
|
|
Drinks []string `json:"drinks"`
|
|
Activities []string `json:"activities"`
|
|
Purposes []string `json:"purposes"`
|
|
Language string `json:"language"`
|
|
}
|
|
|
|
func (h *Handlers) GetPrefs(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
p, err := h.Prefs.Get(c.UserContext(), uid)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
return c.JSON(p)
|
|
}
|
|
|
|
func (h *Handlers) UpdatePrefs(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
var req prefsRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
|
}
|
|
p := &preferences.Prefs{
|
|
UserID: uid,
|
|
Drinks: h.Prefs.Validate(req.Drinks, preferences.AllowedDrinks),
|
|
Activities: h.Prefs.Validate(req.Activities, preferences.AllowedActivities),
|
|
Purposes: h.Prefs.Validate(req.Purposes, preferences.AllowedPurposes),
|
|
}
|
|
if req.Language != "" {
|
|
p.Language = strings.ToLower(strings.TrimSpace(req.Language))
|
|
}
|
|
if err := h.Prefs.Upsert(c.UserContext(), p); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
return c.JSON(p)
|
|
}
|
|
|
|
type locationRequest struct {
|
|
Lat float64 `json:"lat"`
|
|
Lng float64 `json:"lng"`
|
|
Visible *bool `json:"visible"`
|
|
}
|
|
|
|
func (h *Handlers) UpdateLocation(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
var req locationRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
|
}
|
|
if req.Lat < -90 || req.Lat > 90 || req.Lng < -180 || req.Lng > 180 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid coordinates"})
|
|
}
|
|
visible := true
|
|
if req.Visible != nil {
|
|
visible = *req.Visible
|
|
}
|
|
loc := &locations.Location{
|
|
UserID: uid,
|
|
Lat: req.Lat,
|
|
Lng: req.Lng,
|
|
Visible: visible,
|
|
}
|
|
if err := h.Locs.Upsert(c.UserContext(), loc); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"lat": loc.Lat, "lng": loc.Lng, "visible": loc.Visible,
|
|
"updated_at": loc.UpdatedAt,
|
|
"note": "coordinates rounded to ~300m for privacy",
|
|
})
|
|
}
|
|
|
|
type visibilityRequest struct {
|
|
Visible bool `json:"visible"`
|
|
}
|
|
|
|
func (h *Handlers) SetVisibility(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
var req visibilityRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
|
}
|
|
if err := h.Locs.SetVisible(c.UserContext(), uid, req.Visible); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
return c.JSON(fiber.Map{"visible": req.Visible})
|
|
}
|
|
|
|
func (h *Handlers) SearchNearby(c *fiber.Ctx) error {
|
|
uid, err := userID(c)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
|
|
}
|
|
lat := c.QueryFloat("lat", 0)
|
|
lng := c.QueryFloat("lng", 0)
|
|
radius := c.QueryFloat("radius", 5)
|
|
if lat == 0 || lng == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "lat & lng required"})
|
|
}
|
|
if radius < 0.1 || radius > 100 {
|
|
radius = 5
|
|
}
|
|
results, err := h.Locs.Nearby(c.UserContext(), uid, lat, lng, radius, 200)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"count": len(results),
|
|
"results": results,
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) GetUserPublic(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad id"})
|
|
}
|
|
u, err := h.Users.GetByID(c.UserContext(), id)
|
|
if err != nil || u == nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
|
}
|
|
p, _ := h.Prefs.Get(c.UserContext(), id)
|
|
// Скрываем email/phone в публичном профиле
|
|
return c.JSON(fiber.Map{
|
|
"id": u.ID,
|
|
"name": u.Name,
|
|
"city": u.City,
|
|
"gender": u.Gender,
|
|
"age": age(u.Birthdate),
|
|
"bio": u.Bio,
|
|
"photo": u.PhotoURL,
|
|
"verified": u.IsVerified,
|
|
"prefs": p,
|
|
})
|
|
}
|
|
|
|
// ----------------- helpers -----------------
|
|
|
|
func userID(c *fiber.Ctx) (uuid.UUID, error) {
|
|
v, err := auth.UserID(c)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
return uuid.Parse(v)
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
digits := []byte{}
|
|
for n > 0 {
|
|
digits = append([]byte{byte('0' + n%10)}, digits...)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
return "-" + string(digits)
|
|
}
|
|
return string(digits)
|
|
}
|
|
|
|
func age(birth *time.Time) *int {
|
|
if birth == nil {
|
|
return nil
|
|
}
|
|
now := time.Now()
|
|
a := now.Year() - birth.Year()
|
|
if now.Month() < birth.Month() || (now.Month() == birth.Month() && now.Day() < birth.Day()) {
|
|
a--
|
|
}
|
|
return &a
|
|
}
|