admin support for one-click deleteme activation

This commit is contained in:
Umputun
2018-06-06 12:56:16 -05:00
parent fbaf2a5a90
commit fec6268983
9 changed files with 143 additions and 9 deletions
+1
View File
@@ -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_
+1
View File
@@ -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
View File
@@ -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")
+99
View File
@@ -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)
}
+5 -3
View File
@@ -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
+2 -1
View File
@@ -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})
}
+1
View File
@@ -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)
+2
View File
@@ -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)}
+1
View File
@@ -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,