From c3e9c414408a8c9c2806af12e659e395c605496f Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Mon, 28 May 2018 16:20:08 +0200 Subject: [MM-1915] Add Deactivate Account - server side (#8699) --- api4/user.go | 13 +++++++++++++ api4/user_test.go | 15 +++++++++++++++ app/email.go | 22 ++++++++++++++++++++++ config/default.json | 1 + i18n/en.json | 20 ++++++++++++++++++++ model/config.go | 5 +++++ templates/deactivate_body.html | 41 +++++++++++++++++++++++++++++++++++++++++ utils/config.go | 2 +- 8 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 templates/deactivate_body.html diff --git a/api4/user.go b/api4/user.go index 2a539a551..ea90d2127 100644 --- a/api4/user.go +++ b/api4/user.go @@ -713,6 +713,12 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) { return } + // if EnableUserDeactivation flag is disabled the user cannot deactivate himself. + if isSelfDeactive && !*c.App.GetConfig().TeamSettings.EnableUserDeactivation { + c.Err = model.NewAppError("updateUserActive", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized) + return + } + var user *model.User var err *model.AppError @@ -725,6 +731,13 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = err } else { c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active)) + if isSelfDeactive { + c.App.Go(func() { + if err = c.App.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil { + mlog.Error(err.Error()) + } + }) + } ReturnStatusOK(w) } } diff --git a/api4/user_test.go b/api4/user_test.go index 4851f139e..593208c92 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1198,6 +1198,12 @@ func TestUpdateUserActive(t *testing.T) { SystemAdminClient := th.SystemAdminClient user := th.BasicUser + EnableUserDeactivation := th.App.Config().TeamSettings.EnableUserDeactivation + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserDeactivation = EnableUserDeactivation }) + }() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = true }) pass, resp := Client.UpdateUserActive(user.Id, false) CheckNoError(t, resp) @@ -1205,6 +1211,15 @@ func TestUpdateUserActive(t *testing.T) { t.Fatal("should have returned true") } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = false }) + pass, resp = Client.UpdateUserActive(user.Id, false) + CheckUnauthorizedStatus(t, resp) + + if pass { + t.Fatal("should have returned false") + } + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = true }) pass, resp = Client.UpdateUserActive(user.Id, false) CheckUnauthorizedStatus(t, resp) diff --git a/app/email.go b/app/email.go index aa05cefdb..206c48aaa 100644 --- a/app/email.go +++ b/app/email.go @@ -322,6 +322,28 @@ func (a *App) NewEmailTemplate(name, locale string) *utils.HTMLTemplate { return t } +func (a *App) SendDeactivateAccountEmail(email string, locale, siteURL string) *model.AppError { + T := utils.GetUserTranslations(locale) + + rawUrl, _ := url.Parse(siteURL) + + subject := T("api.templates.deactivate_subject", + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], + "ServerURL": rawUrl.Host}) + + bodyPage := a.NewEmailTemplate("deactivate_body", locale) + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = T("api.templates.deactivate_body.title", map[string]interface{}{"ServerURL": rawUrl.Host}) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.deactivate_body.info", + map[string]interface{}{"SiteURL": siteURL}) + + if err := a.SendMail(email, subject, bodyPage.Render()); err != nil { + return model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + func (a *App) SendMail(to, subject, htmlBody string) *model.AppError { license := a.License() return utils.SendMailUsingConfig(to, subject, htmlBody, a.Config(), license != nil && *license.Features.Compliance) diff --git a/config/default.json b/config/default.json index 2d6265dcc..3548339d0 100644 --- a/config/default.json +++ b/config/default.json @@ -71,6 +71,7 @@ "EnableTeamCreation": true, "EnableUserCreation": true, "EnableOpenServer": false, + "EnableUserDeactivation": false, "RestrictCreationToDomains": "", "EnableCustomBrand": false, "CustomBrandText": "", diff --git a/i18n/en.json b/i18n/en.json index 9b59e59f2..1f4d55476 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2750,6 +2750,22 @@ "id": "api.templates.welcome_subject", "translation": "[{{ .SiteName }}] You joined {{ .ServerURL }}" }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Your account at {{ .ServerURL }} has been deactivated" + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Your account has been deactivated at {{ .ServerURL }}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "You deactivated your account on {{ .SiteURL }}.
If this change wasn't initiated by you or you want to reactivate your account, contact your system administrator." + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "Failed to send the deactivate account email successfully" + }, { "id": "api.user.activate_mfa.email_and_ldap_only.app_error", "translation": "MFA is not available for this account type" @@ -3074,6 +3090,10 @@ "id": "api.user.update_active.permissions.app_error", "translation": "You do not have the appropriate permissions" }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "You cannot deactivate yourself because this feature is not enabled. Please contact your System Administrator." + }, { "id": "api.user.update_mfa.not_available.app_error", "translation": "MFA not configured or available on this server" diff --git a/model/config.go b/model/config.go index deaae6db8..ba3a02d33 100644 --- a/model/config.go +++ b/model/config.go @@ -1002,6 +1002,7 @@ type TeamSettings struct { EnableTeamCreation *bool EnableUserCreation *bool EnableOpenServer *bool + EnableUserDeactivation *bool RestrictCreationToDomains string EnableCustomBrand *bool CustomBrandText *string @@ -1036,6 +1037,10 @@ func (s *TeamSettings) SetDefaults() { s.EnableCustomBrand = NewBool(false) } + if s.EnableUserDeactivation == nil { + s.EnableUserDeactivation = NewBool(false) + } + if s.CustomBrandText == nil { s.CustomBrandText = NewString(TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT) } diff --git a/templates/deactivate_body.html b/templates/deactivate_body.html new file mode 100644 index 000000000..45a2f42af --- /dev/null +++ b/templates/deactivate_body.html @@ -0,0 +1,41 @@ +{{define "deactivate_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} diff --git a/utils/config.go b/utils/config.go index c51ace554..d3fd733b7 100644 --- a/utils/config.go +++ b/utils/config.go @@ -448,7 +448,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/") props["SiteName"] = c.TeamSettings.SiteName props["EnableTeamCreation"] = strconv.FormatBool(*c.TeamSettings.EnableTeamCreation) - props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation) + props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation) props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer) props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage props["RestrictTeamInvite"] = *c.TeamSettings.RestrictTeamInvite -- cgit v1.2.3-1-g7c22 From c37d153ffb276e501660133de836a61eec25e544 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Mon, 28 May 2018 22:31:08 +0800 Subject: [MM-10519] Send websocket event whenever the channel has changed it's type (public|private) (#8798) * send websocket event whenever the channel has changed it's type (public|private) * updated per comment Signed-off-by: Saturnino Abril * add channel_converted websocket event Signed-off-by: Saturnino Abril * only send channel_id via websocket message for "channel_converted" event Signed-off-by: Saturnino Abril --- api4/channel_test.go | 31 +++++++++++++++++++++++++++++++ app/channel.go | 6 ++++++ model/websocket_message.go | 3 ++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/api4/channel_test.go b/api4/channel_test.go index b428a382a..2a1e78753 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -933,12 +934,42 @@ func TestConvertChannelToPrivate(t *testing.T) { t.Fatal("should not return a channel") } + WebSocketClient, err := th.CreateWebSocketClient() + if err != nil { + t.Fatal(err) + } + WebSocketClient.Listen() + publicChannel2 := th.CreatePublicChannel() rchannel, resp = th.SystemAdminClient.ConvertChannelToPrivate(publicChannel2.Id) CheckOKStatus(t, resp) if rchannel.Type != model.CHANNEL_PRIVATE { t.Fatal("channel should be converted from public to private") } + + stop := make(chan bool) + eventHit := false + + go func() { + for { + select { + case resp := <-WebSocketClient.EventChannel: + if resp.Event == model.WEBSOCKET_EVENT_CHANNEL_CONVERTED && resp.Data["channel_id"].(string) == publicChannel2.Id { + eventHit = true + } + case <-stop: + return + } + } + }() + + time.Sleep(400 * time.Millisecond) + + stop <- true + + if !eventHit { + t.Fatal("did not receive channel_converted event") + } } func TestRestoreChannel(t *testing.T) { diff --git a/app/channel.go b/app/channel.go index 26e3d771c..b5afdea2d 100644 --- a/app/channel.go +++ b/app/channel.go @@ -366,6 +366,12 @@ func (a *App) UpdateChannelPrivacy(oldChannel *model.Channel, user *model.User) return channel, err } + a.InvalidateCacheForChannel(channel) + + messageWs := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_CONVERTED, channel.TeamId, "", "", nil) + messageWs.Add("channel_id", channel.Id) + a.Publish(messageWs) + return channel, nil } } diff --git a/model/websocket_message.go b/model/websocket_message.go index 071975d6c..31b16d1a7 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -14,8 +14,9 @@ const ( WEBSOCKET_EVENT_POSTED = "posted" WEBSOCKET_EVENT_POST_EDITED = "post_edited" WEBSOCKET_EVENT_POST_DELETED = "post_deleted" - WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" + WEBSOCKET_EVENT_CHANNEL_CONVERTED = "channel_converted" WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" + WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" -- cgit v1.2.3-1-g7c22