diff --git a/internal/api/admin/admin.go b/internal/api/admin/admin.go new file mode 100644 index 0000000..9968af7 --- /dev/null +++ b/internal/api/admin/admin.go @@ -0,0 +1,16 @@ +package admin + +import ( + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + "gorm.io/gorm" +) + +type Service struct { + db *gorm.DB +} + +func New(b *telegram.TipBot) Service { + return Service{ + db: b.Database, + } +} diff --git a/internal/api/admin/ban.go b/internal/api/admin/ban.go new file mode 100644 index 0000000..219859d --- /dev/null +++ b/internal/api/admin/ban.go @@ -0,0 +1,65 @@ +package admin + +import ( + "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "net/http" + "strings" +) + +func (s Service) UnbanUser(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserByTelegramId(r) + if err != nil { + log.Errorf("[ADMIN] could not ban user: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + if !user.Banned { + log.Infof("[ADMIN] user already banned") + w.WriteHeader(http.StatusBadRequest) + return + } + user.Banned = false + adminSlice := strings.Split(user.Wallet.Adminkey, "_") + user.Wallet.Adminkey = adminSlice[len(adminSlice)-1] + s.db.Save(user) + log.Infof("[ADMIN] Unbanned user (%s)", user.ID) + w.WriteHeader(http.StatusOK) +} + +func (s Service) BanUser(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserByTelegramId(r) + if err != nil { + log.Errorf("[ADMIN] could not ban user: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + if user.Banned { + w.WriteHeader(http.StatusBadRequest) + log.Infof("[ADMIN] user already banned") + return + } + user.Banned = true + if reason := r.URL.Query().Get("reason"); reason != "" { + user.Wallet.Adminkey = fmt.Sprintf("%s_%s", reason, user.Wallet.Adminkey) + } + user.Wallet.Adminkey = fmt.Sprintf("%s_%s", "banned", user.Wallet.Adminkey) + s.db.Save(user) + log.Infof("[ADMIN] Banned user (%s)", user.ID) + w.WriteHeader(http.StatusOK) +} + +func (s Service) getUserByTelegramId(r *http.Request) (*lnbits.User, error) { + user := &lnbits.User{} + v := mux.Vars(r) + if v["id"] == "" { + return nil, fmt.Errorf("invalid id") + } + tx := s.db.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) + if tx.Error != nil { + return nil, tx.Error + } + return user, nil +} diff --git a/internal/api/server.go b/internal/api/server.go index a9adcb4..f9a38ab 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -20,9 +20,9 @@ const ( StatusOk = "OK" ) -func NewServer() *Server { +func NewServer(address string) *Server { srv := &http.Server{ - Addr: internal.Configuration.Bot.LNURLServerUrl.Host, + Addr: address, // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, @@ -40,6 +40,9 @@ func NewServer() *Server { func (w *Server) ListenAndServe() { go w.httpServer.ListenAndServe() } +func (w *Server) PathPrefix(path string, handler http.Handler) { + w.router.PathPrefix(path).Handler(handler) +} func (w *Server) AppendRoute(path string, handler func(http.ResponseWriter, *http.Request), methods ...string) { r := w.router.HandleFunc(path, LoggingMiddleware("API", handler)) if len(methods) > 0 { diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 30dee1e..0445a22 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -15,17 +15,18 @@ type Client struct { } type User struct { - ID string `json:"id"` - Name string `json:"name" gorm:"primaryKey"` - Initialized bool `json:"initialized"` - Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` - Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` - StateKey UserStateKey `json:"stateKey"` - StateData string `json:"stateData"` - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` - AnonID string `json:"anon_id"` + ID string `json:"id"` + Name string `json:"name" gorm:"primaryKey"` + Initialized bool `json:"initialized"` + Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` + Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` + StateKey UserStateKey `json:"stateKey"` + StateData string `json:"stateData"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` + AnonID string `json:"anon_id"` AnonIDSha256 string `json:"anon_id_sha256"` + Banned bool `json:"banned"` } const ( diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 201f58c..e01d97c 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -3,10 +3,11 @@ package mutex import ( "context" "fmt" - "github.com/gorilla/mux" "net/http" "sync" + "github.com/gorilla/mux" + cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" ) @@ -18,14 +19,14 @@ func init() { } func ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock endpoint to unlock all users", len(mutexMap.Keys()), mutexMap.Keys()))) + w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock/{id} endpoint to mutex", len(mutexMap.Keys()), mutexMap.Keys()))) } func UnlockHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) if m, ok := mutexMap.Get(vars["id"]); ok { m.(*sync.Mutex).Unlock() - w.Write([]byte(fmt.Sprintf("Unlocked %s mutexe.\nCurrent number of locks: %d\nLocks: %+v", + w.Write([]byte(fmt.Sprintf("Unlocked mutex %s.\nCurrent number of locks: %d\nLocks: %+v", vars["id"], len(mutexMap.Keys()), mutexMap.Keys()))) return } @@ -108,6 +109,6 @@ func Unlock(s string) { log.Tracef("[Mutex] Unlocked %s", s) } else { // this should never happen. Mutex should have been in the mutexMap. - log.Errorf("[Mutex] ⚠⚠⚠️ Unlock %s not in mutexMap. Skip.", s) + log.Errorf("[Mutex] ⚠️⚠️⚠️ Unlock %s not in mutexMap. Skip.", s) } } diff --git a/main.go b/main.go index b5ea0a8..d60dd02 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ package main import ( + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/api/admin" "github.com/LightningTipBot/LightningTipBot/internal/lndhub" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" - "github.com/gorilla/mux" "net/http" "runtime/debug" @@ -40,7 +41,7 @@ func startApiServer(bot *telegram.TipBot) { // start internal webhook server webhook.NewServer(bot) // start external api server - s := api.NewServer() + s := api.NewServer(internal.Configuration.Bot.LNURLServerUrl.Host) // append lnurl handler functions lnUrl := lnurl.New(bot) @@ -53,11 +54,14 @@ func startApiServer(bot *telegram.TipBot) { s.AppendRoute(`/lndhub/ext`, hub.Handle) // start internal admin server - router := mux.NewRouter() - router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) - router.Handle("/mutex", http.HandlerFunc(mutex.ServeHTTP)) - router.Handle("/mutex/unlock/{id}", http.HandlerFunc(mutex.UnlockHTTP)) - go http.ListenAndServe("0.0.0.0:6060", router) + adminService := admin.New(bot) + internalAdminServer := api.NewServer("0.0.0.0:6060") + internalAdminServer.AppendRoute("/mutex", mutex.ServeHTTP) + internalAdminServer.AppendRoute("/mutex/unlock/{id}", mutex.UnlockHTTP) + internalAdminServer.AppendRoute("/admin/ban/{id}", adminService.BanUser) + internalAdminServer.AppendRoute("/admin/unban/{id}", adminService.UnbanUser) + internalAdminServer.PathPrefix("/debug/pprof/", http.DefaultServeMux) + } func withRecovery() {