[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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6b388871a9
Коммит
0dad204007
@@ -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
|
||||
|
||||
20
app/cloud.go
20
app/cloud.go
@@ -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++
|
||||
|
||||
16
app/email.go
16
app/email.go
@@ -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))
|
||||
|
||||
12
i18n/en.json
12
i18n/en.json
@@ -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.
|
||||
|
||||
Ссылка в новой задаче
Block a user