places.go 9.5 KB

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