feature/block ttl (#108)

* add ttl to blocking #88

* remove unused parsing code

* add comments explaining TTL for blocking
This commit is contained in:
Umputun
2018-06-24 15:43:32 -05:00
committed by GitHub
parent b5b7bc50af
commit c316b27fe1
7 changed files with 111 additions and 33 deletions
+3 -3
View File
@@ -428,13 +428,13 @@ Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i
### Admin
* `DELETE /api/v1/admin/comment/{id}?site=site-id&url=post-url` - delete comment by `id`.
* `PUT /api/v1/admin/user/{userid}?site=site-id&block=1` - block or unblock user.
* `GET api/v1/admin/blocked&site=site-id` - list of blocked user ids.
* `PUT /api/v1/admin/user/{userid}?site=site-id&block=1&ttl=7d` - block or unblock user with optional ttl (default=permanent)
* `GET api/v1/admin/blocked&site=site-id` - list of blocked user ids
```go
type BlockedUser struct {
ID string `json:"id"`
Name string `json:"name"`
Timestamp time.Time `json:"time"`
Until time.Time `json:"time"`
}
```
* `GET /api/v1/admin/export?site=side-id&mode=[stream|file]` - export all comments to json stream or gz file.
+9 -2
View File
@@ -117,13 +117,20 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, JSON{"user_id": claims.User.ID, "site_id": claims.SiteID})
}
// PUT /user/{userid}?site=side-id&block=1 - block or unblock user
// PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user
func (a *admin) 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 := a.dataService.SetBlock(siteID, userID, blockStatus); err != nil {
ttl := time.Duration(0) // unlimited duration by default
if ttlParam := r.URL.Query().Get("ttl"); ttlParam != "" {
if d, err := time.ParseDuration(ttlParam); err == nil {
ttl = d
}
}
if err := a.dataService.SetBlock(siteID, userID, blockStatus, ttl); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set blocking status")
return
}
+42 -6
View File
@@ -161,10 +161,13 @@ func TestAdmin_Block(t *testing.T) {
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
block := func(val int) (code int, body []byte) {
block := func(val int, ttl string) (code int, body []byte) {
client := http.Client{}
req, e := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", val), nil)
url := fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", val)
if ttl != "" {
url = url + "&ttl=" + ttl
}
req, e := http.NewRequest(http.MethodPut, url, nil)
assert.Nil(t, e)
req.SetBasicAuth("dev", "password")
resp, e := client.Do(req)
@@ -175,7 +178,8 @@ func TestAdmin_Block(t *testing.T) {
return resp.StatusCode, body
}
code, body := block(1)
// block permanently
code, body := block(1, "")
require.Equal(t, 200, code)
j := JSON{}
err = json.Unmarshal(body, &j)
@@ -193,11 +197,34 @@ func TestAdmin_Block(t *testing.T) {
assert.Equal(t, "", comments.Comments[0].Text)
assert.True(t, comments.Comments[0].Deleted)
code, body = block(-1)
code, body = block(-1, "")
require.Equal(t, 200, code)
err = json.Unmarshal(body, &j)
assert.Nil(t, err)
assert.Equal(t, false, j["block"])
// block with ttl
code, _ = block(1, "10ms")
require.Equal(t, 200, code)
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
comments = commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, "", comments.Comments[0].Text)
assert.True(t, comments.Comments[0].Deleted)
time.Sleep(11 * time.Millisecond)
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
comments = commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, "test test #1", comments.Comments[0].Text)
assert.False(t, comments.Comments[0].Deleted)
}
func TestAdmin_BlockedList(t *testing.T) {
@@ -217,7 +244,7 @@ func TestAdmin_BlockedList(t *testing.T) {
// block user2
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user2", 1), nil)
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d&ttl=10ms", ts.URL, "user2", 1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
@@ -231,6 +258,15 @@ func TestAdmin_BlockedList(t *testing.T) {
assert.Equal(t, 2, len(users), "two users blocked")
assert.Equal(t, "user1", users[0].ID)
assert.Equal(t, "user2", users[1].ID)
time.Sleep(11 * time.Millisecond)
res, code = getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t")
require.Equal(t, 200, code, res)
users = []store.BlockedUser{}
err = json.Unmarshal([]byte(res), &users)
assert.Nil(t, err)
assert.Equal(t, 1, len(users), "one user left blocked")
}
func TestAdmin_ReadOnly(t *testing.T) {
+3 -3
View File
@@ -50,9 +50,9 @@ type PostInfo struct {
// BlockedUser holds id and ts for blocked user
type BlockedUser struct {
ID string `json:"id"`
Name string `json:"name"`
Timestamp time.Time `json:"time"`
ID string `json:"id"`
Name string `json:"name"`
Until time.Time `json:"time"`
}
// DeleteMode defines how much comment info will be erased
+28 -11
View File
@@ -155,8 +155,9 @@ func (b *BoltDB) DeleteUser(siteID string, userID string) error {
return err
}
// SetBlock blocks/unblocks user for given site
func (b *BoltDB) SetBlock(siteID string, userID string, status bool) error {
// SetBlock blocks/unblocks user for given site. ttl defines for for how long, 0 - permanent
// block uses blocksBucketName with key=userID and val=TTL+now
func (b *BoltDB) SetBlock(siteID string, userID string, status bool, ttl time.Duration) error {
bdb, err := b.db(siteID)
if err != nil {
@@ -167,7 +168,11 @@ func (b *BoltDB) SetBlock(siteID string, userID string, status bool) error {
bucket := tx.Bucket([]byte(blocksBucketName))
switch status {
case true:
if e := bucket.Put([]byte(userID), []byte(time.Now().Format(tsNano))); e != nil {
val := time.Now().AddDate(100, 0, 0).Format(tsNano) // permanent is 50year
if ttl > 0 {
val = time.Now().Add(ttl).Format(tsNano)
}
if e := bucket.Put([]byte(userID), []byte(val)); e != nil {
return errors.Wrapf(e, "failed to put %s to %s", userID, blocksBucketName)
}
case false:
@@ -189,7 +194,18 @@ func (b *BoltDB) IsBlocked(siteID string, userID string) (blocked bool) {
_ = bdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blocksBucketName))
blocked = bucket.Get([]byte(userID)) != nil
val := bucket.Get([]byte(userID))
if val == nil {
blocked = false
return nil
}
until, err := time.Parse(tsNano, string(val))
if err != nil {
blocked = false
return nil
}
blocked = time.Now().Before(until)
return nil
})
return blocked
@@ -211,14 +227,15 @@ func (b *BoltDB) Blocked(siteID string) (users []store.BlockedUser, err error) {
if e != nil {
return errors.Wrap(e, "can't parse block ts")
}
// get user name from comment user section
userName := ""
userComments, e := b.User(siteID, string(k), 1, 0)
if e == nil && len(userComments) > 0 {
userName = userComments[0].User.Name
if time.Now().Before(ts) {
// get user name from comment user section
userName := ""
userComments, e := b.User(siteID, string(k), 1, 0)
if e == nil && len(userComments) > 0 {
userName = userComments[0].User.Name
}
users = append(users, store.BlockedUser{ID: string(k), Name: userName, Until: ts})
}
users = append(users, store.BlockedUser{ID: string(k), Name: userName, Timestamp: ts})
return nil
})
})
+24 -7
View File
@@ -3,6 +3,7 @@ package engine
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -126,27 +127,37 @@ func TestBoltAdmin_BlockUser(t *testing.T) {
assert.False(t, b.IsBlocked("radio-t", "user1"), "nothing blocked")
assert.NoError(t, b.SetBlock("radio-t", "user1", true))
assert.NoError(t, b.SetBlock("radio-t", "user1", true, 0))
assert.True(t, b.IsBlocked("radio-t", "user1"), "user1 blocked")
assert.False(t, b.IsBlocked("radio-t", "user2"), "user2 still unblocked")
assert.NoError(t, b.SetBlock("radio-t", "user1", false))
assert.NoError(t, b.SetBlock("radio-t", "user1", false, 0))
assert.False(t, b.IsBlocked("radio-t", "user1"), "user1 unblocked")
assert.EqualError(t, b.SetBlock("bad", "user1", true), `site "bad" not found`)
assert.NoError(t, b.SetBlock("radio-t", "userX", false))
assert.EqualError(t, b.SetBlock("bad", "user1", true, 0), `site "bad" not found`)
assert.NoError(t, b.SetBlock("radio-t", "userX", false, 0))
assert.False(t, b.IsBlocked("radio-t-bad", "user1"), "nothing blocked on wrong site")
}
func TestBoltAdmin_BlockUserWithTTL(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.False(t, b.IsBlocked("radio-t", "user1"), "nothing blocked")
assert.NoError(t, b.SetBlock("radio-t", "user1", true, 10*time.Millisecond))
assert.True(t, b.IsBlocked("radio-t", "user1"), "user1 blocked")
time.Sleep(11 * time.Millisecond)
assert.False(t, b.IsBlocked("radio-t", "user1"), "user1 un-blocked automatically")
}
func TestBoltAdmin_BlockList(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.NoError(t, b.SetBlock("radio-t", "user1", true))
assert.NoError(t, b.SetBlock("radio-t", "user2", true))
assert.NoError(t, b.SetBlock("radio-t", "user3", false))
assert.NoError(t, b.SetBlock("radio-t", "user1", true, 0))
assert.NoError(t, b.SetBlock("radio-t", "user2", true, 10*time.Millisecond))
assert.NoError(t, b.SetBlock("radio-t", "user3", false, 0))
ids, err := b.Blocked("radio-t")
assert.NoError(t, err)
@@ -156,6 +167,12 @@ func TestBoltAdmin_BlockList(t *testing.T) {
assert.Equal(t, "user2", ids[1].ID)
t.Logf("%+v", ids)
time.Sleep(11 * time.Millisecond)
ids, err = b.Blocked("radio-t")
assert.NoError(t, err)
assert.Equal(t, 1, len(ids))
assert.Equal(t, "user1", ids[0].ID)
_, err = b.Blocked("bad")
assert.EqualError(t, err, `site "bad" not found`)
}
+2 -1
View File
@@ -5,6 +5,7 @@ package engine
import (
"sort"
"strings"
"time"
"github.com/umputun/remark/backend/app/store"
)
@@ -44,7 +45,7 @@ type Admin interface {
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error // delete comment by id
DeleteAll(siteID string) error // delete all data from site
DeleteUser(siteID string, userID string) error // remove all comments from user
SetBlock(siteID string, userID string, status bool) error // block or unblock user
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error // block or unblock user with TTL (0-permanent)
IsBlocked(siteID string, userID string) bool // check if user blocked
Blocked(siteID string) ([]store.BlockedUser, error) // get list of blocked users
SetReadOnly(locator store.Locator, status bool) error // set/reset read-only flag