package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "gitea.mrixs.me/minecraft-platform/backend/internal/api" "gitea.mrixs.me/minecraft-platform/backend/internal/core" "gitea.mrixs.me/minecraft-platform/backend/internal/database" "gitea.mrixs.me/minecraft-platform/backend/internal/ws" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { // --- Инициализация логгера (slog) --- logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) slog.Info("Starting backend server initialization...") dbPool := database.Connect() defer dbPool.Close() // --- Инициализация репозиториев --- userRepo := &database.UserRepository{DB: dbPool} serverRepo := &database.ServerRepository{DB: dbPool} modpackRepo := &database.ModpackRepository{DB: dbPool} jobRepo := &database.JobRepository{DB: dbPool} // --- Инициализация сервисов --- userService := &core.UserService{Repo: userRepo} authService := &core.AuthService{UserRepo: userRepo} serverPoller := &core.ServerPoller{Repo: serverRepo} // --- Инициализация WebSocket Hub --- hub := ws.NewHub() go hub.Run() keyPath := os.Getenv("RSA_PRIVATE_KEY_PATH") if keyPath == "" { slog.Error("RSA_PRIVATE_KEY_PATH environment variable is not set") os.Exit(1) } domain := os.Getenv("APP_DOMAIN") if domain == "" { slog.Error("APP_DOMAIN environment variable is not set") os.Exit(1) } profileService, err := core.NewProfileService(userRepo, keyPath, domain) if err != nil { slog.Error("Failed to create profile service", "error", err) os.Exit(1) } modpacksStoragePath := os.Getenv("MODPACKS_STORAGE_PATH") if modpacksStoragePath == "" { slog.Error("MODPACKS_STORAGE_PATH environment variable is not set") os.Exit(1) } janitorService := core.NewFileJanitorService(modpackRepo, modpacksStoragePath) // --- Запуск фоновых задач --- ctx, cancel := context.WithCancel(context.Background()) defer cancel() go serverPoller.Start(ctx) // Передаем контекст для отмены // --- Инициализация хендлеров --- userHandler := &api.UserHandler{Service: userService} authHandler := &api.AuthHandler{Service: authService} profileHandler := &api.ProfileHandler{Service: profileService} serverHandler := &api.ServerHandler{Repo: serverRepo} launcherHandler := &api.LauncherHandler{ModpackRepo: modpackRepo} modpackHandler := &api.ModpackHandler{ ModpackRepo: modpackRepo, JobRepo: jobRepo, JanitorService: janitorService, Hub: hub, } adminUserHandler := &api.AdminUserHandler{UserRepo: userRepo} // --- Настройка роутера --- r := chi.NewRouter() r.Use(middleware.Logger) // Можно заменить на slog middleware, но пока оставим standard r.Use(middleware.Recoverer) // Health Check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) // --- Публичные роуты --- r.Route("/api", func(r chi.Router) { r.Post("/register", userHandler.Register) r.Post("/login", authHandler.Login) r.Get("/servers", serverHandler.GetServers) r.Route("/launcher", func(r chi.Router) { r.Get("/modpacks/{name}/manifest", launcherHandler.GetModpackManifest) r.Get("/modpacks/summary", launcherHandler.GetModpacksSummary) }) }) r.Route("/authserver", func(r chi.Router) { r.Post("/authenticate", authHandler.Authenticate) }) r.Route("/sessionserver/session/minecraft", func(r chi.Router) { r.Post("/join", authHandler.Join) r.Get("/profile/{uuid}", profileHandler.GetProfile) }) // --- Защищенные роуты --- r.Group(func(r chi.Router) { r.Use(api.AuthMiddleware) r.Route("/api/user", func(r chi.Router) { r.Post("/skin", profileHandler.UploadSkin) r.Get("/me", userHandler.GetMe) }) r.Route("/api/admin", func(r chi.Router) { r.Use(api.AdminMiddleware) // WebSocket endpoint for jobs r.Get("/ws/jobs", func(w http.ResponseWriter, r *http.Request) { ws.ServeWs(hub, w, r) }) r.Route("/modpacks", func(r chi.Router) { r.Post("/import", modpackHandler.ImportModpack) }) r.Route("/users", func(r chi.Router) { r.Get("/", adminUserHandler.GetAllUsers) r.Patch("/{id}/role", adminUserHandler.UpdateUserRole) }) }) }) // --- Graceful Shutdown --- srv := &http.Server{ Addr: ":8080", Handler: r, } go func() { slog.Info("Starting backend server on :8080") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("Failed to start server", "error", err) os.Exit(1) } }() // Wait for interrupt signal to gracefully shutdown the server with a timeout. quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("Shutting down server...") // The context is used to inform the server it has 5 seconds to finish // the request it is currently handling shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { slog.Error("Server forced to shutdown", "error", err) } slog.Info("Server exiting") }