| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- // Package handlers
- package handlers
- import (
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "github.com/go-chi/chi/v5"
- "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware"
- "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
- "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/services"
- "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/validator"
- )
- type PlaceHandler struct {
- placeSvc *services.PlaceService
- }
- func NewPlaceHandler(placeSvc *services.PlaceService) *PlaceHandler {
- return &PlaceHandler{placeSvc: placeSvc}
- }
- func (h *PlaceHandler) List(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query()
- filter := models.PlaceFilter{
- Type: q.Get("type"),
- Status: "published",
- }
- if tags := q["tags"]; len(tags) > 0 {
- filter.TagIDs = tags
- }
- if features := q["features"]; len(features) > 0 {
- filter.FeatureIDs = features
- }
- if mr := q.Get("min_rating"); mr != "" {
- if v, err := strconv.ParseFloat(mr, 64); err == nil {
- filter.MinRating = v
- }
- }
- if pmin := q.Get("price_min"); pmin != "" {
- if v, err := strconv.Atoi(pmin); err == nil {
- filter.PriceMin = &v
- }
- }
- if pmax := q.Get("price_max"); pmax != "" {
- if v, err := strconv.Atoi(pmax); err == nil {
- filter.PriceMax = &v
- }
- }
- if bounds := q.Get("bounds"); bounds != "" {
- var b models.Bounds
- if _, err := fmt.Sscanf(bounds, "%f,%f,%f,%f", &b.SWLat, &b.SWLng, &b.NELat, &b.NELng); err == nil {
- filter.Bounds = &b
- }
- }
- if limit := q.Get("limit"); limit != "" {
- if v, err := strconv.Atoi(limit); err == nil {
- filter.Limit_ = v
- }
- }
- filter.Sort = q.Get("sort")
- if q.Get("user_lat") != "" && q.Get("user_lng") != "" {
- if lat, err := strconv.ParseFloat(q.Get("user_lat"), 64); err == nil {
- filter.UserLat = &lat
- }
- if lng, err := strconv.ParseFloat(q.Get("user_lng"), 64); err == nil {
- filter.UserLng = &lng
- }
- }
- // Include tags and features if requested (e.g., ?include=tags,features)
- if include := q.Get("include"); include != "" {
- for _, v := range strings.Split(include, ",") {
- if v == "tags" || v == "features" {
- filter.IncludeTagsFeatures = true
- break
- }
- }
- }
- if cursor := q.Get("cursor"); cursor != "" {
- id, createdAt, rating, err := services.DecodeCursor(cursor)
- if err != nil {
- writeError(w, http.StatusBadRequest, "invalid cursor", err)
- return
- }
- filter.Cursor = id
- filter.CursorCreatedAt = &createdAt
- if q.Get("sort") == "rating" {
- filter.CursorRating = &rating
- }
- }
- result, err := h.placeSvc.List(r.Context(), filter)
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to list places", err)
- return
- }
- if result.Data == nil {
- result.Data = []*models.Place{}
- }
- writeJSON(w, http.StatusOK, result)
- }
- func (h *PlaceHandler) GetByID(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- place, err := h.placeSvc.GetByID(r.Context(), id, true)
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to get place", err)
- return
- }
- if place == nil {
- writeError(w, http.StatusNotFound, "place not found", nil)
- return
- }
- writeJSON(w, http.StatusOK, place)
- }
- func (h *PlaceHandler) ListMy(w http.ResponseWriter, r *http.Request) {
- userID := middleware.GetUserID(r.Context())
- places, err := h.placeSvc.List(r.Context(), models.PlaceFilter{
- OwnerID: userID,
- IncludeTagsFeatures: true,
- })
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to list places", err)
- return
- }
- writeJSON(w, http.StatusOK, places)
- }
- type createPlaceRequest struct {
- Title string `json:"title" validate:"required,min=1,max=255"`
- Description *string `json:"description" validate:"omitempty,max=5000"`
- Address *string `json:"address" validate:"omitempty,max=500"`
- Lat float64 `json:"lat" validate:"required,latitude"`
- Lng float64 `json:"lng" validate:"required,longitude"`
- Type string `json:"type" validate:"required,place_type"`
- AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
- CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
- Tags []string `json:"tags" validate:"dive,slug,max=50"`
- Features []string `json:"features" validate:"dive,slug,max=50"`
- HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
- Currency string `json:"currency" validate:"omitempty,currency,len=3"`
- MinHours int `json:"min_hours" validate:"min=0,max=100"`
- }
- type updatePlaceRequest struct {
- Title *string `json:"title" validate:"omitempty,min=1,max=255"`
- Description *string `json:"description" validate:"omitempty,max=5000"`
- Address *string `json:"address" validate:"omitempty,max=500"`
- Lat *float64 `json:"lat" validate:"omitempty,latitude"`
- Lng *float64 `json:"lng" validate:"omitempty,longitude"`
- AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
- CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
- Tags []string `json:"tags" validate:"dive,slug,max=50"`
- Features []string `json:"features" validate:"dive,slug,max=50"`
- HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
- Currency *string `json:"currency" validate:"omitempty,currency,len=3"`
- MinHours *int `json:"min_hours" validate:"omitempty,min=0,max=100"`
- }
- type moderatePlaceRequest struct {
- Action string `json:"action" validate:"required,oneof=approve reject"`
- Comment *string `json:"comment" validate:"omitempty,max=1000"`
- }
- func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
- userID := middleware.GetUserID(r.Context())
- role := middleware.GetUserRole(r.Context())
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
- var req createPlaceRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeError(w, http.StatusBadRequest, "invalid request body", err)
- return
- }
- if err := validator.Validate(req); err != nil {
- writeValidationError(w, err)
- return
- }
- if req.Type == "studio" && role != "landlord" && role != "moderator" && role != "superadmin" {
- writeError(w, http.StatusForbidden, "only landlords can create studios", nil)
- return
- }
- if req.Type == "place" && role != "customer" && role != "moderator" && role != "superadmin" {
- writeError(w, http.StatusForbidden, "only customers can create places", nil)
- return
- }
- tags := make([]models.Tag, len(req.Tags))
- for i, t := range req.Tags {
- tags[i] = models.Tag{ID: t}
- }
- features := make([]models.Feature, len(req.Features))
- for i, f := range req.Features {
- features[i] = models.Feature{ID: f}
- }
- place, err := h.placeSvc.Create(r.Context(), services.CreatePlaceInput{
- OwnerID: userID,
- Type: req.Type,
- Title: req.Title,
- Description: req.Description,
- Address: req.Address,
- Lat: req.Lat,
- Lng: req.Lng,
- CoverImage: req.CoverImage,
- AccessInfo: req.AccessInfo,
- Tags: tags,
- Features: features,
- HourlyRate: req.HourlyRate,
- Currency: req.Currency,
- MinHours: req.MinHours,
- })
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to create place", err)
- return
- }
- writeJSON(w, http.StatusCreated, place)
- }
- func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- userID := middleware.GetUserID(r.Context())
- role := middleware.GetUserRole(r.Context())
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
- var req updatePlaceRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeError(w, http.StatusBadRequest, "invalid request body", err)
- return
- }
- if err := validator.Validate(req); err != nil {
- writeValidationError(w, err)
- return
- }
- isModerator := role == "moderator" || role == "superadmin"
- input := services.UpdatePlaceInput{
- ID: id,
- OwnerID: userID,
- }
- if req.Title != nil { input.Title = req.Title }
- input.Description = req.Description
- input.Address = req.Address
- input.Lat = req.Lat
- input.Lng = req.Lng
- input.AccessInfo = req.AccessInfo
- input.HourlyRate = req.HourlyRate
- input.Currency = req.Currency
- input.MinHours = req.MinHours
- place, err := h.placeSvc.Update(r.Context(), input, isModerator)
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to update place", err)
- return
- }
- if place == nil {
- writeError(w, http.StatusNotFound, "place not found", nil)
- return
- }
- writeJSON(w, http.StatusOK, place)
- }
- func (h *PlaceHandler) Moderate(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- userID := middleware.GetUserID(r.Context())
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
- var req moderatePlaceRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeError(w, http.StatusBadRequest, "invalid request body", err)
- return
- }
- if err := validator.Validate(req); err != nil {
- writeValidationError(w, err)
- return
- }
- comment := ""
- if req.Comment != nil {
- comment = *req.Comment
- }
- if err := h.placeSvc.Moderate(r.Context(), id, req.Action, comment, userID); err != nil {
- writeError(w, http.StatusInternalServerError, "failed to moderate place", err)
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
- }
- func (h *PlaceHandler) Delete(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- userID := middleware.GetUserID(r.Context())
- role := middleware.GetUserRole(r.Context())
- place, err := h.placeSvc.GetByID(r.Context(), id, false)
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to get place", err)
- return
- }
- if place == nil {
- writeError(w, http.StatusNotFound, "place not found", nil)
- return
- }
- if place.OwnerID != userID && role != "moderator" && role != "superadmin" {
- writeError(w, http.StatusForbidden, "not your place", nil)
- return
- }
- if err := h.placeSvc.Delete(r.Context(), id); err != nil {
- writeError(w, http.StatusInternalServerError, "failed to delete place", err)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- }
|