You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
7.0 KiB
Go

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)
})
}