[MM-32691] - Send email when the users are not able to join a workspace (#16958)

* [MM-32691] - Send email when the users are not able to join a workspace

* Revert "[MM-32691] - Send email when the users are not able to join a workspace"

This reverts commit 3c11643c7c6867d992743a15675f0099df4f4e4d.

* Feeback impl-1

* use date as key for rate limiting

* Fix tests

* Translations

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Allan Guwatudde
2021-02-24 17:37:24 +03:00
коммит произвёл GitHub
родитель 6b388871a9
Коммит 0dad204007
9 изменённых файлов: 110 добавлений и 26 удалений

Просмотреть файл

@@ -36,6 +36,7 @@ func (api *API) InitCloud() {
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.ApiSessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.ApiSessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/limitreached/invite", api.ApiSessionRequired(sendAdminUpgradeRequestEmail)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/subscription/limitreached/join", api.ApiHandler(sendAdminUpgradeRequestEmailOnJoin)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/subscription/stats", api.ApiHandler(getSubscriptionStats)).Methods("GET")
// POST /api/v4/cloud/webhook
@@ -409,7 +410,27 @@ func sendAdminUpgradeRequestEmail(c *Context, w http.ResponseWriter, r *http.Req
return
}
if err = c.App.SendAdminUpgradeRequestEmail(user.Username, sub); err != nil {
if err = c.App.SendAdminUpgradeRequestEmail(user.Username, sub, model.InviteLimitation); err != nil {
c.Err = model.NewAppError("Api4.sendAdminUpgradeRequestEmail", err.Id, nil, err.Error(), err.StatusCode)
return
}
ReturnStatusOK(w)
}
func sendAdminUpgradeRequestEmailOnJoin(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.Cloud {
c.Err = model.NewAppError("Api4.sendAdminUpgradeRequestEmailOnJoin", "api.cloud.license_error", nil, "", http.StatusNotImplemented)
return
}
sub, err := c.App.Cloud().GetSubscription()
if err != nil {
c.Err = model.NewAppError("Api4.sendAdminUpgradeRequestEmailOnJoin", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err = c.App.SendAdminUpgradeRequestEmail("", sub, model.JoinLimitation); err != nil {
c.Err = model.NewAppError("Api4.sendAdminUpgradeRequestEmail", err.Id, nil, err.Error(), err.StatusCode)
return
}

Просмотреть файл

@@ -267,7 +267,7 @@ type AppIface interface {
SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError)
// SendAdminUpgradeRequestEmail takes the username of user trying to alert admins and then applies rate limit of n (number of admins) emails per user per day
// before sending the emails.
SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription) *model.AppError
SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription, action string) *model.AppError
// SendNoCardPaymentFailedEmail
SendNoCardPaymentFailedEmail() *model.AppError
// ServePluginPublicRequest serves public plugin files

Просмотреть файл

@@ -6,6 +6,7 @@ package app
import (
"fmt"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
@@ -23,7 +24,7 @@ func (a *App) getSysAdminsEmailRecipients() ([]*model.User, *model.AppError) {
// SendAdminUpgradeRequestEmail takes the username of user trying to alert admins and then applies rate limit of n (number of admins) emails per user per day
// before sending the emails.
func (a *App) SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription) *model.AppError {
func (a *App) SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription, action string) *model.AppError {
if a.Srv().License() == nil || (a.Srv().License() != nil && !*a.Srv().License().Features.Cloud) {
return nil
}
@@ -32,21 +33,24 @@ func (a *App) SendAdminUpgradeRequestEmail(username string, subscription *model.
return nil
}
year, month, day := time.Now().Date()
key := fmt.Sprintf("%s-%d-%s-%d", action, day, month, year)
if a.Srv().EmailService.PerDayEmailRateLimiter == nil {
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("for username=%s", username), http.StatusInternalServerError)
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("for key=%s", key), http.StatusInternalServerError)
}
// rate limit based on username as key
rateLimited, result, err := a.Srv().EmailService.PerDayEmailRateLimiter.RateLimit(username, 1)
// rate limit based on combination of date and action as key
rateLimited, result, err := a.Srv().EmailService.PerDayEmailRateLimiter.RateLimit(key, 1)
if err != nil {
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("username=%s, error=%v", username, err), http.StatusInternalServerError)
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("for key=%s, error=%v", key, err), http.StatusInternalServerError)
}
if rateLimited {
return model.NewAppError("app.SendAdminUpgradeRequestEmail",
"app.email.rate_limit_exceeded.app_error", map[string]interface{}{"RetryAfter": result.RetryAfter.String(), "ResetAfter": result.ResetAfter.String()},
fmt.Sprintf("username=%s, retry_after_secs=%f, reset_after_secs=%f",
username, result.RetryAfter.Seconds(), result.ResetAfter.Seconds()),
fmt.Sprintf("key=%s, retry_after_secs=%f, reset_after_secs=%f",
key, result.RetryAfter.Seconds(), result.ResetAfter.Seconds()),
http.StatusRequestEntityTooLarge)
}
@@ -59,7 +63,7 @@ func (a *App) SendAdminUpgradeRequestEmail(username string, subscription *model.
countNotOks := 0
for admin := range sysAdmins {
ok, err := a.Srv().EmailService.SendUpgradeEmail(username, sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
ok, err := a.Srv().EmailService.SendUpgradeEmail(username, sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL, action)
if !ok || err != nil {
a.Log().Error("Error sending upgrade request email", mlog.Err(err))
countNotOks++

Просмотреть файл

@@ -634,14 +634,20 @@ func (es *EmailService) SendAtUserLimitWarningEmail(email string, locale string,
}
// SendUpgradeEmail formats an email template and sends an email to an admin specified in the email arg
func (es *EmailService) SendUpgradeEmail(user, email, locale, siteURL string) (bool, *model.AppError) {
func (es *EmailService) SendUpgradeEmail(user, email, locale, siteURL, action string) (bool, *model.AppError) {
T := utils.GetUserTranslations(locale)
subject := T("api.templates.upgrade_request_subject")
bodyPage := es.newEmailTemplate("cloud_upgrade_request_email", locale)
bodyPage.Props["Title"] = T("api.templates.upgrade_request_title", map[string]interface{}{"UserName": user})
bodyPage.Props["Info4"] = T("api.templates.upgrade_request_info4")
if action == model.InviteLimitation {
bodyPage.Props["Title"] = T("api.templates.upgrade_request_title", map[string]interface{}{"UserName": user})
bodyPage.Props["Info4"] = T("api.templates.upgrade_request_info4")
} else {
bodyPage.Props["Title"] = T("api.templates.upgrade_request_title2")
bodyPage.Props["Info4"] = T("api.templates.upgrade_request_info4_2")
}
subject := T("api.templates.upgrade_request_subject")
bodyPage.Props["Info5"] = T("api.templates.at_limit_info5")
bodyPage.Props["BillingPath"] = "admin_console/billing/subscription"
bodyPage.Props["SiteURL"] = siteURL

Просмотреть файл

@@ -85,14 +85,51 @@ func TestSendAdminUpgradeRequestEmail(t *testing.T) {
*cfg.ExperimentalSettings.CloudUserLimit = 10
})
err := th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription)
err := th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription, model.InviteLimitation)
require.Nil(t, err)
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser2.Username, mockSubscription)
require.Nil(t, err)
// other attempts by the same user or other users to send emails are blocked by rate limiter
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription, model.InviteLimitation)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
// second attempt by the same user to send emails is blocked by rate limiter
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription)
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser2.Username, mockSubscription, model.InviteLimitation)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
}
func TestSendAdminUpgradeRequestEmailOnJoin(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
mockSubscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "MyCustomer",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 1000000000,
EndAt: 2000000000,
CreateAt: 1000000000,
Seats: 100,
DNS: "some.dns.server",
IsPaidTier: "false",
}
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ExperimentalSettings.CloudUserLimit = 10
})
err := th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription, model.JoinLimitation)
require.Nil(t, err)
// other attempts by the same user or other users to send emails are blocked by rate limiter
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser.Username, mockSubscription, model.JoinLimitation)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
err = th.App.SendAdminUpgradeRequestEmail(th.BasicUser2.Username, mockSubscription, model.JoinLimitation)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
}

Просмотреть файл

@@ -13264,7 +13264,7 @@ func (a *OpenTracingAppLayer) SendAckToPushProxy(ack *model.PushNotificationAck)
return resultVar0
}
func (a *OpenTracingAppLayer) SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription) *model.AppError {
func (a *OpenTracingAppLayer) SendAdminUpgradeRequestEmail(username string, subscription *model.Subscription, action string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAdminUpgradeRequestEmail")
@@ -13276,7 +13276,7 @@ func (a *OpenTracingAppLayer) SendAdminUpgradeRequestEmail(username string, subs
}()
defer span.Finish()
resultVar0 := a.app.SendAdminUpgradeRequestEmail(username, subscription)
resultVar0 := a.app.SendAdminUpgradeRequestEmail(username, subscription, action)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))

Просмотреть файл

@@ -3006,6 +3006,10 @@
"id": "api.templates.upgrade_request_info4",
"translation": "Because your workspace has reached the user limit for the free version of Mattermost cloud, invitations cannot be sent. Upgrade now to allow more users to join your workspace."
},
{
"id": "api.templates.upgrade_request_info4_2",
"translation": "Someone recently tried to join your workspace but was unable to as your workspace has reached the user limit for the free version of Mattermost cloud. Upgrade now to allow more users to join your workspace."
},
{
"id": "api.templates.upgrade_request_subject",
"translation": "Mattermost user request upgrade of workspace"
@@ -3014,6 +3018,10 @@
"id": "api.templates.upgrade_request_title",
"translation": "{{ .UserName }} would like to invite members to your workspace"
},
{
"id": "api.templates.upgrade_request_title2",
"translation": "New users are unable to join your workspace"
},
{
"id": "api.templates.user_access_token_body.info",
"translation": "A personal access token was added to your account on {{ .SiteURL }}. They can be used to access {{.SiteName}} with your account."
@@ -6442,10 +6450,6 @@
"id": "ent.elasticsearch.not_started.error",
"translation": "Elasticsearch is not started"
},
{
"id": "ent.elasticsearch.post.get_files_batch_for_indexing.error",
"translation": "Unable to get the files batch for indexing."
},
{
"id": "ent.elasticsearch.post.get_posts_batch_for_indexing.error",
"translation": "Unable to get the posts batch for indexing."

Просмотреть файл

@@ -5982,3 +5982,13 @@ func (c *Client4) SendAdminUpgradeRequestEmail() *Response {
return BuildResponse(r)
}
func (c *Client4) SendAdminUpgradeRequestEmailOnJoin() *Response {
r, appErr := c.DoApiPost(c.GetCloudRoute()+"/subscription/limitreached/join", "")
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}

Просмотреть файл

@@ -6,6 +6,8 @@ package model
const (
EventTypeFailedPayment = "failed-payment"
EventTypeFailedPaymentNoCard = "failed-payment-no-card"
JoinLimitation = "join"
InviteLimitation = "invite"
)
// Product model represents a product on the cloud system.