feature/block ttl (#108)
* add ttl to blocking #88 * remove unused parsing code * add comments explaining TTL for blocking
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user