feature/verified (#61)
* add store methods for verification * add verify rest and auth * lint: fix double conversion of userID key * test for middleware, fix log without auth info * make all scripts with -e
This commit is contained in:
@@ -32,6 +32,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.Put("/verify/{userid}", a.setVerifyCtrl)
|
||||
router.Get("/export", a.exportCtrl)
|
||||
router.Put("/pin/{id}", a.setPinCtrl)
|
||||
router.Get("/blocked", a.blockedUsersCtrl)
|
||||
@@ -110,6 +111,20 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"locator": locator, "read-only": roStatus})
|
||||
}
|
||||
|
||||
// PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
|
||||
func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "userid")
|
||||
siteID := r.URL.Query().Get("site")
|
||||
verifyStatus := r.URL.Query().Get("verified") == "1"
|
||||
|
||||
if err := a.dataService.SetVerified(siteID, userID, verifyStatus); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set verify status")
|
||||
return
|
||||
}
|
||||
a.cache.Flush(siteID, userID)
|
||||
render.JSON(w, r, JSON{"user": userID, "verified": verifyStatus})
|
||||
}
|
||||
|
||||
// PUT /pin/{id}?site=siteID&url=post-url&pin=1
|
||||
// mark/unmark comment as a special
|
||||
func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -275,6 +275,44 @@ func TestAdmin_ReadOnly(t *testing.T) {
|
||||
assert.False(t, info.ReadOnly)
|
||||
}
|
||||
|
||||
func TestAdmin_Verify(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)
|
||||
|
||||
verified := srv.DataService.IsVerified("radio-t", "user1")
|
||||
assert.False(t, verified)
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=1", ts.URL), nil)
|
||||
assert.Nil(t, err)
|
||||
withBasicAuth(req, "dev", "password")
|
||||
_, err = client.Do(req)
|
||||
require.Nil(t, err)
|
||||
verified = srv.DataService.IsVerified("radio-t", "user1")
|
||||
assert.True(t, verified)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil)
|
||||
assert.Nil(t, err)
|
||||
withBasicAuth(req, "dev", "password")
|
||||
_, err = client.Do(req)
|
||||
require.Nil(t, err)
|
||||
verified = srv.DataService.IsVerified("radio-t", "user1")
|
||||
assert.False(t, verified)
|
||||
}
|
||||
|
||||
func TestAdmin_ExportStream(t *testing.T) {
|
||||
srv, ts := prep(t)
|
||||
assert.NotNil(t, srv)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/umputun/remark/app/rest"
|
||||
"github.com/umputun/remark/app/store"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMiddleware_AppInfo(t *testing.T) {
|
||||
router := chi.NewRouter()
|
||||
router.With(AppInfo("remark42", "12345")).Get("/blah", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("blah blah"))
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
resp, err := http.Get(ts.URL + "/blah")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "blah blah", string(b))
|
||||
assert.Equal(t, "remark42", resp.Header.Get("App-Name"))
|
||||
assert.Equal(t, "12345", resp.Header.Get("App-Version"))
|
||||
assert.Equal(t, "Umputun", resp.Header.Get("Org"))
|
||||
}
|
||||
|
||||
func TestMiddleware_GetBodyAndUser(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://example.com/request", strings.NewReader("body"))
|
||||
require.Nil(t, err)
|
||||
|
||||
body, user := getBodyAndUser(req, []LoggerFlag{LogAll})
|
||||
assert.Equal(t, "body", body)
|
||||
assert.Equal(t, "", user, "no user")
|
||||
|
||||
req = rest.SetUserInfo(req, store.User{ID: "id1", Name: "user1"})
|
||||
body, user = getBodyAndUser(req, []LoggerFlag{LogAll})
|
||||
assert.Equal(t, ` - id1 "user1"`, user, "no user")
|
||||
|
||||
body, user = getBodyAndUser(req, nil)
|
||||
assert.Equal(t, "", body)
|
||||
assert.Equal(t, "", user, "no user")
|
||||
|
||||
body, user = getBodyAndUser(req, []LoggerFlag{LogNone})
|
||||
assert.Equal(t, "", body)
|
||||
assert.Equal(t, "", user, "no user")
|
||||
|
||||
body, user = getBodyAndUser(req, []LoggerFlag{LogUser})
|
||||
assert.Equal(t, "", body)
|
||||
assert.Equal(t, ` - id1 "user1"`, user, "no user")
|
||||
}
|
||||
@@ -128,11 +128,12 @@ func (s *Rest) routes() chi.Router {
|
||||
|
||||
// api routes
|
||||
router.Route("/api/v1", func(rapi chi.Router) {
|
||||
rapi.Use(Logger(LogAll), tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
rapi.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
|
||||
// open routes
|
||||
rapi.Group(func(ropen chi.Router) {
|
||||
ropen.Use(s.Authenticator.Auth(false))
|
||||
ropen.Use(Logger(LogAll))
|
||||
ropen.Get("/find", s.findCommentsCtrl)
|
||||
ropen.Get("/id/{id}", s.commentByIDCtrl)
|
||||
ropen.Get("/comments", s.findUserCommentsCtrl)
|
||||
@@ -151,13 +152,14 @@ func (s *Rest) routes() chi.Router {
|
||||
// protected routes, require auth
|
||||
rapi.Group(func(rauth chi.Router) {
|
||||
rauth.Use(s.Authenticator.Auth(true))
|
||||
rauth.Use(Logger(LogAll))
|
||||
rauth.Post("/comment", s.createCommentCtrl)
|
||||
rauth.Put("/comment/{id}", s.updateCommentCtrl)
|
||||
rauth.Get("/user", s.userInfoCtrl)
|
||||
rauth.Put("/vote/{id}", s.voteCtrl)
|
||||
|
||||
// admin routes, admin users only
|
||||
rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly))
|
||||
rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly, Logger(LogAll)))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ type CustomClaims struct {
|
||||
User *store.User `json:"user,omitempty"`
|
||||
|
||||
// state and from used for oauth handshake
|
||||
State string `json:"state,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
}
|
||||
|
||||
const jwtCookieName = "JWT"
|
||||
|
||||
+16
-10
@@ -35,13 +35,14 @@ type Provider struct {
|
||||
|
||||
// Params to make initialized and ready to use provider
|
||||
type Params struct {
|
||||
RemarkURL string
|
||||
AvatarProxy *proxy.Avatar
|
||||
JwtService *JWT
|
||||
SecretKey string
|
||||
Admins []string
|
||||
Cid string
|
||||
Csecret string
|
||||
RemarkURL string
|
||||
AvatarProxy *proxy.Avatar
|
||||
JwtService *JWT
|
||||
IsVerifiedFn func(siteID string, userID string) bool
|
||||
SecretKey string
|
||||
Admins []string
|
||||
Cid string
|
||||
Csecret string
|
||||
}
|
||||
|
||||
type userData map[string]interface{}
|
||||
@@ -79,7 +80,7 @@ func (p Provider) Routes() chi.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
// loginHandler - GET /login?from=redirect-back-url
|
||||
// loginHandler - GET /login?from=redirect-back-url&site=siteID
|
||||
func (p Provider) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Printf("[DEBUG] login with %s", p.Name)
|
||||
@@ -87,8 +88,9 @@ func (p Provider) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
state := p.randToken()
|
||||
|
||||
claims := CustomClaims{
|
||||
State: state,
|
||||
From: r.URL.Query().Get("from"),
|
||||
State: state,
|
||||
From: r.URL.Query().Get("from"),
|
||||
SiteID: r.URL.Query().Get("site"),
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: p.randToken(),
|
||||
Issuer: "remark42",
|
||||
@@ -166,7 +168,11 @@ func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[WARN] failed to proxy avatar, %s", e)
|
||||
}
|
||||
}
|
||||
|
||||
u.Admin = isAdmin(u.ID, p.Admins)
|
||||
if p.IsVerifiedFn != nil {
|
||||
u.Verified = p.IsVerifiedFn(oauthClaims.SiteID, u.ID)
|
||||
}
|
||||
|
||||
authClaims := &CustomClaims{
|
||||
User: &u,
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestLogin(t *testing.T) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.Nil(t, err)
|
||||
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
|
||||
resp, err := client.Get("http://localhost:8981/login")
|
||||
resp, err := client.Get("http://localhost:8981/login?site=remark")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
@@ -50,7 +50,7 @@ func TestLogin(t *testing.T) {
|
||||
Admin: false, Blocked: false, IP: ""}, u)
|
||||
|
||||
// check admin user
|
||||
resp, err = client.Get("http://localhost:8981/login")
|
||||
resp, err = client.Get("http://localhost:8981/login?site=remark")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
@@ -58,7 +58,7 @@ func TestLogin(t *testing.T) {
|
||||
err = json.Unmarshal(body, &u)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, store.User{Name: "blah", ID: "mock_myuser2", Picture: "http://exmple.com/pic1.png",
|
||||
Admin: true, Blocked: false, IP: ""}, u)
|
||||
Admin: true, Blocked: false, IP: "", Verified: true}, u)
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
@@ -120,7 +120,8 @@ func mockProvider(t *testing.T, loginPort, authPort int) (*http.Server, *http.Se
|
||||
},
|
||||
}
|
||||
params := Params{RemarkURL: "url", SecretKey: "123456", Cid: "cid", Csecret: "csecret",
|
||||
JwtService: NewJWT("12345", false, time.Hour), Admins: []string{"mock_myuser2"}}
|
||||
JwtService: NewJWT("12345", false, time.Hour), Admins: []string{"mock_myuser2"},
|
||||
IsVerifiedFn: func(siteID, userID string) bool { return userID == "mock_myuser2" }}
|
||||
provider = initProvider(params, provider)
|
||||
|
||||
ts := &http.Server{Addr: fmt.Sprintf(":%d", loginPort), Handler: provider.Routes()}
|
||||
|
||||
@@ -17,7 +17,6 @@ func GetUserInfo(r *http.Request) (user store.User, err error) {
|
||||
if ctx == nil {
|
||||
return store.User{}, errors.New("no info about user")
|
||||
}
|
||||
|
||||
if u, ok := ctx.Value(contextKey("user")).(store.User); ok {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user