package router import ( "context" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/jackc/pgx/v5/pgxpool" "github.com/denisovdennis/autohero/internal/game" "github.com/denisovdennis/autohero/internal/handler" "github.com/denisovdennis/autohero/internal/storage" "github.com/denisovdennis/autohero/internal/world" ) // Deps holds all dependencies needed by the router. type Deps struct { Engine *game.Engine Hub *handler.Hub PgPool *pgxpool.Pool BotToken string PaymentProviderToken string AdminBasicAuthUsername string AdminBasicAuthPassword string AdminBasicAuthRealm string Logger *slog.Logger ServerStartedAt time.Time } // New creates the chi router with all routes wired. func New(deps Deps) *chi.Mux { r := chi.NewRouter() // Middleware stack. r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(middleware.Heartbeat("/ping")) r.Use(corsMiddleware) // Health. healthH := handler.NewHealthHandler() r.Get("/health", healthH.Health) // Stores (PostgreSQL-backed persistence). heroStore := storage.NewHeroStore(deps.PgPool, deps.Logger) // WebSocket (needs heroStore to resolve telegram ID → hero DB ID). wsH := handler.NewWSHandler(deps.Hub, heroStore, deps.Logger) r.Get("/ws", wsH.HandleWS) logStore := storage.NewLogStore(deps.PgPool) questStore := storage.NewQuestStore(deps.PgPool) gearStore := storage.NewGearStore(deps.PgPool) achievementStore := storage.NewAchievementStore(deps.PgPool) taskStore := storage.NewDailyTaskStore(deps.PgPool) worldSvc := world.NewService() // Auth endpoint (no auth middleware required - this IS the auth). authH := handler.NewAuthHandler(deps.BotToken, heroStore, deps.Logger) r.Post("/api/v1/auth/telegram", authH.TelegramAuth) // Telegram Payments. paymentsH := handler.NewPaymentsHandler(deps.BotToken, deps.PaymentProviderToken, heroStore, logStore, deps.Logger) r.Post("/api/v1/payments/create-invoice", paymentsH.CreateInvoice) r.Post("/api/v1/payments/telegram-webhook", paymentsH.TelegramWebhook) // Admin routes protected with HTTP Basic authentication. adminH := handler.NewAdminHandler(heroStore, deps.Engine, deps.Hub, deps.PgPool, deps.Logger) r.Route("/admin", func(r chi.Router) { r.Use(handler.BasicAuthMiddleware(handler.BasicAuthConfig{ Username: deps.AdminBasicAuthUsername, Password: deps.AdminBasicAuthPassword, Realm: deps.AdminBasicAuthRealm, })) r.Get("/heroes", adminH.ListHeroes) r.Get("/heroes/{heroId}", adminH.GetHero) r.Post("/heroes/{heroId}/set-level", adminH.SetHeroLevel) r.Post("/heroes/{heroId}/set-gold", adminH.SetHeroGold) r.Post("/heroes/{heroId}/set-hp", adminH.SetHeroHP) r.Post("/heroes/{heroId}/add-potions", adminH.AddPotions) r.Post("/heroes/{heroId}/revive", adminH.ReviveHero) r.Post("/heroes/{heroId}/reset", adminH.ResetHero) r.Post("/heroes/{heroId}/reset-buff-charges", adminH.ResetBuffCharges) r.Post("/heroes/{heroId}/start-adventure", adminH.StartHeroAdventure) r.Post("/heroes/{heroId}/teleport-town", adminH.TeleportHeroTown) r.Post("/heroes/{heroId}/start-rest", adminH.StartHeroRest) r.Delete("/heroes/{heroId}", adminH.DeleteHero) r.Get("/towns", adminH.ListTowns) r.Post("/time/pause", adminH.PauseTime) r.Post("/time/resume", adminH.ResumeTime) r.Get("/engine/status", adminH.EngineStatus) r.Get("/engine/combats", adminH.ActiveCombats) r.Get("/ws/connections", adminH.WSConnections) r.Get("/info", adminH.ServerInfo) r.Post("/payments/set-webhook", paymentsH.SetWebhook) }) // API v1 (authenticated routes). gameH := handler.NewGameHandler(deps.Engine, heroStore, logStore, worldSvc, deps.Logger, deps.ServerStartedAt, questStore, gearStore, achievementStore, taskStore, deps.Hub) mapsH := handler.NewMapsHandler(worldSvc, deps.Logger) questH := handler.NewQuestHandler(questStore, heroStore, logStore, deps.Logger) npcH := handler.NewNPCHandler(questStore, heroStore, gearStore, logStore, deps.Logger, deps.Engine, deps.Hub) deps.Engine.SetNPCAlmsHandler(func(ctx context.Context, heroID int64) error { return npcH.ProcessAlmsByHeroID(ctx, heroID) }) achieveH := handler.NewAchievementHandler(achievementStore, heroStore, deps.Logger) taskH := handler.NewDailyTaskHandler(taskStore, heroStore, deps.Logger) r.Route("/api/v1", func(r chi.Router) { // Apply Telegram auth middleware to all routes in this group. // Disabled for now to allow development without a bot token. // r.Use(handler.TelegramAuthMiddleware(deps.BotToken)) r.Use(handler.APITimePausedMiddleware(deps.Engine)) r.Get("/hero", gameH.GetHero) r.Get("/hero/init", gameH.InitHero) r.Post("/hero/name", gameH.SetHeroName) r.Post("/hero/buff/{buffType}", gameH.ActivateBuff) r.Post("/hero/encounter", gameH.RequestEncounter) r.Post("/hero/victory", gameH.ReportVictory) r.Post("/hero/revive", gameH.ReviveHero) r.Post("/hero/purchase-buff-refill", gameH.PurchaseBuffRefill) r.Post("/hero/purchase-subscription", gameH.PurchaseSubscription) r.Get("/hero/loot", gameH.GetLoot) r.Get("/hero/log", gameH.GetAdventureLog) r.Post("/hero/use-potion", gameH.UsePotion) r.Get("/weapons", gameH.GetWeapons) r.Get("/armor", gameH.GetArmor) r.Get("/maps/{mapId}", mapsH.GetMap) // Quest system routes. r.Get("/towns", questH.ListTowns) r.Get("/towns/{townId}/npcs", questH.ListNPCsByTown) r.Get("/npcs/{npcId}/quests", questH.ListQuestsByNPC) r.Post("/hero/quests/{questId}/accept", questH.AcceptQuest) r.Get("/hero/quests", questH.ListHeroQuests) r.Post("/hero/quests/{questId}/claim", questH.ClaimQuestReward) r.Delete("/hero/quests/{questId}", questH.AbandonQuest) // NPC interaction routes. r.Post("/hero/npc-interact", npcH.InteractNPC) r.Get("/hero/nearby-npcs", npcH.NearbyNPCs) r.Post("/hero/npc-alms", npcH.NPCAlms) r.Post("/hero/npc-heal", npcH.HealHero) r.Post("/hero/npc-buy-potion", npcH.BuyPotion) // Gear routes. r.Get("/hero/gear", gameH.GetHeroGear) r.Get("/hero/equipment", gameH.GetHeroGear) // backward compat r.Get("/gear/catalog", gameH.GetGearCatalog) // Achievement routes. r.Get("/hero/achievements", achieveH.GetHeroAchievements) // Daily/weekly task routes. r.Get("/hero/tasks", taskH.ListHeroTasks) r.Post("/hero/tasks/{taskId}/claim", taskH.ClaimTask) // Shared world routes. r.Get("/hero/nearby", gameH.NearbyHeroes) }) return r } // corsMiddleware adds CORS headers to all responses. func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Telegram-Init-Data") w.Header().Set("Access-Control-Max-Age", "86400") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }