// پکیج admin یک پنل ادمینِ سرور‌سایدِ سبک (HTML) برای مدیریت فروشگاه و کاربران است. // همه‌ی صفحات پشت Basic Auth هستند و در همان باینری Go سرو می‌شوند (بدون پروسه‌ی جدا). package admin import ( "context" "database/sql" "embed" "html/template" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "hakemsho/internal/economy" ) //go:embed templates/*.html var tplFS embed.FS type Handler struct { db *sql.DB eco *economy.Service tpl *template.Template } func New(db *sql.DB, eco *economy.Service) *Handler { tpl := template.Must(template.ParseFS(tplFS, "templates/*.html")) return &Handler{db: db, eco: eco, tpl: tpl} } // Routes زیرروترِ /admin را با Basic Auth برمی‌گرداند. func (h *Handler) Routes(user, pass string) http.Handler { r := chi.NewRouter() r.Use(middleware.BasicAuth("hakemsho-admin", map[string]string{user: pass})) r.Get("/", h.dashboard) r.Get("/shop", h.shop) r.Post("/shop/{kind}/add", h.addItem) r.Post("/shop/{kind}/delete", h.deleteItem) r.Post("/shop/{kind}", h.updateItem) r.Get("/users", h.users) r.Post("/users/coins", h.adjustCoins) r.Post("/users/grant", h.grantToUser) return r } func (h *Handler) render(w http.ResponseWriter, page string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := h.tpl.ExecuteTemplate(w, page, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // --- داشبورد --- func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() count := func(q string) int64 { var n int64 _ = h.db.QueryRowContext(ctx, q).Scan(&n) return n } data := map[string]any{ "Users": count(`SELECT COUNT(*) FROM users`), "Purchases": count(`SELECT COUNT(*) FROM purchases WHERE status='verified'`), "Games": count(`SELECT COUNT(*) FROM game_history`), "Coins": count(`SELECT COALESCE(SUM(coins),0) FROM users`), "Nav": "dashboard", } h.render(w, "dashboard.html", data) } // --- فروشگاه (ویرایش کاتالوگ) --- func (h *Handler) shop(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data := map[string]any{ "Nav": "shop", "Coins": h.rows(ctx, `SELECT id,title,coins,vip_days,price_toman,bonus_pct,sort,enabled FROM coin_packages ORDER BY sort`, "id", "title", "coins", "vip_days", "price_toman", "bonus_pct", "sort", "enabled"), "Tickets": h.rows(ctx, `SELECT id,title,tickets,price_toman,sort,enabled FROM ticket_packages ORDER BY sort`, "id", "title", "tickets", "price_toman", "sort", "enabled"), "Cards": h.rows(ctx, `SELECT id,title,price_coins,sort,enabled FROM card_skins ORDER BY sort`, "id", "title", "price_coins", "sort", "enabled"), "Boosters": h.rows(ctx, `SELECT id,title,multiplier,hours,price_toman,sort,enabled FROM boosters ORDER BY sort`, "id", "title", "multiplier", "hours", "price_toman", "sort", "enabled"), "VIP": h.rows(ctx, `SELECT id,title,months,price_toman,sort,enabled FROM vip_packages ORDER BY sort`, "id", "title", "months", "price_toman", "sort", "enabled"), "Tiers": h.rows(ctx, `SELECT id,title,hands,entry,prize,xp,trophy,sort,enabled FROM table_tiers ORDER BY sort`, "id", "title", "hands", "entry", "prize", "xp", "trophy", "sort", "enabled"), "Saved": r.URL.Query().Get("saved") == "1", } h.render(w, "shop.html", data) } // updateItem یک ردیفِ کاتالوگ را بر اساس kind به‌روزرسانی و کش را تازه می‌کند. func (h *Handler) updateItem(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } kind := chi.URLParam(r, "kind") id := r.FormValue("id") f := func(k string) int { v, _ := strconv.Atoi(r.FormValue(k)); return v } en := 0 if r.FormValue("enabled") == "on" { en = 1 } var q string var args []any switch kind { case "coin": q = `UPDATE coin_packages SET title=?,coins=?,vip_days=?,price_toman=?,bonus_pct=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("coins"), f("vip_days"), f("price_toman"), f("bonus_pct"), f("sort"), en, id} case "ticket": q = `UPDATE ticket_packages SET title=?,tickets=?,price_toman=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("tickets"), f("price_toman"), f("sort"), en, id} case "card": q = `UPDATE card_skins SET title=?,price_coins=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("price_coins"), f("sort"), en, id} case "booster": q = `UPDATE boosters SET title=?,multiplier=?,hours=?,price_toman=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("multiplier"), f("hours"), f("price_toman"), f("sort"), en, id} case "vip": q = `UPDATE vip_packages SET title=?,months=?,price_toman=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("months"), f("price_toman"), f("sort"), en, id} case "tier": q = `UPDATE table_tiers SET title=?,hands=?,entry=?,prize=?,xp=?,trophy=?,sort=?,enabled=? WHERE id=?` args = []any{r.FormValue("title"), f("hands"), f("entry"), f("prize"), f("xp"), f("trophy"), f("sort"), en, id} default: http.Error(w, "unknown kind", http.StatusBadRequest) return } if _, err := h.db.ExecContext(r.Context(), q, args...); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _ = h.eco.LoadCatalog(r.Context()) // تازه‌سازی کشِ کاتالوگ http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther) } // addItem یک ردیفِ جدید به کاتالوگ اضافه می‌کند (افزودن محصول). func (h *Handler) addItem(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } kind := chi.URLParam(r, "kind") id := r.FormValue("id") if id == "" { http.Error(w, "id required", http.StatusBadRequest) return } f := func(k string) int { v, _ := strconv.Atoi(r.FormValue(k)); return v } en := 0 if r.FormValue("enabled") == "on" { en = 1 } var q string var args []any switch kind { case "coin": q = `INSERT INTO coin_packages (id,title,coins,vip_days,price_toman,bonus_pct,sort,enabled) VALUES (?,?,?,?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("coins"), f("vip_days"), f("price_toman"), f("bonus_pct"), f("sort"), en} case "ticket": q = `INSERT INTO ticket_packages (id,title,tickets,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("tickets"), f("price_toman"), f("sort"), en} case "card": q = `INSERT INTO card_skins (id,title,price_coins,sort,enabled) VALUES (?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("price_coins"), f("sort"), en} case "booster": q = `INSERT INTO boosters (id,title,multiplier,hours,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("multiplier"), f("hours"), f("price_toman"), f("sort"), en} case "vip": q = `INSERT INTO vip_packages (id,title,months,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("months"), f("price_toman"), f("sort"), en} case "tier": q = `INSERT INTO table_tiers (id,title,hands,entry,prize,xp,trophy,sort,enabled) VALUES (?,?,?,?,?,?,?,?,?)` args = []any{id, r.FormValue("title"), f("hands"), f("entry"), f("prize"), f("xp"), f("trophy"), f("sort"), en} default: http.Error(w, "unknown kind", http.StatusBadRequest) return } if _, err := h.db.ExecContext(r.Context(), q, args...); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _ = h.eco.LoadCatalog(r.Context()) http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther) } // deleteItem یک ردیفِ کاتالوگ را حذف می‌کند. func (h *Handler) deleteItem(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } tables := map[string]string{ "coin": "coin_packages", "ticket": "ticket_packages", "card": "card_skins", "booster": "boosters", "vip": "vip_packages", "tier": "table_tiers", } tbl, ok := tables[chi.URLParam(r, "kind")] if !ok { http.Error(w, "unknown kind", http.StatusBadRequest) return } if _, err := h.db.ExecContext(r.Context(), "DELETE FROM "+tbl+" WHERE id = ?", r.FormValue("id")); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _ = h.eco.LoadCatalog(r.Context()) http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther) } // --- کاربران --- func (h *Handler) users(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query().Get("q") query := `SELECT id, mobile, coins, tickets, xp, trophies, is_admin FROM users` var args []any if q != "" { query += ` WHERE mobile LIKE ?` args = append(args, "%"+q+"%") } query += ` ORDER BY id DESC LIMIT 100` rows, err := h.db.QueryContext(ctx, query, args...) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer rows.Close() type userRow struct { ID int64 Mobile string Coins, Tickets, XP, Trophy int64 Level int IsAdmin bool } var list []userRow for rows.Next() { var u userRow var admin int if err := rows.Scan(&u.ID, &u.Mobile, &u.Coins, &u.Tickets, &u.XP, &u.Trophy, &admin); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } u.IsAdmin = admin == 1 u.Level, _, _ = economy.LevelInfo(u.XP) list = append(list, u) } h.render(w, "users.html", map[string]any{ "Nav": "users", "Users": list, "Q": q, "Saved": r.URL.Query().Get("saved") == "1", }) } // adjustCoins سکه‌ی یک کاربر را افزایش/کاهش می‌دهد (لاگ در wallet_tx). func (h *Handler) adjustCoins(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } userID, _ := strconv.ParseInt(r.FormValue("user_id"), 10, 64) amount, _ := strconv.ParseInt(r.FormValue("amount"), 10, 64) if userID > 0 && amount != 0 { _ = h.eco.Adjust(r.Context(), userID, economy.CurrencyCoin, amount, "admin_adjust", "") } http.Redirect(w, r, "/admin/users?saved=1", http.StatusSeeOther) } // grantToUser یک محصول (سکه/بلیط/VIP/کارت) را به کاربر اعطا می‌کند. func (h *Handler) grantToUser(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } userID, _ := strconv.ParseInt(r.FormValue("user_id"), 10, 64) kind := r.FormValue("kind") value := r.FormValue("value") ctx := r.Context() if userID <= 0 { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } var err error switch kind { case "coins": amount, _ := strconv.ParseInt(value, 10, 64) err = h.eco.Adjust(ctx, userID, economy.CurrencyCoin, amount, "admin_grant", "") case "tickets": amount, _ := strconv.ParseInt(value, 10, 64) err = h.eco.Adjust(ctx, userID, economy.CurrencyTicket, amount, "admin_grant", "") case "vip": days, _ := strconv.Atoi(value) err = h.eco.GrantVIPDays(ctx, userID, days) case "card": err = h.eco.GrantCard(ctx, userID, value) } if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } http.Redirect(w, r, "/admin/users?saved=1", http.StatusSeeOther) } // rows یک کوئری را به []map[string]any تبدیل می‌کند (برای رندرِ عمومیِ جدول‌ها). func (h *Handler) rows(ctx context.Context, query string, cols ...string) []map[string]any { rs, err := h.db.QueryContext(ctx, query) if err != nil { return nil } defer rs.Close() var out []map[string]any for rs.Next() { vals := make([]any, len(cols)) ptrs := make([]any, len(cols)) for i := range vals { ptrs[i] = &vals[i] } if err := rs.Scan(ptrs...); err != nil { return out } m := make(map[string]any, len(cols)) for i, c := range cols { m[c] = vals[i] } out = append(out, m) } return out }