[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>
Этот коммит содержится в:
Nick Misasi
2020-10-26 13:24:26 -04:00
коммит произвёл GitHub
родитель b68f171162
Коммит 3697f92045
7 изменённых файлов: 276 добавлений и 0 удалений

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

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

@@ -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")

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

@@ -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 — thats 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": "Youve 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 Обычный файл
Просмотреть файл

@@ -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}}