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 }