[MM-28363] User Limit Overage Warning Emails (#16053)
* Adding files, commit of UI in good shape * Translations added, working with activation and deactivation * Add check for error * Fix i18n? * Push without subscription check so Steve and Matt can look at it * Fix font-weight in chrome * Fix font-weight on button * UX fixes * Fixes for PR * Add back subscription stuff * Fix tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
19
api4/user.go
19
api4/user.go
@@ -155,6 +155,15 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// New user created, check cloud limits and send emails if needed
|
||||
if ruser != nil {
|
||||
err = c.App.CheckAndSendUserLimitWarningEmails()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddMeta("user", ruser) // overwrite meta
|
||||
|
||||
@@ -1349,6 +1358,16 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ACTIVATION_STATUS_CHANGE, "", "", "", nil)
|
||||
c.App.Publish(message)
|
||||
|
||||
// If activating, run cloud check for limit overages
|
||||
if active {
|
||||
emailErr := c.App.CheckAndSendUserLimitWarningEmails()
|
||||
if emailErr != nil {
|
||||
c.Err = emailErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
|
||||
@@ -388,6 +388,7 @@ type AppIface interface {
|
||||
CancelJob(jobId string) *model.AppError
|
||||
ChannelMembersToAdd(since int64, channelID *string) ([]*model.UserChannelIDPair, *model.AppError)
|
||||
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
|
||||
CheckAndSendUserLimitWarningEmails() *model.AppError
|
||||
CheckForClientSideCert(r *http.Request) (string, string, string)
|
||||
CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError
|
||||
CheckRolesExist(roleNames []string) *model.AppError
|
||||
|
||||
57
app/cloud.go
Обычный файл
57
app/cloud.go
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func (a *App) CheckAndSendUserLimitWarningEmails() *model.AppError {
|
||||
if a.Srv().License() == nil || (a.Srv().License() != nil && !*a.Srv().License().Features.Cloud) {
|
||||
// Not cloud instance, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
subscription, subErr := a.Cloud().GetSubscription()
|
||||
if subErr != nil {
|
||||
return subErr
|
||||
}
|
||||
|
||||
if subscription != nil && subscription.IsPaidTier == "true" {
|
||||
// Paid subscription, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
cloudUserLimit := *a.Config().ExperimentalSettings.CloudUserLimit
|
||||
systemUserCount, _ := a.Srv().Store.User().Count(model.UserCountOptions{})
|
||||
remainingUsers := cloudUserLimit - systemUserCount
|
||||
|
||||
if remainingUsers > 0 {
|
||||
return nil
|
||||
}
|
||||
userOptions := &model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 100,
|
||||
Role: model.SYSTEM_ADMIN_ROLE_ID,
|
||||
Inactive: false,
|
||||
}
|
||||
sysAdmins, err := a.GetUsers(userOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// -1 means they are 1 user over the limit - we only want to send the email for the 11th user
|
||||
if remainingUsers == -1 {
|
||||
// Over limit by 1 user
|
||||
for admin := range sysAdmins {
|
||||
a.Srv().EmailService.SendOverUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
}
|
||||
} else if remainingUsers == 0 {
|
||||
// At limit
|
||||
for admin := range sysAdmins {
|
||||
a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
44
app/email.go
44
app/email.go
@@ -589,3 +589,47 @@ func (es *EmailService) CreateVerifyEmailToken(userId string, newEmail string) (
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendAtUserLimitWarningEmail(email string, locale string, siteURL string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.at_limit_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("reached_user_limit_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.at_limit_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.at_limit_info1")
|
||||
bodyPage.Props["Info2"] = T("api.templates.at_limit_info2")
|
||||
bodyPage.Props["Button"] = T("api.templates.upgrade_mattermost_cloud")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendAtUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserLimitWarningEmail(email string, locale string, siteURL string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("reached_user_limit_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_info1")
|
||||
bodyPage.Props["Info2"] = T("api.templates.over_limit_info2")
|
||||
bodyPage.Props["Button"] = T("api.templates.upgrade_mattermost_cloud")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -1010,6 +1010,28 @@ func (a *OpenTracingAppLayer) ChannelMembersToRemove(teamID *string) ([]*model.C
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckAndSendUserLimitWarningEmails() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckAndSendUserLimitWarningEmails")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.CheckAndSendUserLimitWarningEmails()
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, string, string) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckForClientSideCert")
|
||||
|
||||
44
i18n/en.json
44
i18n/en.json
@@ -2586,6 +2586,26 @@
|
||||
"id": "api.team.update_team_scheme.scheme_scope.error",
|
||||
"translation": "Unable to set the scheme to the team because the supplied scheme is not a team scheme."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.at_limit_info1",
|
||||
"translation": "It looks like you have 10 or more users in your workspace now — that’s great! If you want to invite more team members, consider upgrading Mattermost Cloud Professional now."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.at_limit_info2",
|
||||
"translation": "Alternatively, you can disable users in the Admin Console to open up spots for more users or stay below the free user limit."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.at_limit_subject",
|
||||
"translation": "Mattermost Cloud User Limit Reached"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.at_limit_title",
|
||||
"translation": "You’ve reached the user limit for the free tier "
|
||||
},
|
||||
{
|
||||
"id": "api.templates.copyright",
|
||||
"translation": "© 2017 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.deactivate_body.info",
|
||||
"translation": "You deactivated your account on {{ .SiteURL }}."
|
||||
@@ -2650,6 +2670,10 @@
|
||||
"id": "api.templates.email_organization",
|
||||
"translation": "Sent by "
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_us_anytime_at",
|
||||
"translation": "Email us any time at "
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_warning",
|
||||
"translation": "If you did not make this change, please contact the system administrator."
|
||||
@@ -2702,6 +2726,22 @@
|
||||
"id": "api.templates.mfa_deactivated_body.title",
|
||||
"translation": "Multi-factor authentication was removed"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_info1",
|
||||
"translation": "It looks like you have more than 10 users in your workspace which is beyond the free tier limits of Mattermost Cloud Professional. To avoid any disruption in your Mattermost workspace, please upgrade."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_info2",
|
||||
"translation": "Alternatively, you can disable users in the Admin Console to open up spots for more users or stay below the free user limit."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_subject",
|
||||
"translation": "Mattermost Cloud Workspace Over User Limit"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_title",
|
||||
"translation": "Your workspace is over the user limit for the free tier"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.password_change_body.info",
|
||||
"translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .TeamURL }} by {{.Method}}."
|
||||
@@ -2766,6 +2806,10 @@
|
||||
"id": "api.templates.signin_change_email.subject",
|
||||
"translation": "[{{ .SiteName }}] Your sign-in method has been updated"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.upgrade_mattermost_cloud",
|
||||
"translation": "Upgrade"
|
||||
},
|
||||
{
|
||||
"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."
|
||||
|
||||
89
templates/reached_user_limit_body.html
Обычный файл
89
templates/reached_user_limit_body.html
Обычный файл
@@ -0,0 +1,89 @@
|
||||
{{define "reached_user_limit_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5" width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 20px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2 style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription" target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 14px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
<p style="font-weight: normal; line-height: 20px; color: #3D3C40; margin-bottom: 24px; font-size: 14px; font-family: Arial">
|
||||
{{ .Props.Info2 }}</p>
|
||||
<img src="{{.Props.SiteURL}}/static/images/credit-card-empty-state.png"
|
||||
width="320px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
Ссылка в новой задаче
Block a user