places.go 11 KB

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