diff --git a/README.md b/README.md index 670a2e23..946847e0 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,5 @@ type Locator struct { - `GET /api/v1/id/{id}` - get comment by `id` - `GET /api/v1/count?url=post-url` - get comment's count for `{url}` - `PUT /api/v1/vote/{id}?url=post-url&vote=1` - vote for comment. `vote`=1 will increase score, -1 decreases. _auth required_ -- `DELETE /api/v1/comment/{id}?url=post-url` - delete comment by `id`. _auth and admin required_ \ No newline at end of file +- `DELETE /api/v1/comment/{id}?url=post-url` - delete comment by `id`. _auth and admin required_ +- `PUT /user/{userid}?site=side-id&block=1` - block or unblock user. _auth and admin required_ diff --git a/app/rest/moderator.go b/app/rest/moderator.go index b6462be5..5578567d 100644 --- a/app/rest/moderator.go +++ b/app/rest/moderator.go @@ -18,6 +18,7 @@ func (m *moderator) routes() chi.Router { router := chi.NewRouter() router.Use(AdminOnly) router.Delete("/comment/{id}", m.deleteCommentCtrl) + router.Put("/user/{userid}", m.setBlockCtrl) return router } @@ -28,6 +29,7 @@ func (m *moderator) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("[WARN] bad id %s", chi.URLParam(r, "id")) httpError(w, r, http.StatusBadRequest, err, "can't parse id") + return } log.Printf("[INFO] delete comment %d", id) @@ -43,3 +45,17 @@ func (m *moderator) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(w, r, JSON{"id": id, "url": url}) } + +// PUT /user/{userid}?site=side-id&block=1 +func (m *moderator) setBlockCtrl(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "userid") + siteID := r.URL.Query().Get("site") + blockStatus := r.URL.Query().Get("block") == "1" + + if err := m.dataStore.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil { + httpError(w, r, http.StatusBadRequest, err, "can't set blocking status") + return + } + + render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) +} diff --git a/app/rest/server.go b/app/rest/server.go index bf6c0707..bc165ed2 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -1,6 +1,7 @@ package rest import ( + "errors" "html/template" "log" "net/http" @@ -98,7 +99,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { } user, err := GetUserInfo(r) - if err != nil { + if err != nil { // this not suppose to happen (handled by Auth), just dbl-check httpError(w, r, http.StatusUnauthorized, err, "can't get user info") return } @@ -114,6 +115,13 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[INFO] create comment %+v", comment) + // check if user blocked + if s.Store.IsBlocked(store.Locator{}, comment.User.ID) { + log.Printf("[WARN] user %s rejected (blocked)", err) + httpError(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") + return + } + id, err := s.Store.Create(comment) if err != nil { log.Printf("[WARN] can't save comment, %s", err) diff --git a/app/store/bolt.go b/app/store/bolt.go index 59c755bd..56a915d6 100644 --- a/app/store/bolt.go +++ b/app/store/bolt.go @@ -20,7 +20,10 @@ type BoltDB struct { *bolt.DB } -var lastBucketName = "last" +var ( + lastBucketName = "last" + blocksBucketPrefix = "block-" +) // NewBoltDB makes persistent boltdb-based store func NewBoltDB(dbFile string) (*BoltDB, error) { @@ -270,6 +273,48 @@ func (b *BoltDB) Count(locator Locator) (count int, err error) { return count, err } +// SetBlock blocks/unblocks user for given site +func (b *BoltDB) SetBlock(locator Locator, userID string, status bool) error { + blockBucketName := b.bucketForBlock(locator, userID) + return b.Update(func(tx *bolt.Tx) error { + + bucket, e := tx.CreateBucketIfNotExists([]byte(blockBucketName)) + if e != nil { + return errors.Errorf("no bucket %s in store", string(blockBucketName)) + } + + switch status { + case true: + if e := bucket.Put([]byte(userID), []byte(time.Now().Format(time.RFC3339))); e != nil { + return errors.Wrapf(e, "failed to put %s to %s", userID, string(blockBucketName)) + } + case false: + if e := bucket.Delete([]byte(userID)); e != nil { + return errors.Wrapf(e, "failed to clean %s from %s", userID, string(blockBucketName)) + } + } + return nil + }) +} + +// IsBlocked checks if user blocked +func (b *BoltDB) IsBlocked(locator Locator, userID string) (result bool) { + blockBucketName := b.bucketForBlock(locator, userID) + _ = b.View(func(tx *bolt.Tx) error { + result = false + bucket := tx.Bucket(blockBucketName) + if bucket != nil && bucket.Get([]byte(userID)) != nil { + result = true + } + return nil + }) + return result +} + +func (b *BoltDB) bucketForBlock(locator Locator, userID string) []byte { + return []byte(fmt.Sprintf("%s%s", blocksBucketPrefix, locator.SiteID)) +} + func (b *BoltDB) keyFromComment(comment Comment) []byte { return []byte(fmt.Sprintf("%22d", comment.ID)) } diff --git a/app/store/bolt_test.go b/app/store/bolt_test.go index a0a0666e..f24640e3 100644 --- a/app/store/bolt_test.go +++ b/app/store/bolt_test.go @@ -27,17 +27,22 @@ func TestBoltDB_Delete(t *testing.T) { defer os.Remove(testDb) b := prep(t) - res, err := b.Find(Request{Locator: Locator{URL: "https://radio-t.com"}}) + loc := Locator{URL: "https://radio-t.com"} + res, err := b.Find(Request{Locator: loc}) assert.Nil(t, err) assert.Equal(t, 2, len(res)) - err = b.Delete(Locator{URL: "https://radio-t.com"}, res[0].ID) + err = b.Delete(loc, res[0].ID) assert.Nil(t, err) - res, err = b.Find(Request{Locator: Locator{URL: "https://radio-t.com"}}) + res, err = b.Find(Request{Locator: loc}) assert.Nil(t, err) assert.Equal(t, 1, len(res)) assert.Equal(t, "some text2", res[0].Text) + + comments, err := b.Last(loc, 10) + assert.Nil(t, err) + assert.Equal(t, 1, len(comments), "only 1 left in last") } func TestBoltDB_Get(t *testing.T) { @@ -105,6 +110,22 @@ func TestBoltDB_Count(t *testing.T) { assert.Equal(t, 2, c) } +func TestBoltDB_BlockUser(t *testing.T) { + defer os.Remove(testDb) + b := prep(t) + + assert.False(t, b.IsBlocked(Locator{SiteID: "site1"}, "user1"), "nothing blocked") + + assert.NoError(t, b.SetBlock(Locator{SiteID: "site1"}, "user1", true)) + assert.True(t, b.IsBlocked(Locator{SiteID: "site1"}, "user1"), "user1 blocked") + + assert.False(t, b.IsBlocked(Locator{SiteID: "site1"}, "user2"), "user2 still unblocked") + + assert.NoError(t, b.SetBlock(Locator{SiteID: "site1"}, "user1", false)) + assert.False(t, b.IsBlocked(Locator{SiteID: "site1"}, "user1"), "user1 unblocked") + +} + // makes new boltdb, put two records func prep(t *testing.T) *BoltDB { os.Remove(testDb) diff --git a/app/store/store.go b/app/store/store.go index afd451da..7e88d6d7 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -47,4 +47,7 @@ type Interface interface { Get(locator Locator, id int64) (Comment, error) Vote(locator Locator, commentID int64, userID string, val bool) (Comment, error) Count(locator Locator) (int, error) + + SetBlock(locator Locator, userID string, status bool) error + IsBlocked(locator Locator, userID string) bool }