package main import ( "context" "log/slog" "net/http" "os" "strconv" "strings" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "hakemsho/internal/admin" "hakemsho/internal/auth" "hakemsho/internal/config" "hakemsho/internal/economy" "hakemsho/internal/httpx" "hakemsho/internal/static" "hakemsho/internal/store" "hakemsho/internal/telescope" "hakemsho/internal/user" "hakemsho/internal/ws" ) func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil))) cfg := config.Load() st, err := store.Open(cfg.DBPath) if err != nil { slog.Error("open db", "err", err) os.Exit(1) } defer st.Close() // لایه‌های دامنه users := user.NewRepo(st.DB) otp := auth.NewOTPStore(st.DB, cfg.OTPTTL) kave := auth.NewKavenegar(cfg.KaveAPIKey, cfg.KaveTemple) jwt := auth.NewJWT(cfg.JWTSecret, cfg.JWTTTL) authH := auth.NewHandler(users, otp, kave, jwt, cfg.AdminMobile, cfg.AdminOTP) // تلسکوپ: بازرسِ درون‌برنامه‌ایِ درخواست‌ها. شناسه‌ی کاربر را ارزان (فقط HMAC) // از توکن درمی‌آورد و شماره‌ی موبایل را در زمانِ نمایش (با DB) resolve می‌کند. scope := telescope.New(cfg.TelescopeSize, func(r *http.Request) int64 { tok := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if tok == "" || tok == r.Header.Get("Authorization") { tok = r.URL.Query().Get("token") } if tok == "" { return 0 } id, err := jwt.Verify(tok) if err != nil { return 0 } return id }, func(id int64) string { if u, err := users.FindByID(context.Background(), id); err == nil { return u.Mobile } return "" }, ) // اقتصاد و فروشگاه. تأییدِ خرید: مایکت (X-Access-Token) و کافه‌بازار (امضای RSA). // اگر کلیدها تنظیم نشده باشند، به تأییدکننده‌ی توسعه برمی‌گردیم (dev fallback). iap := economy.StoreRouter{DevFallback: true} // مایکت: تأییدِ آفلاینِ RSA (اگر کلید باشد) و/یا سمت‌سرور با access token. if cfg.MyketRSAKey != "" || (cfg.MyketAccessToken != "" && cfg.IAPPackage != "") { mv, err := economy.NewMyketVerifier(cfg.IAPPackage, cfg.MyketAccessToken).WithRSAKey(cfg.MyketRSAKey) if err != nil { slog.Error("myket rsa key", "err", err) os.Exit(1) } iap.Myket = mv slog.Info("IAP myket configured", "package", cfg.IAPPackage, "rsa", cfg.MyketRSAKey != "", "server", cfg.MyketAccessToken != "") } if cfg.BazaarRSAKey != "" { bv, err := economy.NewBazaarRSAVerifier(cfg.BazaarRSAKey) if err != nil { slog.Error("bazaar rsa key", "err", err) os.Exit(1) } iap.Bazaar = bv slog.Info("IAP bazaar (RSA) configured") } eco := economy.New(st.DB, economy.DevAdVerifier{}, iap) if err := eco.LoadCatalog(context.Background()); err != nil { slog.Error("load catalog", "err", err) os.Exit(1) } ecoH := economy.NewHandler(eco) // هاب WebSocket برای بازی realtime hub := ws.NewHub(func(token string) (int64, string, error) { id, err := jwt.Verify(token) if err != nil { return 0, "", err } name := "بازیکن " + strconv.FormatInt(id, 10) if u, err := users.FindByID(context.Background(), id); err == nil { if u.FirstName != nil && *u.FirstName != "" { name = *u.FirstName } else { name = u.Mobile } } return id, name, nil }) hub.SetSettler(gameSettler{eco: eco}) go hub.Run() r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(15 * time.Second)) // بازرسِ درخواست‌ها: هر درخواستِ /api و /admin را در رینگ‌بافر ثبت می‌کند. r.Use(scope.Middleware) r.Get("/health", func(w http.ResponseWriter, _ *http.Request) { httpx.JSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) // WebSocket بازی (احراز هویت با توکن در ?token= یا هدر Authorization) r.Get("/ws", hub.ServeWS) // تصاویرِ کارت (هر اسکین یک پوشه) — از backend سرویس می‌شود تا اپ سبک بماند. r.Handle("/cards/*", static.CardsHandler(cfg.CardsDir)) // تصاویرِ فرش (به‌جای میز) — از پنل ادمین آپلود می‌شوند. r.Handle("/carpets/*", static.CarpetsHandler(cfg.CarpetsDir)) // پنل ادمین (HTML سرور‌ساید، پشت Basic Auth) r.Mount("/admin", admin.New(st.DB, eco, cfg.CardsDir, cfg.CarpetsDir).Routes(cfg.AdminUser, cfg.AdminPass)) // بازرسِ درخواست‌ها (Telescope-lite) — پشتِ همان Basic Auth ادمین. r.Group(func(r chi.Router) { r.Use(middleware.BasicAuth("hakemsho-admin", map[string]string{cfg.AdminUser: cfg.AdminPass})) r.Get("/admin/telescope", scope.Page) r.Get("/admin/telescope/data", scope.Data) }) r.Route("/api", func(r chi.Router) { r.Route("/auth", func(r chi.Router) { r.Post("/login-otp", authH.LoginOTP) r.Post("/check-otp", authH.CheckOTP) }) // مسیرهای محافظت‌شده با JWT r.Group(func(r chi.Router) { r.Use(jwt.Middleware) r.Get("/me", authH.Me) r.Post("/profile", authH.UpdateProfile) // کیف‌پول و فروشگاه r.Get("/wallet", ecoH.Wallet) r.Get("/stats", ecoH.Stats) r.Get("/leaderboard", ecoH.Leaderboard) r.Get("/tables/info", ecoH.TablesInfo) r.Get("/shop", ecoH.Shop) r.Post("/shop/buy-card", ecoH.BuyCard) r.Post("/shop/select-card", ecoH.SelectCard) r.Get("/chat-packs", ecoH.ChatPacks) r.Post("/shop/buy-chat-pack", ecoH.BuyChatPack) r.Get("/carpets", ecoH.Carpets) r.Post("/shop/buy-carpet", ecoH.BuyCarpet) r.Post("/shop/select-carpet", ecoH.SelectCarpet) r.Post("/shop/purchase", ecoH.Purchase) r.Post("/rewards/daily", ecoH.Daily) r.Post("/rewards/ad", ecoH.AdReward) }) }) srv := &http.Server{ Addr: cfg.Addr, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } slog.Info("server starting", "addr", cfg.Addr) if err := srv.ListenAndServe(); err != nil { slog.Error("server", "err", err) os.Exit(1) } }