main.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // Package main — точка входа HTTP API сервера Photoplaces.
  2. // Инициализирует конфигурацию, логгер, подключение к БД и Redis,
  3. // репозитории, сервисы, хендлеры, middleware (CORS, rate limiting, auth)
  4. // и запускает HTTP сервер на chi-роутере с graceful shutdown.
  5. package main
  6. import (
  7. "context"
  8. "log/slog"
  9. "net/http"
  10. "os"
  11. "os/signal"
  12. "strings"
  13. "syscall"
  14. "time"
  15. "github.com/go-chi/chi/v5"
  16. chimiddleware "github.com/go-chi/chi/v5/middleware"
  17. "github.com/go-chi/cors"
  18. "github.com/redis/go-redis/v9"
  19. "golang.org/x/time/rate"
  20. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/config"
  21. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/handlers"
  22. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/log"
  23. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware"
  24. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/repository"
  25. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/services"
  26. )
  27. func main() {
  28. cfg := config.Load()
  29. ctx := context.Background()
  30. // Инициализация структурированного логгера
  31. logger := log.Init(cfg.AppEnv)
  32. ctx = log.WithContext(ctx, logger)
  33. logger.InfoContext(ctx, "starting application",
  34. slog.String("env", cfg.AppEnv),
  35. slog.String("port", cfg.ServerPort),
  36. )
  37. // Подключение к базе данных
  38. pool, err := repository.NewPool(ctx, cfg.DatabaseURL)
  39. if err != nil {
  40. logger.ErrorContext(ctx, "database connection failed", log.WithError(err))
  41. os.Exit(1)
  42. }
  43. defer pool.Close()
  44. logger.InfoContext(ctx, "database connected")
  45. // Репозитории
  46. userRepo := repository.NewUserRepo(pool)
  47. refreshTokenRepo := repository.NewRefreshTokenRepo(pool)
  48. placeRepo := repository.NewPlaceRepo(pool)
  49. serviceRepo := repository.NewServiceRepo(pool)
  50. reviewRepo := repository.NewReviewRepo(pool)
  51. bookingRepo := repository.NewBookingRepo(pool)
  52. tagRepo := repository.NewTagRepo(pool)
  53. featureRepo := repository.NewFeatureRepo(pool)
  54. moderationLogRepo := repository.NewModerationLogRepo(pool)
  55. // Хендлеры (сначала те, что нужны сервисам)
  56. uploadHandler, err := handlers.NewUploadHandler(cfg.S3Endpoint, cfg.S3PublicEndpoint, cfg.S3AccessKey, cfg.S3SecretKey, cfg.S3Bucket, strings.HasPrefix(cfg.S3Endpoint, "https://"))
  57. if err != nil {
  58. logger.ErrorContext(ctx, "upload handler init failed", log.WithError(err))
  59. os.Exit(1)
  60. }
  61. // Сервисы
  62. authSvc := services.NewAuthService(userRepo, refreshTokenRepo, cfg.JWTSecret, cfg.JWTRefreshSecret)
  63. placeSvc := services.NewPlaceService(placeRepo, uploadHandler, moderationLogRepo)
  64. // Set AppEnv for error handling (production hides internal errors)
  65. handlers.AppEnv = cfg.AppEnv
  66. // Фоновая очистка истёкших refresh токенов (каждые 6 часов)
  67. go func() {
  68. ticker := time.NewTicker(6 * time.Hour)
  69. defer ticker.Stop()
  70. for {
  71. select {
  72. case <-ticker.C:
  73. if err := refreshTokenRepo.CleanupExpired(ctx); err != nil {
  74. logger.ErrorContext(ctx, "cleanup expired tokens failed", log.WithError(err))
  75. }
  76. case <-ctx.Done():
  77. return
  78. }
  79. }
  80. }()
  81. // Хендлеры
  82. authHandler := handlers.NewAuthHandler(authSvc, cfg.AppEnv)
  83. userHandler := handlers.NewUserHandler(userRepo)
  84. placeHandler := handlers.NewPlaceHandler(placeSvc, authSvc)
  85. serviceHandler := handlers.NewServiceHandler(serviceRepo, tagRepo)
  86. reviewHandler := handlers.NewReviewHandler(reviewRepo)
  87. bookingHandler := handlers.NewBookingHandler(bookingRepo)
  88. tagHandler := handlers.NewTagHandler(tagRepo, featureRepo)
  89. wsHub := handlers.NewWSHub(cfg.AllowedOrigins, cfg.AppEnv == "production")
  90. go wsHub.Run()
  91. setupHandler := handlers.NewSetupHandler(userRepo, authSvc, cfg.AppEnv)
  92. // Redis для rate limiting
  93. redisOpts, err := redis.ParseURL(cfg.RedisURL)
  94. if err != nil {
  95. logger.ErrorContext(ctx, "parse redis url failed", log.WithError(err))
  96. os.Exit(1)
  97. }
  98. redisClient := redis.NewClient(redisOpts)
  99. defer redisClient.Close()
  100. redisAvailable := true
  101. if err := redisClient.Ping(ctx).Err(); err != nil {
  102. logger.WarnContext(ctx, "redis connection failed, falling back to in-memory rate limiting", log.WithError(err))
  103. redisAvailable = false
  104. } else {
  105. logger.InfoContext(ctx, "redis connected")
  106. }
  107. failOpen := cfg.AppEnv != "production"
  108. var (
  109. authLimiter rateLimiterInterface
  110. apiReadLimiter rateLimiterInterface
  111. apiWriteLimiter rateLimiterInterface
  112. adminLimiter rateLimiterInterface
  113. )
  114. // Create rate limiters with Redis fallback to in-memory
  115. authLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
  116. func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
  117. return middleware.RateLimitAuthEndpoints(client, log, fo)
  118. },
  119. func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(30/60.0), 30) }, // 30 req/min
  120. )
  121. apiReadLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
  122. func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
  123. return middleware.RateLimitAPIRead(client, log, fo)
  124. },
  125. func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(60/60.0), 60) }, // 60 req/min
  126. )
  127. apiWriteLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
  128. func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
  129. return middleware.RateLimitAPIWrite(client, log, fo)
  130. },
  131. func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(10/60.0), 10) }, // 10 req/min
  132. )
  133. adminLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
  134. func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
  135. return middleware.RateLimitAdmin(client, log, fo)
  136. },
  137. func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(100/60.0), 100) }, // 100 req/min
  138. )
  139. // Роутер
  140. r := chi.NewRouter()
  141. // Middleware для логирования запросов
  142. r.Use(requestLoggerMiddleware(logger))
  143. r.Use(chimiddleware.Recoverer)
  144. r.Use(chimiddleware.RequestID)
  145. r.Use(cors.Handler(cors.Options{
  146. AllowedOrigins: cfg.AllowedOrigins,
  147. AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"},
  148. AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
  149. AllowCredentials: true,
  150. MaxAge: 300,
  151. }))
  152. r.Route("/api/v1", func(r chi.Router) {
  153. r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
  154. w.Write([]byte(`{"status":"ok"}`))
  155. })
  156. // Admin setup (first superadmin creation)
  157. r.Get("/admin/setup-status", setupHandler.Status)
  158. r.Post("/admin/setup", setupHandler.Setup)
  159. // Public auth - strict rate limiting
  160. r.Group(func(r chi.Router) {
  161. if authLimiter != nil {
  162. r.Use(authLimiter.Middleware())
  163. }
  164. r.Post("/auth/register", authHandler.Register)
  165. r.Post("/auth/login", authHandler.Login)
  166. r.Post("/auth/refresh", authHandler.Refresh)
  167. })
  168. // Public read endpoints - moderate rate limiting
  169. r.Group(func(r chi.Router) {
  170. if apiReadLimiter != nil {
  171. r.Use(apiReadLimiter.Middleware())
  172. }
  173. r.Get("/reviews", reviewHandler.List)
  174. r.Get("/tags", tagHandler.ListTags)
  175. r.Get("/features", tagHandler.ListFeatures)
  176. r.Get("/services", serviceHandler.List)
  177. r.Get("/services/{id}", serviceHandler.GetByID)
  178. r.Get("/places", placeHandler.List)
  179. r.Get("/places/{id}", placeHandler.GetByID)
  180. r.Get("/users/{id}", userHandler.GetProfile)
  181. r.Get("/geocode/reverse", handlers.GeocodeReverse)
  182. })
  183. // WebSocket visitor dots
  184. r.Get("/ws/visitors", wsHub.HandleWS)
  185. // Authenticated write endpoints - stricter rate limiting
  186. r.Group(func(r chi.Router) {
  187. r.Use(middleware.AuthMiddleware(authSvc))
  188. if apiWriteLimiter != nil {
  189. r.Use(apiWriteLimiter.Middleware())
  190. }
  191. r.Get("/auth/me", authHandler.Me)
  192. r.Post("/auth/logout", authHandler.Logout)
  193. // Users
  194. r.Patch("/users/me", userHandler.UpdateMe)
  195. // Upload
  196. r.Post("/upload/presigned-url", uploadHandler.PresignedURL)
  197. r.Post("/upload", uploadHandler.UploadFile)
  198. // Bookings
  199. r.Post("/bookings", bookingHandler.Create)
  200. r.Get("/bookings/me", bookingHandler.ListMy)
  201. r.Patch("/bookings/{id}/cancel", bookingHandler.Cancel)
  202. // Reviews
  203. r.Post("/reviews", reviewHandler.Create)
  204. // Services
  205. r.Post("/services", serviceHandler.Create)
  206. r.Patch("/services/{id}", serviceHandler.Update)
  207. r.Delete("/services/{id}", serviceHandler.Delete)
  208. // Places
  209. r.Get("/places/my", placeHandler.ListMy)
  210. r.Post("/places", placeHandler.Create)
  211. r.Patch("/places/{id}", placeHandler.Update)
  212. r.Delete("/places/{id}", placeHandler.Delete)
  213. })
  214. // Moderator / Admin - higher limits
  215. r.Group(func(r chi.Router) {
  216. r.Use(middleware.AuthMiddleware(authSvc))
  217. r.Use(middleware.RoleMiddleware("moderator", "superadmin"))
  218. if adminLimiter != nil {
  219. r.Use(adminLimiter.Middleware())
  220. }
  221. r.Post("/places/{id}/moderate", placeHandler.Moderate)
  222. r.Post("/places/{id}/restore", placeHandler.Restore)
  223. r.Get("/admin/users", userHandler.AdminListUsers)
  224. r.Post("/admin/users", userHandler.AdminCreateUser)
  225. r.Patch("/admin/users/{id}", userHandler.AdminUpdateUser)
  226. r.Patch("/admin/bookings/{id}/confirm", bookingHandler.Confirm)
  227. r.Post("/admin/tags", tagHandler.CreateTag)
  228. r.Delete("/admin/tags", tagHandler.DeleteTag)
  229. r.Post("/admin/features", tagHandler.CreateFeature)
  230. r.Delete("/admin/features", tagHandler.DeleteFeature)
  231. r.Delete("/admin/places/{id}", placeHandler.HardDelete)
  232. })
  233. })
  234. // HTTP сервер с graceful shutdown
  235. server := &http.Server{
  236. Addr: ":" + cfg.ServerPort,
  237. Handler: r,
  238. ReadTimeout: 15 * time.Second,
  239. WriteTimeout: 15 * time.Second,
  240. IdleTimeout: 60 * time.Second,
  241. }
  242. // Запуск сервера в горутине
  243. go func() {
  244. logger.InfoContext(ctx, "starting http server", slog.String("addr", server.Addr))
  245. if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  246. logger.ErrorContext(ctx, "http server error", log.WithError(err))
  247. os.Exit(1)
  248. }
  249. }()
  250. // Ожидание сигнала завершения
  251. quit := make(chan os.Signal, 1)
  252. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
  253. <-quit
  254. logger.InfoContext(ctx, "shutdown signal received, gracefully stopping...")
  255. // Graceful shutdown с таймаутом
  256. shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  257. defer cancel()
  258. if err := server.Shutdown(shutdownCtx); err != nil {
  259. logger.ErrorContext(ctx, "server shutdown failed", log.WithError(err))
  260. os.Exit(1)
  261. }
  262. logger.InfoContext(ctx, "server stopped gracefully")
  263. }
  264. // requestLoggerMiddleware логирует входящие HTTP запросы
  265. func requestLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
  266. return func(next http.Handler) http.Handler {
  267. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  268. start := time.Now()
  269. ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor)
  270. requestID := chimiddleware.GetReqID(r.Context())
  271. ctx := log.WithContext(r.Context(), logger.With("request_id", requestID))
  272. logger.InfoContext(ctx, "request started",
  273. slog.String("method", r.Method),
  274. slog.String("path", r.URL.Path),
  275. slog.String("remote_addr", r.RemoteAddr),
  276. slog.String("user_agent", r.UserAgent()),
  277. )
  278. next.ServeHTTP(ww, r.WithContext(ctx))
  279. logger.InfoContext(ctx, "request completed",
  280. slog.Int("status", ww.Status()),
  281. slog.Int("bytes_written", ww.BytesWritten()),
  282. slog.Duration("duration", time.Since(start)),
  283. )
  284. })
  285. }
  286. }
  287. // rateLimiterInterface defines the common interface for rate limiters
  288. type rateLimiterInterface interface {
  289. Middleware() func(http.Handler) http.Handler
  290. }
  291. // newRateLimiterWithFallback creates a rate limiter with Redis as primary and in-memory as fallback
  292. func newRateLimiterWithFallback(
  293. redisClient *redis.Client,
  294. redisAvailable bool,
  295. logger *slog.Logger,
  296. failOpen bool,
  297. redisLimiterFn func(*redis.Client, *slog.Logger, bool) (rateLimiterInterface, error),
  298. inMemoryLimiterFn func() *middleware.RateLimiter,
  299. ) rateLimiterInterface {
  300. if redisAvailable {
  301. if limiter, err := redisLimiterFn(redisClient, logger, failOpen); err == nil {
  302. logger.InfoContext(context.Background(), "using Redis rate limiter")
  303. return limiter
  304. }
  305. logger.WarnContext(context.Background(), "failed to create Redis rate limiter, falling back to in-memory")
  306. }
  307. logger.InfoContext(context.Background(), "using in-memory rate limiter")
  308. return inMemoryLimiterFn()
  309. }