From ff3761fcbd88bc7d15f7dd56108bc89c412f5266 Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Tue, 11 Apr 2023 09:16:04 +0200 Subject: [PATCH] Add API endpoint for log querying (admin) --- api/admin/logs.go | 72 +++++++++++++++++++++++++++ api/admin/logs_test.go | 38 ++++++++++++++ api/admin/router.go | 24 +++++++++ api/config/config_test.go | 12 +---- api/helpers/apiTest/mockBotApiUser.go | 11 ++++ api/helpers/chatId.go | 17 +++++++ api/server.go | 6 ++- 7 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 api/admin/logs.go create mode 100644 api/admin/logs_test.go create mode 100644 api/admin/router.go diff --git a/api/admin/logs.go b/api/admin/logs.go new file mode 100644 index 0000000..ec26b0f --- /dev/null +++ b/api/admin/logs.go @@ -0,0 +1,72 @@ +package admin + +import ( + "database/sql" + "net/http" + "strconv" + "time" + + "github.com/LucaBernstein/beancount-bot-tg/db" + "github.com/gin-gonic/gin" +) + +type Log struct { + Created string `json:"createdOn"` + Chat string `json:"chat"` + Level int `json:"level"` + Message string `json:"message"` +} + +func GetLogs(from, to string, minLevel int) ([]Log, error) { + rows, err := db.Connection().Query(` + SELECT "created", "chat", "level", "message" + FROM "app::log" + WHERE "created" >= $1 AND "created" < $2 AND "level" >= $3 + ORDER BY "created" DESC + `, from, to, minLevel) + if err != nil { + return nil, err + } + logs := []Log{} + for rows.Next() { + logEntry := &Log{} + chatDb := sql.NullString{} + err := rows.Scan(&logEntry.Created, &chatDb, &logEntry.Level, &logEntry.Message) + if err != nil { + return nil, err + } + logEntry.Chat = chatDb.String + logs = append(logs, *logEntry) + } + return logs, nil +} + +func (r *Router) Logs(c *gin.Context) { + from := c.Query("from") + if from == "" { + from = time.Now().Add(-24 * time.Hour).Format(time.DateTime) + } + to := c.Query("to") + if to == "" { + to = time.Now().Format(time.DateTime) + } + minLevelQ := c.Query("minLevel") + if minLevelQ == "" { + minLevelQ = "0" + } + minLevel, err := strconv.Atoi(minLevelQ) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + logs, err := GetLogs(from, to, minLevel) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + c.JSON(http.StatusOK, logs) +} diff --git a/api/admin/logs_test.go b/api/admin/logs_test.go new file mode 100644 index 0000000..e8e4de4 --- /dev/null +++ b/api/admin/logs_test.go @@ -0,0 +1,38 @@ +package admin_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/LucaBernstein/beancount-bot-tg/api/admin" + "github.com/LucaBernstein/beancount-bot-tg/api/helpers/apiTest" + "github.com/LucaBernstein/beancount-bot-tg/bot/botTest" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestLogs(t *testing.T) { + token, mockBc, msg := apiTest.MockBcApiUser(t, 919) + r := gin.Default() + g := r.Group("") + admin.NewRouter(mockBc).Hook(g) + + // Should be forbidden without admin priviledges + err := apiTest.PromoteAdmin(msg.Chat.ID, false) + botTest.HandleErr(t, err) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/logs", nil) + req.Header.Add("Authorization", "Bearer "+token) + r.ServeHTTP(w, req) + assert.Equal(t, 403, w.Result().StatusCode) + + err = apiTest.PromoteAdmin(msg.Chat.ID, true) + botTest.HandleErr(t, err) + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/logs", nil) + req.Header.Add("Authorization", "Bearer "+token) + r.ServeHTTP(w, req) + assert.Equal(t, 200, w.Result().StatusCode) +} diff --git a/api/admin/router.go b/api/admin/router.go new file mode 100644 index 0000000..3f43bdf --- /dev/null +++ b/api/admin/router.go @@ -0,0 +1,24 @@ +package admin + +import ( + "github.com/LucaBernstein/beancount-bot-tg/api/helpers" + "github.com/LucaBernstein/beancount-bot-tg/bot" + "github.com/gin-gonic/gin" +) + +type Router struct { + bc *bot.BotController +} + +func NewRouter(bc *bot.BotController) *Router { + return &Router{ + bc: bc, + } +} + +func (r *Router) Hook(g *gin.RouterGroup) { + g.Use(helpers.AttachChatId(r.bc)) + g.Use(helpers.EnsureAdmin(r.bc)) + + g.GET("/logs", r.Logs) +} diff --git a/api/config/config_test.go b/api/config/config_test.go index a2a4174..775cdbe 100644 --- a/api/config/config_test.go +++ b/api/config/config_test.go @@ -10,20 +10,10 @@ import ( "github.com/LucaBernstein/beancount-bot-tg/api/helpers/apiTest" "github.com/LucaBernstein/beancount-bot-tg/bot/botTest" "github.com/LucaBernstein/beancount-bot-tg/db" - "github.com/LucaBernstein/beancount-bot-tg/helpers" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) -func PromoteAdmin(chatId int64) error { - _, err := db.Connection().Exec(`DELETE FROM "bot::userSetting" WHERE "tgChatId" = $1 AND "setting" = $2`, chatId, helpers.USERSET_ADM) - if err != nil { - return err - } - _, err = db.Connection().Exec(`INSERT INTO "bot::userSetting" ("tgChatId", "setting", "value") VALUES ($1, $2, $3)`, chatId, helpers.USERSET_ADM, "true") - return err -} - func AllSettingsTypes() ([]string, error) { rows, err := db.Connection().Query(`SELECT "setting" FROM "bot::userSettingTypes"`) if err != nil { @@ -43,7 +33,7 @@ func AllSettingsTypes() ([]string, error) { func TestFullConfigMap(t *testing.T) { token, mockBc, msg := apiTest.MockBcApiUser(t, 786) - err := PromoteAdmin(msg.Chat.ID) + err := apiTest.PromoteAdmin(msg.Chat.ID, true) botTest.HandleErr(t, err) settings, err := AllSettingsTypes() diff --git a/api/helpers/apiTest/mockBotApiUser.go b/api/helpers/apiTest/mockBotApiUser.go index 176cb12..c014e22 100644 --- a/api/helpers/apiTest/mockBotApiUser.go +++ b/api/helpers/apiTest/mockBotApiUser.go @@ -29,3 +29,14 @@ func MockBcApiUser(t *testing.T, id int64) (token string, mockBc *bot.BotControl return token, mockBc, msg } + +func PromoteAdmin(chatId int64, becomes bool) error { + _, err := db.Connection().Exec(`DELETE FROM "bot::userSetting" WHERE "tgChatId" = $1 AND "setting" = $2`, chatId, helpers.USERSET_ADM) + if err != nil { + return err + } + if becomes { + _, err = db.Connection().Exec(`INSERT INTO "bot::userSetting" ("tgChatId", "setting", "value") VALUES ($1, $2, $3)`, chatId, helpers.USERSET_ADM, "true") + } + return err +} diff --git a/api/helpers/chatId.go b/api/helpers/chatId.go index df0f118..55232e6 100644 --- a/api/helpers/chatId.go +++ b/api/helpers/chatId.go @@ -8,6 +8,7 @@ import ( "github.com/LucaBernstein/beancount-bot-tg/bot" "github.com/gin-gonic/gin" + "gopkg.in/telebot.v3" ) const K_CHAT_ID = "tgChatId" @@ -42,3 +43,19 @@ func AttachChatId(bc *bot.BotController) gin.HandlerFunc { c.Next() } } + +func EnsureAdmin(bc *bot.BotController) gin.HandlerFunc { + return func(c *gin.Context) { + tgChatId := c.GetInt64("tgChatId") + m := &telebot.Message{Chat: &telebot.Chat{ID: tgChatId}} + isAdmin := bc.Repo.UserIsAdmin(m) + if !isAdmin { + c.JSON(http.StatusForbidden, gin.H{ + "error": "missing priviledges", + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/api/server.go b/api/server.go index c968abb..353837a 100644 --- a/api/server.go +++ b/api/server.go @@ -3,8 +3,9 @@ package api import ( "log" - "github.com/LucaBernstein/beancount-bot-tg/api/token" + "github.com/LucaBernstein/beancount-bot-tg/api/admin" "github.com/LucaBernstein/beancount-bot-tg/api/config" + "github.com/LucaBernstein/beancount-bot-tg/api/token" "github.com/LucaBernstein/beancount-bot-tg/api/transactions" "github.com/LucaBernstein/beancount-bot-tg/bot" "github.com/gin-gonic/gin" @@ -26,6 +27,9 @@ func StartWebServer(bc *bot.BotController) { configGroup := apiGroup.Group("/config") config.NewRouter(bc).Hook(configGroup) + adminGroup := apiGroup.Group("/admin") + admin.NewRouter(bc).Hook(adminGroup) + port := ":8080" log.Printf("Web server started on %s", port) r.Run(port)