places.go 8.6 KB

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