Add email subscription info to User information (#528)

* replace store.Locator with SiteID where only it is used

* add EmailSubscription flag to User information

* add /user endpoint test for email subscription
This commit is contained in:
Dmitry Verkhoturov
2020-01-05 13:42:16 -06:00
committed by Umputun
parent 2efab67640
commit d15176f36d
9 changed files with 67 additions and 43 deletions
+1 -1
View File
@@ -805,7 +805,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto
c.User.SetAdmin(ds.IsAdmin(c.Audience, c.User.ID))
c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID))
var err error
c.User.Email, err = ds.GetUserEmail(store.Locator{SiteID: c.Audience}, c.User.ID)
c.User.Email, err = ds.GetUserEmail(c.Audience, c.User.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", c.User.ID, err)
}
+1 -1
View File
@@ -188,7 +188,7 @@ func (e *Email) Send(ctx context.Context, req Request) (err error) {
if req.Verification.Token != "" {
log.Printf("[DEBUG] send verification via %s, user %s", e, req.Verification.User)
msg, err = e.buildVerificationMessage(req.Verification.User, req.Email, req.Verification.Token, req.Verification.Locator.SiteID)
msg, err = e.buildVerificationMessage(req.Verification.User, req.Email, req.Verification.Token, req.Verification.SiteID)
if err != nil {
return err
}
+5 -5
View File
@@ -32,7 +32,7 @@ type Destination interface {
// Store defines the minimal interface accessing stored comments used by notifier
type Store interface {
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
GetUserEmail(locator store.Locator, userID string) (string, error)
GetUserEmail(siteID string, userID string) (string, error)
}
// Request notification either about comment or about particular user verification
@@ -45,9 +45,9 @@ type Request struct {
// VerificationMetadata required to send notify method verification message
type VerificationMetadata struct {
Locator store.Locator // only SiteID is used
User string
Token string
SiteID string
User string
Token string
}
const defaultQueueSize = 100
@@ -82,7 +82,7 @@ func (s *Service) Submit(req Request) {
if s.dataService != nil && req.Comment.ParentID != "" {
if p, err := s.dataService.Get(req.Comment.Locator, req.Comment.ParentID, store.User{}); err == nil {
req.parent = p
req.Email, err = s.dataService.GetUserEmail(req.Comment.Locator, p.User.ID)
req.Email, err = s.dataService.GetUserEmail(req.Comment.Locator.SiteID, p.User.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", p.User.ID, err)
}
+1 -1
View File
@@ -123,6 +123,6 @@ func (m mockStore) Get(_ store.Locator, id string, _ store.User) (store.Comment,
return res, nil
}
func (m mockStore) GetUserEmail(_ store.Locator, _ string) (string, error) {
func (m mockStore) GetUserEmail(_ string, _ string) (string, error) {
return "", errors.New("no such user")
}
+23 -15
View File
@@ -48,9 +48,9 @@ type privStore interface {
Vote(req service.VoteReq) (comment store.Comment, err error)
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
GetUserEmail(locator store.Locator, userID string) (string, error)
SetUserEmail(locator store.Locator, userID string, value string) (string, error)
DeleteUserDetail(locator store.Locator, userID string, detail engine.UserDetail) error
GetUserEmail(siteID string, userID string) (string, error)
SetUserEmail(siteID string, userID string, value string) (string, error)
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
ValidateComment(c *store.Comment) error
IsVerified(siteID string, userID string) bool
IsReadOnly(locator store.Locator) bool
@@ -202,6 +202,14 @@ func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
if siteID := r.URL.Query().Get("site"); siteID != "" {
user.Verified = s.dataService.IsVerified(siteID, user.ID)
email, err := s.dataService.GetUserEmail(siteID, user.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
}
if len(email) > 0 {
user.EmailSubscription = true
}
}
render.JSON(w, r, user)
@@ -253,7 +261,7 @@ func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) {
func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
siteID := r.URL.Query().Get("site")
address, err := s.dataService.GetUserEmail(store.Locator{SiteID: siteID}, user.ID)
address, err := s.dataService.GetUserEmail(siteID, user.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
}
@@ -271,7 +279,7 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
rest.SendErrorJSON(w, r, http.StatusBadRequest, errors.New("missing parameter"), "address parameter is required", rest.ErrInternal)
return
}
existingAddress, err := s.dataService.GetUserEmail(store.Locator{SiteID: siteID}, user.ID)
existingAddress, err := s.dataService.GetUserEmail(siteID, user.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
}
@@ -299,9 +307,9 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
notify.Request{
Email: address,
Verification: notify.VerificationMetadata{
Locator: store.Locator{SiteID: siteID},
User: user.Name,
Token: tkn,
SiteID: siteID,
User: user.Name,
Token: tkn,
},
},
)
@@ -318,7 +326,7 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
return
}
user := rest.MustGetUserInfo(r)
locator := store.Locator{SiteID: r.URL.Query().Get("site")}
siteID := r.URL.Query().Get("site")
confClaims, err := s.authenticator.TokenService().Parse(tkn)
if err != nil {
@@ -340,7 +348,7 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
log.Printf("[DEBUG] set email for user %s", user.ID)
val, err := s.dataService.SetUserEmail(locator, user.ID, address)
val, err := s.dataService.SetUserEmail(siteID, user.ID, address)
if err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code)
@@ -368,7 +376,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorHTML(w, r, http.StatusBadRequest, errors.New("missing parameter"), "token parameter is required", rest.ErrInternal)
return
}
locator := store.Locator{SiteID: r.URL.Query().Get("site")}
siteID := r.URL.Query().Get("site")
confClaims, err := s.authenticator.TokenService().Parse(tkn)
if err != nil {
@@ -389,7 +397,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
userID := elems[0]
address := elems[1]
existingAddress, err := s.dataService.GetUserEmail(locator, userID)
existingAddress, err := s.dataService.GetUserEmail(siteID, userID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", userID, err)
}
@@ -404,7 +412,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] unsubscribe user %s", userID)
if err := s.dataService.DeleteUserDetail(locator, userID, engine.UserEmail); err != nil {
if err := s.dataService.DeleteUserDetail(siteID, userID, engine.UserEmail); err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
return
@@ -438,10 +446,10 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
// DELETE /email?site=siteID - removes user's email
func (s *private) deleteEmailCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
locator := store.Locator{SiteID: r.URL.Query().Get("site")}
siteID := r.URL.Query().Get("site")
log.Printf("[DEBUG] remove email for user %s", user.ID)
if err := s.dataService.DeleteUserDetail(locator, user.ID, engine.UserEmail); err != nil {
if err := s.dataService.DeleteUserDetail(siteID, user.ID, engine.UserEmail); err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
return
+15
View File
@@ -644,6 +644,21 @@ func TestRest_EmailNotification(t *testing.T) {
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
// get user information to verify the subscription
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/user?site=remark42", nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var user store.User
err = json.Unmarshal(body, &user)
assert.NoError(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true,
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, user)
// create child comment from another user, email notification expected
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
`{"text": "test 789",
+6 -6
View File
@@ -160,10 +160,10 @@ func (s *DataStore) Put(locator store.Locator, comment store.Comment) error {
}
// GetUserEmail gets user email
func (s *DataStore) GetUserEmail(locator store.Locator, userID string) (string, error) {
func (s *DataStore) GetUserEmail(siteID string, userID string) (string, error) {
res, err := s.Engine.UserDetail(engine.UserDetailRequest{
Detail: engine.UserEmail,
Locator: locator,
Locator: store.Locator{SiteID: siteID},
UserID: userID,
})
if err != nil {
@@ -176,10 +176,10 @@ func (s *DataStore) GetUserEmail(locator store.Locator, userID string) (string,
}
// SetUserEmail sets user email
func (s *DataStore) SetUserEmail(locator store.Locator, userID string, value string) (string, error) {
func (s *DataStore) SetUserEmail(siteID string, userID string, value string) (string, error) {
res, err := s.Engine.UserDetail(engine.UserDetailRequest{
Detail: engine.UserEmail,
Locator: locator,
Locator: store.Locator{SiteID: siteID},
UserID: userID,
Update: value,
})
@@ -193,9 +193,9 @@ func (s *DataStore) SetUserEmail(locator store.Locator, userID string, value str
}
// DeleteUserDetail deletes user detail
func (s *DataStore) DeleteUserDetail(locator store.Locator, userID string, detail engine.UserDetail) error {
func (s *DataStore) DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error {
return s.Engine.Delete(engine.DeleteRequest{
Locator: locator,
Locator: store.Locator{SiteID: siteID},
UserID: userID,
UserDetail: detail,
})
+6 -6
View File
@@ -842,31 +842,31 @@ func TestService_UserDetailsOperations(t *testing.T) {
AdminStore: admin.NewStaticKeyStore("secret 123")}
// add single valid entry
result, err := b.SetUserEmail(store.Locator{SiteID: "radio-t"}, "u1", "test@example.com")
result, err := b.SetUserEmail("radio-t", "u1", "test@example.com")
assert.NoError(t, err, "No error inserting entry expected")
assert.Equal(t, "test@example.com", result)
// read valid entry back
result, err = b.GetUserEmail(store.Locator{SiteID: "radio-t"}, "u1")
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Equal(t, "test@example.com", result)
// delete existing entry
err = b.DeleteUserDetail(store.Locator{SiteID: "radio-t"}, "u1", engine.UserEmail)
err = b.DeleteUserDetail("radio-t", "u1", engine.UserEmail)
assert.NoError(t, err, "No error deleting entry expected")
// read deleted entry
result, err = b.GetUserEmail(store.Locator{SiteID: "radio-t"}, "u1")
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Empty(t, result)
// insert entry with invalid site_id
result, err = b.SetUserEmail(store.Locator{SiteID: "bad-site"}, "u3", "does_not_matter@example.com")
result, err = b.SetUserEmail("bad-site", "u3", "does_not_matter@example.com")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
// read entry with invalid site_id
result, err = b.GetUserEmail(store.Locator{SiteID: "bad-site"}, "u3")
result, err = b.GetUserEmail("bad-site", "u3")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
}
+9 -8
View File
@@ -15,14 +15,15 @@ import (
// User holds user-related info
type User struct {
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
IP string `json:"ip,omitempty"`
Admin bool `json:"admin"`
Blocked bool `json:"block,omitempty"`
Verified bool `json:"verified,omitempty"`
SiteID string `json:"site_id,omitempty"`
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
IP string `json:"ip,omitempty"`
Admin bool `json:"admin"`
Blocked bool `json:"block,omitempty"`
Verified bool `json:"verified,omitempty"`
EmailSubscription bool `json:"email_subscription,omitempty"`
SiteID string `json:"site_id,omitempty"`
}
var reValidSha = regexp.MustCompile("^[a-fA-F0-9]{40}$")