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:
committed by
Umputun
parent
2efab67640
commit
d15176f36d
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}$")
|
||||
|
||||
Reference in New Issue
Block a user