admin support for one-click deleteme activation
This commit is contained in:
@@ -411,6 +411,7 @@ Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i
|
||||
* `DELETE /api/v1/admin/user/{userid}?site=site-id&block=1` - delete all user's comments.
|
||||
* `PUT /api/v1/admin/readonly?site=site-id&url=post-url&ro=1` - set read-only status
|
||||
* `PUT /api/v1/admin/verify/{userid}?site=site-id&verified=1` - set verified status
|
||||
* `GET /api/v1/admin/deleteme?token=token` - process deleteme user's request
|
||||
|
||||
_all admin calls require auth and admin privilege_
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ func New(opts Opts) (*Application, error) {
|
||||
DataService: dataService,
|
||||
Exporter: exporter,
|
||||
WebRoot: opts.WebRoot,
|
||||
RemarkURL: opts.RemarkURL,
|
||||
ImageProxy: &proxy.Image{Enabled: opts.ImageProxy, RoutePath: "/api/v1/img", RemarkURL: opts.RemarkURL},
|
||||
AvatarProxy: avatarProxy,
|
||||
ReadOnlyAge: opts.ReadOnlyAge,
|
||||
|
||||
+31
-5
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/umputun/remark/app/rest/auth"
|
||||
|
||||
"github.com/umputun/remark/app/migrator"
|
||||
"github.com/umputun/remark/app/rest"
|
||||
@@ -20,10 +21,11 @@ import (
|
||||
|
||||
// admin provides router for all requests available for admin users only
|
||||
type admin struct {
|
||||
dataService service.DataStore
|
||||
exporter migrator.Exporter
|
||||
cache cache.LoadingCache
|
||||
defAvatarURL string
|
||||
dataService service.DataStore
|
||||
exporter migrator.Exporter
|
||||
cache cache.LoadingCache
|
||||
defAvatarURL string
|
||||
authenticator auth.Authenticator
|
||||
}
|
||||
|
||||
func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Router {
|
||||
@@ -32,6 +34,7 @@ func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Route
|
||||
router.Delete("/comment/{id}", a.deleteCommentCtrl)
|
||||
router.Put("/user/{userid}", a.setBlockCtrl)
|
||||
router.Delete("/user/{userid}", a.deleteUserCtrl)
|
||||
router.Get("/deleteme", a.deleteMeRequestCtrl)
|
||||
router.Put("/verify/{userid}", a.setVerifyCtrl)
|
||||
router.Get("/export", a.exportCtrl)
|
||||
router.Put("/pin/{id}", a.setPinCtrl)
|
||||
@@ -57,7 +60,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"id": id, "locator": locator})
|
||||
}
|
||||
|
||||
// DELETE /user/{userid}?site=side-id
|
||||
// DELETE /user/{userid}?site=side-id - delete all user comments for requested userid
|
||||
func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID := chi.URLParam(r, "userid")
|
||||
@@ -73,6 +76,29 @@ func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID})
|
||||
}
|
||||
|
||||
// GET /deleteme?token=jwt - delete all user comments by user's request. Gets info about deleted used from provided token
|
||||
// request made GET to allow direct click from the email sent by user
|
||||
func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
claims, err := a.authenticator.JWTService.Parse(token)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't process token")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[INFO] delete all user comments by request for %s, site %s", claims.User.ID, claims.SiteID)
|
||||
|
||||
if err := a.dataService.DeleteUser(claims.SiteID, claims.User.ID); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete user")
|
||||
return
|
||||
}
|
||||
a.cache.Flush(claims.SiteID, claims.User.ID)
|
||||
render.Status(r, http.StatusOK)
|
||||
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
|
||||
func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "userid")
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/remark/app/rest/auth"
|
||||
"github.com/umputun/remark/app/store"
|
||||
)
|
||||
|
||||
@@ -364,3 +366,100 @@ func TestAdmin_ExportFile(t *testing.T) {
|
||||
assert.Equal(t, 2, strings.Count(string(ungzBody), "\"text\""))
|
||||
t.Logf("%s", string(ungzBody))
|
||||
}
|
||||
|
||||
func TestAdmin_DeleteMeRequest(t *testing.T) {
|
||||
srv, ts := prep(t)
|
||||
assert.NotNil(t, srv)
|
||||
defer cleanup(ts)
|
||||
|
||||
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
|
||||
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
|
||||
|
||||
_, err := srv.DataService.Create(c1)
|
||||
assert.Nil(t, err)
|
||||
_, err = srv.DataService.Create(c2)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comments, err := srv.DataService.User("radio-t", "user1", 0, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(comments), "a comment for user1")
|
||||
|
||||
claims := auth.CustomClaims{
|
||||
SiteID: "radio-t",
|
||||
SessionOnly: true,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: "1234567",
|
||||
Issuer: "remark42",
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
},
|
||||
User: &store.User{
|
||||
ID: "user1",
|
||||
},
|
||||
}
|
||||
|
||||
token, err := srv.Authenticator.JWTService.Token(&claims)
|
||||
assert.Nil(t, err)
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil)
|
||||
assert.Nil(t, err)
|
||||
req.SetBasicAuth("dev", "password")
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
_, err = srv.DataService.User("radio-t", "user1", 0, 0)
|
||||
assert.EqualError(t, err, "no comments for user user1 in store")
|
||||
}
|
||||
|
||||
func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
|
||||
srv, ts := prep(t)
|
||||
assert.NotNil(t, srv)
|
||||
defer cleanup(ts)
|
||||
|
||||
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
|
||||
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
|
||||
|
||||
_, err := srv.DataService.Create(c1)
|
||||
assert.Nil(t, err)
|
||||
_, err = srv.DataService.Create(c2)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// try with bad token
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, "bad token"), nil)
|
||||
assert.Nil(t, err)
|
||||
req.SetBasicAuth("dev", "password")
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
|
||||
// try with bad auth
|
||||
claims := auth.CustomClaims{
|
||||
SiteID: "radio-t",
|
||||
SessionOnly: true,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: "1234567",
|
||||
Issuer: "remark42",
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
},
|
||||
User: &store.User{
|
||||
ID: "user1",
|
||||
},
|
||||
}
|
||||
|
||||
token, err := srv.Authenticator.JWTService.Token(&claims)
|
||||
assert.Nil(t, err)
|
||||
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil)
|
||||
assert.Nil(t, err)
|
||||
req.SetBasicAuth("dev", "bad-password")
|
||||
resp, err = client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type Rest struct {
|
||||
AvatarProxy *proxy.Avatar
|
||||
ImageProxy *proxy.Image
|
||||
WebRoot string
|
||||
RemarkURL string
|
||||
ReadOnlyAge int
|
||||
ScoreThresholds struct {
|
||||
Low int
|
||||
@@ -100,9 +101,10 @@ func (s *Rest) routes() chi.Router {
|
||||
router.Use(AppInfo("remark42", s.Version), Ping)
|
||||
|
||||
s.adminService = admin{
|
||||
dataService: s.DataService,
|
||||
exporter: s.Exporter,
|
||||
cache: s.Cache,
|
||||
dataService: s.DataService,
|
||||
exporter: s.Exporter,
|
||||
cache: s.Cache,
|
||||
authenticator: s.Authenticator,
|
||||
}
|
||||
|
||||
ipFn := func(ip string) string { return store.HashValue(ip, s.DataService.Secret)[:12] } // logger uses it for anonymization
|
||||
|
||||
@@ -252,5 +252,6 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make token")
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, JSON{"site": siteID, "user_id": user.ID, "token": tokenStr})
|
||||
link := fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", s.RemarkURL, tokenStr)
|
||||
render.JSON(w, r, JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
|
||||
}
|
||||
|
||||
@@ -375,6 +375,7 @@ func TestRest_DeleteMe(t *testing.T) {
|
||||
claims, err := srv.Authenticator.JWTService.Parse(token)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "dev", claims.User.ID)
|
||||
assert.Equal(t, "https://demo.remark42.com/api/v1/admin/deleteme?token="+token, m["link"])
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -393,6 +393,8 @@ func TestRest_Info(t *testing.T) {
|
||||
assert.NotNil(t, srv)
|
||||
defer cleanup(ts)
|
||||
|
||||
srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only
|
||||
|
||||
user := store.User{ID: "user1", Name: "user name 1"}
|
||||
c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
|
||||
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)}
|
||||
|
||||
@@ -63,6 +63,7 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
|
||||
Exporter: &migrator.Remark{DataStore: &dataStore},
|
||||
Cache: &mockCache{},
|
||||
WebRoot: "/tmp",
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
AvatarProxy: &proxy.Avatar{StorePath: "/tmp", RoutePath: "/api/v1/avatar"},
|
||||
ImageProxy: &proxy.Image{},
|
||||
ReadOnlyAge: 10,
|
||||
|
||||
Reference in New Issue
Block a user