diff --git a/README.md b/README.md index 4055096b..9d9c83c9 100644 --- a/README.md +++ b/README.md @@ -507,7 +507,7 @@ type Comment struct { User User `json:"user"` // user info, read only Locator Locator `json:"locator"` // post locator Score int `json:"score"` // comment score, read only - Votes map[string]bool `json:"votes"` // comment votes, read only + Vote int `json:"vote"` // vote for the current user, -1/1/0. Controversy float64 `json:"controversy,omitempty"` // comment controversy, read only Timestamp time.Time `json:"time"` // time stamp, read only Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response @@ -609,11 +609,11 @@ Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i ### Images management -* `GET /api/v1/picture/{user}/{id}` - load stored image +* `GET /api/v1/picture/{user}/{id}` - load stored image * `POST /api/v1/picture` - upload and store image, uses post form with `FormFile("file")`. returns `{"id": user/imgid}` _auth required_ _returned id should be appended to load image url on caller side_ - + ### Admin * `DELETE /api/v1/admin/comment/{id}?site=site-id&url=post-url` - delete comment by `id`. diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index e855b942..c53f7963 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -305,20 +305,54 @@ func (s *Rest) routes() chi.Router { return router } +func (s *Rest) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) { + + res = s.adminService.alterComments(comments, r) // apply admin's alteration + + // prepare vote info for client view + vote := func(c store.Comment, r *http.Request) store.Comment { + + c.Vote = 0 // default is "none" (not voted) + + user, err := rest.GetUserInfo(r) + if err != nil { + c.Votes = nil // hide voters list and don't set Vote for non-authed user + return c + } + + if v, ok := c.Votes[user.ID]; ok { + if v { + c.Vote = 1 + } else { + c.Vote = -1 + } + } + + c.Votes = nil // hide voters list + return c + } + + for i, c := range res { + c = vote(c, r) + res[i] = c + } + + return res +} + // serves static files from /web or embedded by statik func addFileServer(r chi.Router, path string, root http.FileSystem) { var webFS http.Handler statikFS, err := fs.New() - if err == nil { - log.Printf("[INFO] run file server for %s, embedded", root) - webFS = http.FileServer(statikFS) - } if err != nil { log.Printf("[DEBUG] no embedded assets loaded, %s", err) log.Printf("[INFO] run file server for %s, path %s", root, path) webFS = http.FileServer(root) + } else { + log.Printf("[INFO] run file server for %s, embedded", root) + webFS = http.FileServer(statikFS) } origPath := path @@ -364,9 +398,24 @@ func filterComments(comments []store.Comment, fn func(c store.Comment) bool) []s // admins will have different keys in order to prevent leak of admin-only data to regular users func URLKey(r *http.Request) string { adminPrefix := "admin!!" - key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view - if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make separate cache key for admins - key = adminPrefix + key + key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view + if user, err := rest.GetUserInfo(r); err == nil && user.Admin { + key = adminPrefix + key // make separate cache key for admins + } + return key +} + +// URLKeyWithUser gets url from request to use it as cache key and attaching user ID +// admins will have different keys in order to prevent leak of admin-only data to regular users +func URLKeyWithUser(r *http.Request) string { + adminPrefix := "admin!!" + key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view + if user, err := rest.GetUserInfo(r); err == nil { + if user.Admin { + key = adminPrefix + user.ID + "!!" + key // make separate cache key for admins + } else { + key = user.ID + "!!" + key // make separate cache key for authed users + } } return key } diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 9d9b1a13..be42276d 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -352,7 +352,7 @@ func TestRest_Vote(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) assert.Nil(t, err) - req.SetBasicAuth("admin", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) assert.Nil(t, err) return resp.StatusCode @@ -360,22 +360,65 @@ func TestRest_Vote(t *testing.T) { assert.Equal(t, 200, vote(1), "first vote allowed") assert.Equal(t, 400, vote(1), "second vote rejected") - body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) assert.Equal(t, 200, code) cr := store.Comment{} err := json.Unmarshal([]byte(body), &cr) assert.Nil(t, err) assert.Equal(t, 1, cr.Score) - assert.Equal(t, map[string]bool{"admin": true}, cr.Votes) + assert.Equal(t, 1, cr.Vote) + assert.Equal(t, map[string]bool(nil), cr.Votes) assert.Equal(t, 200, vote(-1), "opposite vote allowed") - body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) assert.Equal(t, 200, code) cr = store.Comment{} err = json.Unmarshal([]byte(body), &cr) assert.Nil(t, err) assert.Equal(t, 0, cr.Score) - assert.Equal(t, map[string]bool{}, cr.Votes) + assert.Equal(t, 0, cr.Vote) + + assert.Equal(t, 200, vote(-1), "opposite vote allowed one more time") + body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + assert.Equal(t, 200, code) + cr = store.Comment{} + err = json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, -1, cr.Score) + assert.Equal(t, -1, cr.Vote) + + assert.Equal(t, 400, vote(-1), "dbl vote not allowed") + body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + assert.Equal(t, 200, code) + cr = store.Comment{} + err = json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, -1, cr.Score) + assert.Equal(t, -1, cr.Vote) + + body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + assert.Equal(t, 200, code) + cr = store.Comment{} + err = json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, -1, cr.Score) + assert.Equal(t, 0, cr.Vote, "no vote info for not authed user") + assert.Equal(t, map[string]bool(nil), cr.Votes) + + req, err := http.NewRequest("GET", + fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil) + assert.NoError(t, err) + resp, err := sendReq(t, req, adminUmputunToken) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + cr = store.Comment{} + err = json.NewDecoder(resp.Body).Decode(&cr) + assert.Nil(t, err) + assert.Equal(t, -1, cr.Score) + assert.Equal(t, 0, cr.Vote, "no vote info for different user") + assert.Equal(t, map[string]bool(nil), cr.Votes) + + assert.Equal(t, map[string]bool(nil), cr.Votes) } func TestRest_UserAllData(t *testing.T) { diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index 3657bacd..92f26ad5 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -28,13 +28,13 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { } log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format")) - key := cache.NewKey(locator.SiteID).ID(URLKey(r)).Scopes(locator.SiteID, locator.URL) + key := cache.NewKey(locator.SiteID).ID(URLKeyWithUser(r)).Scopes(locator.SiteID, locator.URL) data, err := s.Cache.Get(key, func() ([]byte, error) { comments, e := s.DataService.Find(locator, sort) if e != nil { comments = []store.Comment{} // error should clear comments and continue for post info } - maskedComments := s.adminService.alterComments(comments, r) + maskedComments := s.alterComments(comments, r) var b []byte switch r.URL.Query().Get("format") { case "tree": @@ -130,7 +130,7 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - comments = s.adminService.alterComments(comments, r) + comments = s.alterComments(comments, r) // filter deleted from last comments view. Blocked marked as deleted and will sneak in without filterDeleted := filterComments(comments, func(c store.Comment) bool { return !c.Deleted }) return encodeJSONWithHTML(filterDeleted) @@ -160,7 +160,7 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id", rest.ErrCommentNotFound) return } - comment = s.adminService.alterComments([]store.Comment{comment}, r)[0] + comment = s.alterComments([]store.Comment{comment}, r)[0] render.Status(r, http.StatusOK) if err = R.RenderJSONWithHTML(w, r, comment); err != nil { @@ -186,13 +186,13 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID) - key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(userID, siteID) + key := cache.NewKey(siteID).ID(URLKeyWithUser(r)).Scopes(userID, siteID) data, err := s.Cache.Get(key, func() ([]byte, error) { comments, e := s.DataService.User(siteID, userID, limit, 0) if e != nil { return nil, e } - comments = s.adminService.alterComments(comments, r) + comments = s.alterComments(comments, r) comments = filterComments(comments, func(c store.Comment) bool { return !c.Deleted }) count, e := s.DataService.UserCount(siteID, userID) if e != nil { diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index ef5108d7..276d69ac 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "os" + "strconv" "strings" "testing" "time" @@ -38,6 +39,8 @@ var getStartedHTML = "/tmp/getstarted.html" var devToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg" +var adminUmputunToken = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyYWRpb3QiLCJleHAiOjE5NTQ1OTc5ODAsImp0aSI6Ijk3YTJlMGFjNGRjN2Q1ZjY5MjZkNWU4NjIwYWNlZjlhNDBjMCIsImlhdCI6MTQ1NDU5NzY4MCwiaXNzIjoicmVtYXJrNDIiLCJ1c2VyIjp7Im5hbWUiOiJVbXB1dHVuIiwiaWQiOiJnaXRodWJfZWYwZjcwNmE3IiwicGljdHVyZSI6Imh0dHBzOi8vcmVtYXJrNDIucmFkaW8tdC5jb20vYXBpL3YxL2F2YXRhci9jYjQyZmY0OTNhZGU2OTZkODhhM2E1OTBmMTM2YWU5ZTM0ZGU3YzFiLmltYWdlIiwiYXR0cnMiOnsiYWRtaW4iOnRydWUsImJsb2NrZWQiOmZhbHNlfX19.I5a8EHbUJy8mApuYCPDRThbC-1jP0sbPh1qwNyY1V4E" + func TestRest_FileServer(t *testing.T) { ts, _, teardown := startupT(t) defer teardown() @@ -178,7 +181,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv.Shutdown() } -func Test_rejectAnonUser(t *testing.T) { +func TestRest_rejectAnonUser(t *testing.T) { ts := httptest.NewServer(fakeAuth(rejectAnonUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello") @@ -198,6 +201,54 @@ func Test_rejectAnonUser(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode, "real user") } +func Test_URLKey(t *testing.T) { + tbl := []struct { + url string + user store.User + key string + }{ + {"http://example.com/1", store.User{}, "http://example.com/1"}, + {"http://example.com/1", store.User{ID: "user"}, "http://example.com/1"}, + {"http://example.com/1", store.User{ID: "user", Admin: true}, "admin!!http://example.com/1"}, + } + + for i, tt := range tbl { + t.Run(strconv.Itoa(i), func(t *testing.T) { + r, err := http.NewRequest("GET", tt.url, nil) + require.NoError(t, err) + if tt.user.ID != "" { + r = rest.SetUserInfo(r, tt.user) + } + assert.Equal(t, tt.key, URLKey(r)) + }) + } + +} + +func Test_URLKeyWithUser(t *testing.T) { + tbl := []struct { + url string + user store.User + key string + }{ + {"http://example.com/1", store.User{}, "http://example.com/1"}, + {"http://example.com/1", store.User{ID: "user"}, "user!!http://example.com/1"}, + {"http://example.com/2", store.User{ID: "user2"}, "user2!!http://example.com/2"}, + {"http://example.com/1", store.User{ID: "user", Admin: true}, "admin!!user!!http://example.com/1"}, + } + + for i, tt := range tbl { + t.Run(strconv.Itoa(i), func(t *testing.T) { + r, err := http.NewRequest("GET", tt.url, nil) + require.NoError(t, err) + if tt.user.ID != "" { + r = rest.SetUserInfo(r, tt.user) + } + assert.Equal(t, tt.key, URLKeyWithUser(r)) + }) + } + +} func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) { testDb := fmt.Sprintf("/tmp/test-remark-%d.db", rand.Int31()) @@ -296,6 +347,14 @@ func get(t *testing.T, url string) (string, int) { return string(body), r.StatusCode } +func sendReq(t *testing.T, r *http.Request, token string) (*http.Response, error) { + client := http.Client{Timeout: 5 * time.Second} + if token != "" { + r.Header.Set("X-JWT", token) + } + return client.Do(r) +} + func getWithDevAuth(t *testing.T, url string) (body string, code int) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", url, nil) diff --git a/backend/app/rest/api/rss.go b/backend/app/rest/api/rss.go index 1b7107d2..06d4e788 100644 --- a/backend/app/rest/api/rss.go +++ b/backend/app/rest/api/rss.go @@ -41,7 +41,7 @@ func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - comments = s.adminService.alterComments(comments, r) + comments = s.alterComments(comments, r) rss, e := s.toRssFeed(locator.URL, comments, "post comments for "+r.URL.Query().Get("url")) if e != nil { return nil, e @@ -73,7 +73,7 @@ func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - comments = s.adminService.alterComments(comments, r) + comments = s.alterComments(comments, r) rss, e := s.toRssFeed(r.URL.Query().Get("site"), comments, "site comment for "+siteID) if e != nil { @@ -107,7 +107,7 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, errors.Wrap(e, "can't get last comments") } - comments = s.adminService.alterComments(comments, r) + comments = s.alterComments(comments, r) replies := []store.Comment{} for _, c := range comments { if len(replies) > maxRssItems || c.Timestamp.Add(maxReplyDuration).Before(time.Now()) { diff --git a/backend/app/store/comment.go b/backend/app/store/comment.go index 895c3962..a6241184 100644 --- a/backend/app/store/comment.go +++ b/backend/app/store/comment.go @@ -17,7 +17,8 @@ type Comment struct { User User `json:"user"` Locator Locator `json:"locator"` Score int `json:"score"` - Votes map[string]bool `json:"votes"` + Votes map[string]bool `json:"votes,omitempty"` + Vote int `json:"vote"` // vote for the current user, -1/1/0. Controversy float64 `json:"controversy,omitempty"` Timestamp time.Time `json:"time" bson:"time"` Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response diff --git a/backend/app/store/engine/bolt_admin_test.go b/backend/app/store/engine/bolt_admin_test.go index 69778ee4..b2196f43 100644 --- a/backend/app/store/engine/bolt_admin_test.go +++ b/backend/app/store/engine/bolt_admin_test.go @@ -107,6 +107,7 @@ func TestBoltAdmin_DeleteUser(t *testing.T) { b, teardown := prep(t) defer teardown() + err := b.DeleteUser("radio-t", "user1") require.NoError(t, err) @@ -157,6 +158,7 @@ func TestBoltAdmin_BlockUserWithTTL(t *testing.T) { b, teardown := prep(t) defer teardown() + assert.False(t, b.IsBlocked("radio-t", "user1"), "nothing blocked") assert.NoError(t, b.SetBlock("radio-t", "user1", true, 50*time.Millisecond)) assert.True(t, b.IsBlocked("radio-t", "user1"), "user1 blocked") diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index fe30a28c..43849ee0 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -206,6 +206,15 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string, comment.Score-- } + comment.Vote = 0 + if v, ok := comment.Votes[userID]; ok { + if v { + comment.Vote = 1 + } else { + comment.Vote = -1 + } + } + comment.Controversy = s.controversy(s.upsAndDowns(comment)) return comment, s.Put(locator, comment) diff --git a/backend/app/store/service/service_test.go b/backend/app/store/service/service_test.go index 73b66ee5..6fbc9401 100644 --- a/backend/app/store/service/service_test.go +++ b/backend/app/store/service/service_test.go @@ -48,7 +48,7 @@ func TestService_CreateFromEmpty(t *testing.T) { assert.Equal(t, "user", res.User.ID) assert.Equal(t, "name", res.User.Name) assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP) - assert.Equal(t, map[string]bool{}, res.Votes) + assert.Equal(t, map[string]bool(nil), res.Votes) } func TestService_CreateFromPartial(t *testing.T) { @@ -179,11 +179,13 @@ func TestService_Vote(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 3, len(res)) assert.Equal(t, 0, res[0].Score) - assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially") + assert.Equal(t, 0, res[0].Vote) + assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially") c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true) assert.Nil(t, err) assert.Equal(t, 1, c.Score) + assert.Equal(t, 1, c.Vote) assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +") c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true) @@ -197,6 +199,7 @@ func TestService_Vote(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 3, len(res)) assert.Equal(t, 1, res[0].Score) + assert.Equal(t, 1, res[0].Vote) assert.Equal(t, 0.0, res[0].Controversy) _, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false) @@ -205,7 +208,8 @@ func TestService_Vote(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 3, len(res)) assert.Equal(t, 0, res[0].Score) - assert.Equal(t, map[string]bool{}, res[0].Votes, "vote reset ok") + assert.Equal(t, 0, res[0].Vote) + assert.Equal(t, map[string]bool(nil), res[0].Votes, "vote reset ok") } func TestService_VoteLimit(t *testing.T) { @@ -251,7 +255,7 @@ func TestService_VoteAggressive(t *testing.T) { t.Logf("%+v", res[0]) assert.Equal(t, 3, len(res)) assert.Equal(t, 0, res[0].Score) - assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially") + assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially") // add a vote as user2 _, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user2", true) diff --git a/backend/remark.rest b/backend/remark.rest index 7319bf87..ec6dd3c7 100644 --- a/backend/remark.rest +++ b/backend/remark.rest @@ -5,6 +5,10 @@ GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=tree&url={{url}} ### find request with plain GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}} +### find request with plain +GET http://127.0.0.1:8080/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}} +X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg + ### last 50 comments GET {{host}}/api/v1/last/50?site={{site}} @@ -53,7 +57,8 @@ Content-Type: application/json PUT {{host}}/api/v1/admin/pin/3665976683?site={{site}}&url={{url}}&pin=1 ### vote for comment -PUT {{host}}/api/v1/vote/73e346f4-d57d-41a8-8803-6671aa187d8e?site={{site}}&url={{url}}&vote=1 +PUT http://127.0.0.1:8080/api/v1/vote/8a8c0b80-0d0a-41c3-84ad-f4034704e827?site={{site}}&url={{url}}&vote=-1 +X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg ### get user info GET {{host}}/api/v1/user diff --git a/web/.babelrc b/web/.babelrc index 7b541de0..d99459f5 100644 --- a/web/.babelrc +++ b/web/.babelrc @@ -7,7 +7,7 @@ "browsers": ["> 1%", "android >= 4.4.4", "ios >= 9", "IE >= 11"] }, "useBuiltIns": "usage", - "corejs": 3 + "corejs": 3 } ], [ diff --git a/web/app/common/api.ts b/web/app/common/api.ts index ed6b9c6f..b5948148 100644 --- a/web/app/common/api.ts +++ b/web/app/common/api.ts @@ -52,7 +52,10 @@ export const logOut = (): Promise => export const getConfig = (): Promise => fetcher.get(`/config`); export const getPostComments = (sort: Sorting): Promise => - fetcher.get(`/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`); + fetcher.get({ + url: `/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`, + withCredentials: true, + }); export const getLastComments = (siteId: string, max: number): Promise => fetcher.get(`/last/${max}?site=${siteId}`); @@ -63,7 +66,8 @@ export const getCommentsCount = (siteId: string, urls: string[]): Promise<{ url: body: urls, }); -export const getComment = (id: Comment['id']): Promise => fetcher.get(`/id/${id}?url=${url}`); +export const getComment = (id: Comment['id']): Promise => + fetcher.get({ url: `/id/${id}?url=${url}`, withCredentials: true }); export const getUserComments = ( userId: User['id'], @@ -71,7 +75,11 @@ export const getUserComments = ( ): Promise<{ comments: Comment[]; count: number; -}> => fetcher.get(`/comments?user=${userId}&limit=${limit}`); +}> => + fetcher.get({ + url: `/comments?user=${userId}&limit=${limit}`, + withCredentials: true, + }); export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise => fetcher.put({ diff --git a/web/app/common/types.ts b/web/app/common/types.ts index c14d8146..d1843789 100644 --- a/web/app/common/types.ts +++ b/web/app/common/types.ts @@ -44,8 +44,12 @@ export interface Comment { locator: Locator; /** comment score, read only */ score: number; - /** comment votes, read only */ - votes: { [key: string]: boolean }; + /** + * vote delta, + * if user hasn't voted delta will be 0, + * -1/+1 for downvote/upvote + */ + vote: number; /** comment controversy, read only */ controversy?: number; /** pointer to have empty default in json response */ diff --git a/web/app/components/comment/comment.test.tsx b/web/app/components/comment/comment.test.tsx index 254bb4ef..cc8a947e 100644 --- a/web/app/components/comment/comment.test.tsx +++ b/web/app/components/comment/comment.test.tsx @@ -12,7 +12,7 @@ const DefaultProps: Partial = { view: 'main', data: { text: 'test comment', - votes: {}, + vote: 0, user: { id: 'someone', picture: 'somepicture-url', @@ -121,7 +121,7 @@ describe('', () => { const element = ( ); @@ -146,7 +146,7 @@ describe('', () => { const element = ( ); diff --git a/web/app/components/comment/comment.tsx b/web/app/components/comment/comment.tsx index 5fd98b97..3062f922 100644 --- a/web/app/components/comment/comment.tsx +++ b/web/app/components/comment/comment.tsx @@ -104,18 +104,8 @@ export class Comment extends Component { } updateState(props: Props) { - let scoreDelta = 0; - if (props.user) { - if (props.data.votes[props.user.id] === true) { - ++scoreDelta; - } - if (props.data.votes[props.user.id] === false) { - --scoreDelta; - } - } - this.setState({ - scoreDelta, + scoreDelta: props.data.vote, cachedScore: props.data.score, }); diff --git a/web/app/components/root/root.tsx b/web/app/components/root/root.tsx index 345f708a..c638be64 100644 --- a/web/app/components/root/root.tsx +++ b/web/app/components/root/root.tsx @@ -113,6 +113,17 @@ export class Root extends Component { window.addEventListener('message', this.onMessage.bind(this)); } + logIn = async (p: AuthProvider): Promise => { + const user = await this.props.logIn(p); + await this.props.fetchComments(this.props.sort); + return user; + }; + + logOut = async (): Promise => { + await this.props.logOut(); + await this.props.fetchComments(this.props.sort); + }; + checkUrlHash( e: Event & { newURL?: string; @@ -202,8 +213,8 @@ export class Root extends Component { providers={StaticStore.config.auth_providers} isCommentsDisabled={isCommentsDisabled} postInfo={this.props.info} - onSignIn={this.props.logIn} - onSignOut={this.props.logOut} + onSignIn={this.logIn} + onSignOut={this.logOut} onBlockedUsersShow={this.onBlockedUsersShow} onBlockedUsersHide={this.onBlockedUsersHide} onCommentsEnable={this.props.enableComments} diff --git a/web/webpack.config.js b/web/webpack.config.js index 92ba409e..47aef48c 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -139,6 +139,7 @@ module.exports = () => ({ }, stats: { children: false, + entrypoints: false, }, devServer: { host: 'localhost', @@ -157,5 +158,9 @@ module.exports = () => ({ changeOrigin: true, }, }, + stats: { + children: false, + entrypoints: false, + }, }, });