places.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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, appEnv string) *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. places, err := h.placeSvc.List(r.Context(), filter)
  78. if err != nil {
  79. writeError(w, http.StatusInternalServerError, "failed to list places", err)
  80. return
  81. }
  82. if places == nil {
  83. places = []*models.Place{}
  84. }
  85. writeJSON(w, http.StatusOK, map[string]interface{}{
  86. "data": places,
  87. })
  88. }
  89. func (h *PlaceHandler) GetByID(w http.ResponseWriter, r *http.Request) {
  90. id := chi.URLParam(r, "id")
  91. place, err := h.placeSvc.GetByID(r.Context(), id, true)
  92. if err != nil {
  93. writeError(w, http.StatusInternalServerError, "failed to get place", err)
  94. return
  95. }
  96. if place == nil {
  97. writeError(w, http.StatusNotFound, "place not found", nil)
  98. return
  99. }
  100. writeJSON(w, http.StatusOK, place)
  101. }
  102. type createPlaceRequest struct {
  103. Title string `json:"title" validate:"required,min=1,max=255"`
  104. Description *string `json:"description" validate:"omitempty,max=5000"`
  105. Address *string `json:"address" validate:"omitempty,max=500"`
  106. Lat float64 `json:"lat" validate:"required,latitude"`
  107. Lng float64 `json:"lng" validate:"required,longitude"`
  108. Type string `json:"type" validate:"required,place_type"`
  109. AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
  110. CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
  111. Tags []string `json:"tags" validate:"dive,slug,max=50"`
  112. Features []string `json:"features" validate:"dive,slug,max=50"`
  113. HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
  114. Currency string `json:"currency" validate:"omitempty,currency,len=3"`
  115. MinHours int `json:"min_hours" validate:"min=0,max=100"`
  116. }
  117. type updatePlaceRequest struct {
  118. Title *string `json:"title" validate:"omitempty,min=1,max=255"`
  119. Description *string `json:"description" validate:"omitempty,max=5000"`
  120. Address *string `json:"address" validate:"omitempty,max=500"`
  121. Lat *float64 `json:"lat" validate:"omitempty,latitude"`
  122. Lng *float64 `json:"lng" validate:"omitempty,longitude"`
  123. AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"`
  124. CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"`
  125. Tags []string `json:"tags" validate:"dive,slug,max=50"`
  126. Features []string `json:"features" validate:"dive,slug,max=50"`
  127. HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
  128. Currency *string `json:"currency" validate:"omitempty,currency,len=3"`
  129. MinHours *int `json:"min_hours" validate:"omitempty,min=0,max=100"`
  130. }
  131. type moderatePlaceRequest struct {
  132. Action string `json:"action" validate:"required,oneof=approve reject"`
  133. Comment *string `json:"comment" validate:"omitempty,max=1000"`
  134. }
  135. func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
  136. userID := middleware.GetUserID(r.Context())
  137. role := middleware.GetUserRole(r.Context())
  138. var req createPlaceRequest
  139. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  140. writeError(w, http.StatusBadRequest, "invalid request body", err)
  141. return
  142. }
  143. if err := validator.Validate(req); err != nil {
  144. writeValidationError(w, err)
  145. return
  146. }
  147. if req.Type == "studio" && role != "landlord" && role != "moderator" && role != "superadmin" {
  148. writeError(w, http.StatusForbidden, "only landlords can create studios", nil)
  149. return
  150. }
  151. if req.Type == "place" && role != "customer" && role != "moderator" && role != "superadmin" {
  152. writeError(w, http.StatusForbidden, "only customers can create places", nil)
  153. return
  154. }
  155. tags := make([]models.Tag, len(req.Tags))
  156. for i, t := range req.Tags {
  157. tags[i] = models.Tag{ID: t}
  158. }
  159. features := make([]models.Feature, len(req.Features))
  160. for i, f := range req.Features {
  161. features[i] = models.Feature{ID: f}
  162. }
  163. place, err := h.placeSvc.Create(r.Context(), services.CreatePlaceInput{
  164. OwnerID: userID,
  165. Type: req.Type,
  166. Title: req.Title,
  167. Description: req.Description,
  168. Address: req.Address,
  169. Lat: req.Lat,
  170. Lng: req.Lng,
  171. CoverImage: req.CoverImage,
  172. AccessInfo: req.AccessInfo,
  173. Tags: tags,
  174. Features: features,
  175. HourlyRate: req.HourlyRate,
  176. Currency: req.Currency,
  177. MinHours: req.MinHours,
  178. })
  179. if err != nil {
  180. writeError(w, http.StatusInternalServerError, "failed to create place", err)
  181. return
  182. }
  183. writeJSON(w, http.StatusCreated, place)
  184. }
  185. func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
  186. id := chi.URLParam(r, "id")
  187. userID := middleware.GetUserID(r.Context())
  188. role := middleware.GetUserRole(r.Context())
  189. var req updatePlaceRequest
  190. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  191. writeError(w, http.StatusBadRequest, "invalid request body", err)
  192. return
  193. }
  194. if err := validator.Validate(req); err != nil {
  195. writeValidationError(w, err)
  196. return
  197. }
  198. isModerator := role == "moderator" || role == "superadmin"
  199. input := services.UpdatePlaceInput{
  200. ID: id,
  201. OwnerID: userID,
  202. }
  203. if req.Title != nil { input.Title = req.Title }
  204. input.Description = req.Description
  205. input.Address = req.Address
  206. input.Lat = req.Lat
  207. input.Lng = req.Lng
  208. input.AccessInfo = req.AccessInfo
  209. input.HourlyRate = req.HourlyRate
  210. input.Currency = req.Currency
  211. input.MinHours = req.MinHours
  212. place, err := h.placeSvc.Update(r.Context(), input, isModerator)
  213. if err != nil {
  214. writeError(w, http.StatusInternalServerError, "failed to update place", err)
  215. return
  216. }
  217. if place == nil {
  218. writeError(w, http.StatusNotFound, "place not found", nil)
  219. return
  220. }
  221. writeJSON(w, http.StatusOK, place)
  222. }
  223. func (h *PlaceHandler) Moderate(w http.ResponseWriter, r *http.Request) {
  224. id := chi.URLParam(r, "id")
  225. userID := middleware.GetUserID(r.Context())
  226. var req moderatePlaceRequest
  227. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  228. writeError(w, http.StatusBadRequest, "invalid request body", err)
  229. return
  230. }
  231. if err := validator.Validate(req); err != nil {
  232. writeValidationError(w, err)
  233. return
  234. }
  235. comment := ""
  236. if req.Comment != nil {
  237. comment = *req.Comment
  238. }
  239. if err := h.placeSvc.Moderate(r.Context(), id, req.Action, comment, userID); err != nil {
  240. writeError(w, http.StatusInternalServerError, "failed to moderate place", err)
  241. return
  242. }
  243. writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
  244. }
  245. func (h *PlaceHandler) Delete(w http.ResponseWriter, r *http.Request) {
  246. id := chi.URLParam(r, "id")
  247. userID := middleware.GetUserID(r.Context())
  248. role := middleware.GetUserRole(r.Context())
  249. place, err := h.placeSvc.GetByID(r.Context(), id, false)
  250. if err != nil {
  251. writeError(w, http.StatusInternalServerError, "failed to get place", err)
  252. return
  253. }
  254. if place == nil {
  255. writeError(w, http.StatusNotFound, "place not found", nil)
  256. return
  257. }
  258. if place.OwnerID != userID && role != "moderator" && role != "superadmin" {
  259. writeError(w, http.StatusForbidden, "not your place", nil)
  260. return
  261. }
  262. if err := h.placeSvc.Delete(r.Context(), id); err != nil {
  263. writeError(w, http.StatusInternalServerError, "failed to delete place", err)
  264. return
  265. }
  266. w.WriteHeader(http.StatusNoContent)
  267. }