places.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package handlers
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "github.com/go-chi/chi/v5"
  8. "github.com/photoplaces/backend/internal/middleware"
  9. "github.com/photoplaces/backend/internal/models"
  10. "github.com/photoplaces/backend/internal/services"
  11. "github.com/photoplaces/backend/internal/validator"
  12. )
  13. type PlaceHandler struct {
  14. placeSvc *services.PlaceService
  15. }
  16. func NewPlaceHandler(placeSvc *services.PlaceService) *PlaceHandler {
  17. return &PlaceHandler{placeSvc: placeSvc}
  18. }
  19. func (h *PlaceHandler) List(w http.ResponseWriter, r *http.Request) {
  20. q := r.URL.Query()
  21. filter := models.PlaceFilter{
  22. Type: q.Get("type"),
  23. Status: "published",
  24. }
  25. if tags := q["tags"]; len(tags) > 0 {
  26. filter.TagIDs = tags
  27. }
  28. if features := q["features"]; len(features) > 0 {
  29. filter.FeatureIDs = features
  30. }
  31. if mr := q.Get("min_rating"); mr != "" {
  32. if v, err := strconv.ParseFloat(mr, 64); err == nil {
  33. filter.MinRating = v
  34. }
  35. }
  36. if pmin := q.Get("price_min"); pmin != "" {
  37. if v, err := strconv.Atoi(pmin); err == nil {
  38. filter.PriceMin = &v
  39. }
  40. }
  41. if pmax := q.Get("price_max"); pmax != "" {
  42. if v, err := strconv.Atoi(pmax); err == nil {
  43. filter.PriceMax = &v
  44. }
  45. }
  46. if bounds := q.Get("bounds"); bounds != "" {
  47. var b models.Bounds
  48. if _, err := fmt.Sscanf(bounds, "%f,%f,%f,%f", &b.SWLat, &b.SWLng, &b.NELat, &b.NELng); err == nil {
  49. filter.Bounds = &b
  50. }
  51. }
  52. if limit := q.Get("limit"); limit != "" {
  53. if v, err := strconv.Atoi(limit); err == nil {
  54. filter.Limit_ = v
  55. }
  56. }
  57. filter.Sort = q.Get("sort")
  58. if q.Get("user_lat") != "" && q.Get("user_lng") != "" {
  59. if lat, err := strconv.ParseFloat(q.Get("user_lat"), 64); err == nil {
  60. filter.UserLat = &lat
  61. }
  62. if lng, err := strconv.ParseFloat(q.Get("user_lng"), 64); err == nil {
  63. filter.UserLng = &lng
  64. }
  65. }
  66. places, err := h.placeSvc.List(r.Context(), filter)
  67. if err != nil {
  68. writeError(w, http.StatusInternalServerError, err.Error())
  69. return
  70. }
  71. if places == nil {
  72. places = []*models.Place{}
  73. }
  74. writeJSON(w, http.StatusOK, map[string]interface{}{
  75. "data": places,
  76. })
  77. }
  78. func (h *PlaceHandler) GetByID(w http.ResponseWriter, r *http.Request) {
  79. id := chi.URLParam(r, "id")
  80. place, err := h.placeSvc.GetByID(r.Context(), id, true)
  81. if err != nil {
  82. writeError(w, http.StatusInternalServerError, err.Error())
  83. return
  84. }
  85. if place == nil {
  86. writeError(w, http.StatusNotFound, "place not found")
  87. return
  88. }
  89. writeJSON(w, http.StatusOK, place)
  90. }
  91. type createPlaceRequest struct {
  92. Title string `json:"title" validate:"required,min=1,max=255"`
  93. Description *string `json:"description" validate:"omitempty,max=5000"`
  94. Address *string `json:"address" validate:"omitempty,max=500"`
  95. Lat float64 `json:"lat" validate:"required,latitude"`
  96. Lng float64 `json:"lng" validate:"required,longitude"`
  97. Type string `json:"type" validate:"required,place_type"`
  98. AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
  99. CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
  100. Tags []string `json:"tags" validate:"dive,slug,max=50"`
  101. Features []string `json:"features" validate:"dive,slug,max=50"`
  102. HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
  103. Currency string `json:"currency" validate:"omitempty,currency,len=3"`
  104. MinHours int `json:"min_hours" validate:"min=0,max=100"`
  105. }
  106. type updatePlaceRequest struct {
  107. Title *string `json:"title" validate:"omitempty,min=1,max=255"`
  108. Description *string `json:"description" validate:"omitempty,max=5000"`
  109. Address *string `json:"address" validate:"omitempty,max=500"`
  110. Lat *float64 `json:"lat" validate:"omitempty,latitude"`
  111. Lng *float64 `json:"lng" validate:"omitempty,longitude"`
  112. AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
  113. CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
  114. Tags []string `json:"tags" validate:"dive,slug,max=50"`
  115. Features []string `json:"features" validate:"dive,slug,max=50"`
  116. HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
  117. Currency *string `json:"currency" validate:"omitempty,currency,len=3"`
  118. MinHours *int `json:"min_hours" validate:"omitempty,min=0,max=100"`
  119. }
  120. type moderatePlaceRequest struct {
  121. Action string `json:"action" validate:"required,oneof=approve reject"`
  122. Comment *string `json:"comment" validate:"omitempty,max=1000"`
  123. }
  124. func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
  125. userID := middleware.GetUserID(r.Context())
  126. role := middleware.GetUserRole(r.Context())
  127. var req createPlaceRequest
  128. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  129. writeError(w, http.StatusBadRequest, "invalid request body")
  130. return
  131. }
  132. if err := validator.Validate(req); err != nil {
  133. writeValidationError(w, err)
  134. return
  135. }
  136. if req.Type == "studio" && role != "landlord" && role != "moderator" && role != "superadmin" {
  137. writeError(w, http.StatusForbidden, "only landlords can create studios")
  138. return
  139. }
  140. if req.Type == "place" && role != "customer" && role != "moderator" && role != "superadmin" {
  141. writeError(w, http.StatusForbidden, "only customers can create places")
  142. return
  143. }
  144. tags := make([]models.Tag, len(req.Tags))
  145. for i, t := range req.Tags {
  146. tags[i] = models.Tag{ID: t}
  147. }
  148. features := make([]models.Feature, len(req.Features))
  149. for i, f := range req.Features {
  150. features[i] = models.Feature{ID: f}
  151. }
  152. place, err := h.placeSvc.Create(r.Context(), services.CreatePlaceInput{
  153. OwnerID: userID,
  154. Type: req.Type,
  155. Title: req.Title,
  156. Description: req.Description,
  157. Address: req.Address,
  158. Lat: req.Lat,
  159. Lng: req.Lng,
  160. CoverImage: req.CoverImage,
  161. AccessInfo: req.AccessInfo,
  162. Tags: tags,
  163. Features: features,
  164. HourlyRate: req.HourlyRate,
  165. Currency: req.Currency,
  166. MinHours: req.MinHours,
  167. })
  168. if err != nil {
  169. writeError(w, http.StatusInternalServerError, err.Error())
  170. return
  171. }
  172. writeJSON(w, http.StatusCreated, place)
  173. }
  174. func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
  175. id := chi.URLParam(r, "id")
  176. userID := middleware.GetUserID(r.Context())
  177. role := middleware.GetUserRole(r.Context())
  178. var req updatePlaceRequest
  179. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  180. writeError(w, http.StatusBadRequest, "invalid request body")
  181. return
  182. }
  183. if err := validator.Validate(req); err != nil {
  184. writeValidationError(w, err)
  185. return
  186. }
  187. isModerator := role == "moderator" || role == "superadmin"
  188. input := services.UpdatePlaceInput{
  189. ID: id,
  190. OwnerID: userID,
  191. }
  192. if req.Title != nil { input.Title = req.Title }
  193. input.Description = req.Description
  194. input.Address = req.Address
  195. input.Lat = req.Lat
  196. input.Lng = req.Lng
  197. input.AccessInfo = req.AccessInfo
  198. input.HourlyRate = req.HourlyRate
  199. input.Currency = req.Currency
  200. input.MinHours = req.MinHours
  201. place, err := h.placeSvc.Update(r.Context(), input, isModerator)
  202. if err != nil {
  203. writeError(w, http.StatusInternalServerError, err.Error())
  204. return
  205. }
  206. if place == nil {
  207. writeError(w, http.StatusNotFound, "place not found")
  208. return
  209. }
  210. writeJSON(w, http.StatusOK, place)
  211. }
  212. func (h *PlaceHandler) Moderate(w http.ResponseWriter, r *http.Request) {
  213. id := chi.URLParam(r, "id")
  214. userID := middleware.GetUserID(r.Context())
  215. var req moderatePlaceRequest
  216. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  217. writeError(w, http.StatusBadRequest, "invalid request body")
  218. return
  219. }
  220. if err := validator.Validate(req); err != nil {
  221. writeValidationError(w, err)
  222. return
  223. }
  224. if err := h.placeSvc.Moderate(r.Context(), id, req.Action, req.Comment, userID); err != nil {
  225. writeError(w, http.StatusInternalServerError, err.Error())
  226. return
  227. }
  228. writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
  229. }
  230. func (h *PlaceHandler) Delete(w http.ResponseWriter, r *http.Request) {
  231. id := chi.URLParam(r, "id")
  232. userID := middleware.GetUserID(r.Context())
  233. role := middleware.GetUserRole(r.Context())
  234. place, err := h.placeSvc.GetByID(r.Context(), id, false)
  235. if err != nil {
  236. writeError(w, http.StatusInternalServerError, err.Error())
  237. return
  238. }
  239. if place == nil {
  240. writeError(w, http.StatusNotFound, "place not found")
  241. return
  242. }
  243. if place.OwnerID != userID && role != "moderator" && role != "superadmin" {
  244. writeError(w, http.StatusForbidden, "not your place")
  245. return
  246. }
  247. if err := h.placeSvc.Delete(r.Context(), id); err != nil {
  248. writeError(w, http.StatusInternalServerError, err.Error())
  249. return
  250. }
  251. w.WriteHeader(http.StatusNoContent)
  252. }