summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/team.go4
-rw-r--r--api/templates/email_change_body.html5
-rw-r--r--api/templates/email_change_subject.html2
-rw-r--r--api/templates/email_change_verify_body.html56
-rw-r--r--api/templates/email_change_verify_subject.html1
-rw-r--r--api/templates/find_teams_body.html4
-rw-r--r--api/templates/invite_body.html2
-rw-r--r--api/templates/password_change_body.html2
-rw-r--r--api/templates/post_body.html2
-rw-r--r--api/templates/reset_body.html2
-rw-r--r--api/templates/signup_team_body.html2
-rw-r--r--api/templates/verify_body.html2
-rw-r--r--api/templates/welcome_body.html2
-rw-r--r--api/user.go32
-rw-r--r--config/config.json2
-rw-r--r--docker/dev/config_docker.json2
-rw-r--r--docker/local/config_docker.json2
-rw-r--r--mattermost.go93
-rw-r--r--model/config.go6
-rw-r--r--model/security_bulletin.go55
-rw-r--r--store/sql_user_store.go31
-rw-r--r--store/sql_user_store_test.go23
-rw-r--r--store/store.go1
-rw-r--r--utils/config.go1
-rw-r--r--utils/diagnostic.go26
-rw-r--r--web/react/components/about_build_modal.jsx62
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx16
-rw-r--r--web/react/components/create_comment.jsx3
-rw-r--r--web/react/components/navbar_dropdown.jsx22
-rw-r--r--web/react/components/post_deleted_modal.jsx36
-rw-r--r--web/react/components/rhs_thread.jsx11
-rw-r--r--web/react/components/team_signup_choose_auth.jsx2
-rw-r--r--web/react/components/team_signup_with_email.jsx2
-rw-r--r--web/react/components/team_signup_with_sso.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx69
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx11
-rw-r--r--web/sass-files/sass/partials/_settings.scss1
-rw-r--r--web/static/images/Battlehouse-logodark.pngbin6981 -> 0 bytes
-rw-r--r--web/static/images/Mattermost-logodark.pngbin10380 -> 0 bytes
-rw-r--r--web/static/images/logo-email.png (renamed from web/static/images/Bladekick-logodark.png)bin10380 -> 10380 bytes
-rw-r--r--web/web.go7
41 files changed, 495 insertions, 109 deletions
diff --git a/api/team.go b/api/team.go
index 152e3d6d7..9021fefb9 100644
--- a/api/team.go
+++ b/api/team.go
@@ -432,9 +432,9 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
}
subjectPage := NewServerTemplatePage("find_teams_subject")
- subjectPage.Props["SiteURL"] = c.GetSiteURL()
+ subjectPage.ClientProps["SiteURL"] = c.GetSiteURL()
bodyPage := NewServerTemplatePage("find_teams_body")
- bodyPage.Props["SiteURL"] = c.GetSiteURL()
+ bodyPage.ClientProps["SiteURL"] = c.GetSiteURL()
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
c.Err = result.Err
diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html
index 0ec4ace2a..d4e6abd02 100644
--- a/api/templates/email_change_body.html
+++ b/api/templates/email_change_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
@@ -18,7 +18,7 @@
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2>
- <p>You updated your email for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }}<br> If this change wasn't initiated by you, please reply to this email and let us know.</p>
+ <p>You email address for {{.Props.TeamDisplayName}} has been changed to {{.Props.NewEmail}}.<br>If you did not make this change, please contact the system administrator.</p>
</td>
</tr>
<tr>
@@ -51,4 +51,3 @@
</table>
{{end}}
-
diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html
index 5690b148a..962ae868e 100644
--- a/api/templates/email_change_subject.html
+++ b/api/templates/email_change_subject.html
@@ -1 +1 @@
-{{define "email_change_subject"}}You updated your email for {{.Props.TeamDisplayName}} on {{ .Props.Domain }}{{end}}
+{{define "email_change_subject"}}[{{.ClientProps.SiteName}}] Your email address has changed for {{.Props.TeamDisplayName}}{{end}}
diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html
new file mode 100644
index 000000000..356f2454c
--- /dev/null
+++ b/api/templates/email_change_verify_body.html
@@ -0,0 +1,56 @@
+{{define "email_change_verify_body"}}
+
+<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
+ <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="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
+ <tr>
+ <td style="padding: 20px 20px 10px; text-align:left;">
+ <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
+ <tr>
+ <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
+ <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2>
+ <p>To finish updating your email address for {{.Props.TeamDisplayName}}, please click the link below to confirm this is the right address.</p>
+ <p style="margin: 20px 0 15px">
+ <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a>
+ </p>
+ </td>
+ </tr>
+ <tr>
+ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;">
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
+ Best wishes,<br>
+ The {{.ClientProps.SiteName}} Team<br>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;">
+ <p style="margin: 25px 0;">
+ <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt="">
+ </p>
+ <p style="padding: 0 50px;">
+ (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ </p>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+{{end}}
diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html
new file mode 100644
index 000000000..5e2ac1452
--- /dev/null
+++ b/api/templates/email_change_verify_subject.html
@@ -0,0 +1 @@
+{{define "email_change_verify_subject"}}[{{.ClientProps.SiteName}}] Verify new email address for {{.Props.TeamDisplayName}}{{end}}
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
index 9d34b7a23..3046ee5f8 100644
--- a/api/templates/find_teams_body.html
+++ b/api/templates/find_teams_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.ClientProps.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
@@ -42,7 +42,7 @@
<tr>
<td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;">
<p style="margin: 25px 0;">
- <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt="">
+ <img width="65" src="{{.ClientProps.SiteURL}}/static/images/circles.png" alt="">
</p>
<p style="padding: 0 50px;">
(c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br>
diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html
index 9e1ce33b2..fdfcfa9f1 100644
--- a/api/templates/invite_body.html
+++ b/api/templates/invite_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html
index 3fef3a5c8..c420d7a69 100644
--- a/api/templates/password_change_body.html
+++ b/api/templates/password_change_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/post_body.html b/api/templates/post_body.html
index a6b81e2f6..1dd30ca45 100644
--- a/api/templates/post_body.html
+++ b/api/templates/post_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html
index dc6152627..d388689cf 100644
--- a/api/templates/reset_body.html
+++ b/api/templates/reset_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index e6ffb3a5b..83c1679b9 100644
--- a/api/templates/signup_team_body.html
+++ b/api/templates/signup_team_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html
index 8187c8908..def067a84 100644
--- a/api/templates/verify_body.html
+++ b/api/templates/verify_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html
index 5fe3450b7..ff31ee8d5 100644
--- a/api/templates/welcome_body.html
+++ b/api/templates/welcome_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/api/user.go b/api/user.go
index 78f8768a4..34cbec151 100644
--- a/api/user.go
+++ b/api/user.go
@@ -887,7 +887,11 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
l4g.Error(tresult.Err.Message)
} else {
team := tresult.Data.(*model.Team)
- fireAndForgetEmailChangeEmail(rusers[1].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
+ fireAndForgetEmailChangeEmail(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
+
+ if utils.Cfg.EmailSettings.RequireEmailVerification {
+ FireAndForgetEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ }
}
}
@@ -1328,7 +1332,7 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL,
}()
}
-func fireAndForgetEmailChangeEmail(email, teamDisplayName, teamURL, siteURL string) {
+func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
go func() {
subjectPage := NewServerTemplatePage("email_change_subject")
@@ -1338,14 +1342,34 @@ func fireAndForgetEmailChangeEmail(email, teamDisplayName, teamURL, siteURL stri
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["TeamDisplayName"] = teamDisplayName
bodyPage.Props["TeamURL"] = teamURL
+ bodyPage.Props["NewEmail"] = newEmail
- if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
- l4g.Error("Failed to send update password email successfully err=%v", err)
+ if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send email change notification email successfully err=%v", err)
}
}()
}
+func FireAndForgetEmailChangeVerifyEmail(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) {
+ go func() {
+
+ link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)
+
+ subjectPage := NewServerTemplatePage("email_change_verify_subject")
+ subjectPage.Props["SiteURL"] = siteURL
+ subjectPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage := NewServerTemplatePage("email_change_verify_body")
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage.Props["VerifyUrl"] = link
+
+ if err := utils.SendMail(newUserEmail, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send email change verification email successfully err=%v", err)
+ }
+ }()
+}
+
func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
diff --git a/config/config.json b/config/config.json
index 88da33215..919737da7 100644
--- a/config/config.json
+++ b/config/config.json
@@ -78,7 +78,7 @@
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true,
- "EnableDiagnostic": false
+ "EnableSecurityFixAlert": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index ef91a21ea..ab5b0a7be 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -78,7 +78,7 @@
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true,
- "EnableDiagnostic": false
+ "EnableSecurityFixAlert": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index ef91a21ea..ab5b0a7be 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -78,7 +78,7 @@
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true,
- "EnableDiagnostic": false
+ "EnableSecurityFixAlert": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/mattermost.go b/mattermost.go
index e78e8d04a..8fa217a3e 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -6,6 +6,9 @@ package main
import (
"flag"
"fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
"os"
"os/signal"
"runtime"
@@ -63,7 +66,7 @@ func main() {
manualtesting.InitManualTesting()
}
- diagnosticsJob()
+ securityAndDiagnosticsJob()
// wait for kill signal before attempting to gracefully shutdown
// the running service
@@ -75,49 +78,85 @@ func main() {
}
}
-func diagnosticsJob() {
+func securityAndDiagnosticsJob() {
go func() {
for {
- if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
+ if utils.Cfg.PrivacySettings.EnableSecurityFixAlert && model.IsOfficalBuild() {
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
- lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0)
+ lastSecurityTime, _ := strconv.ParseInt(props["LastSecurityTime"], 10, 0)
currentTime := model.GetMillis()
- if (currentTime - lastTime) > 1000*60*60*24*7 {
- l4g.Info("Sending error and diagnostic information to mattermost")
+ id := props["DiagnosticId"]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: "DiagnosticId", Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
- id := props["DiagnosticId"]
- if len(id) == 0 {
- id = model.NewId()
- systemId := &model.System{Name: "DiagnosticId", Value: id}
- <-api.Srv.Store.System().Save(systemId)
- }
+ v := url.Values{}
+ v.Set(utils.PROP_DIAGNOSTIC_ID, id)
+ v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
+ v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
+ v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
+ v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT)
+
+ if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
+ l4g.Info("Checking for security update from Mattermost")
- systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)}
- if lastTime == 0 {
- <-api.Srv.Store.System().Save(systemLastTime)
+ systemSecurityLastTime := &model.System{Name: "LastSecurityTime", Value: strconv.FormatInt(currentTime, 10)}
+ if lastSecurityTime == 0 {
+ <-api.Srv.Store.System().Save(systemSecurityLastTime)
} else {
- <-api.Srv.Store.System().Update(systemLastTime)
+ <-api.Srv.Store.System().Update(systemSecurityLastTime)
}
- m := make(map[string]string)
- m[utils.PROP_DIAGNOSTIC_ID] = id
- m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber
- m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName
- m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS
- m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT
-
- if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
- m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10)
+ res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode())
+ if err != nil {
+ l4g.Error("Failed to get security update information from Mattermost.")
+ return
}
- utils.SendDiagnostic(m)
+ bulletins := model.SecurityBulletinsFromJson(res.Body)
+
+ for _, bulletin := range bulletins {
+ if bulletin.AppliesToVersion == model.CurrentVersion {
+ if props["SecurityBulletin_"+bulletin.Id] == "" {
+ if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil {
+ l4g.Error("Failed to get system admins for security update information from Mattermost.")
+ return
+ } else {
+ users := results.Data.(map[string]*model.User)
+
+ resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id)
+ if err != nil {
+ l4g.Error("Failed to get security bulletin details")
+ return
+ }
+
+ body, err := ioutil.ReadAll(resBody.Body)
+ res.Body.Close()
+ if err != nil || resBody.StatusCode != 200 {
+ l4g.Error("Failed to read security bulletin details")
+ return
+ }
+
+ for _, user := range users {
+ l4g.Info("Sending security bulletin for " + bulletin.Id + " to " + user.Email)
+ utils.SendMail(user.Email, "Mattermost Security Bulletin", string(body))
+ }
+ }
+
+ bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
+ <-api.Srv.Store.System().Save(bulletinSeen)
+ }
+ }
+ }
}
}
}
- time.Sleep(time.Hour * 24)
+ time.Sleep(time.Hour * 4)
}
}()
}
diff --git a/model/config.go b/model/config.go
index c67b36063..086b0d4ee 100644
--- a/model/config.go
+++ b/model/config.go
@@ -110,9 +110,9 @@ type RateLimitSettings struct {
}
type PrivacySettings struct {
- ShowEmailAddress bool
- ShowFullName bool
- EnableDiagnostic bool
+ ShowEmailAddress bool
+ ShowFullName bool
+ EnableSecurityFixAlert bool
}
type TeamSettings struct {
diff --git a/model/security_bulletin.go b/model/security_bulletin.go
new file mode 100644
index 000000000..a64e03f6d
--- /dev/null
+++ b/model/security_bulletin.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type SecurityBulletin struct {
+ Id string `json:"id"`
+ AppliesToVersion string `json:"applies_to_version"`
+}
+
+type SecurityBulletins []SecurityBulletin
+
+func (me *SecurityBulletin) ToJson() string {
+ b, err := json.Marshal(me)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func SecurityBulletinFromJson(data io.Reader) *SecurityBulletin {
+ decoder := json.NewDecoder(data)
+ var o SecurityBulletin
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (me SecurityBulletins) ToJson() string {
+ if b, err := json.Marshal(me); err != nil {
+ return "[]"
+ } else {
+ return string(b)
+ }
+}
+
+func SecurityBulletinsFromJson(data io.Reader) SecurityBulletins {
+ decoder := json.NewDecoder(data)
+ var o SecurityBulletins
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o
+ } else {
+ return nil
+ }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 0a723d965..f82f87290 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -370,6 +370,37 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
return storeChannel
}
+func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*model.User
+
+ if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE Roles = :Roles", map[string]interface{}{"Roles": "system_admin"}); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetSystemAdminProfiles", "We encounted an error while finding user profiles", err.Error())
+ } else {
+
+ userMap := make(map[string]*model.User)
+
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = ""
+ userMap[u.Id] = u
+ }
+
+ result.Data = userMap
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index e2a454023..bfdd14fef 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -259,6 +259,29 @@ func TestUserStoreGetProfiles(t *testing.T) {
}
}
+func TestUserStoreGetSystemAdminProfiles(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ u2 := model.User{}
+ u2.TeamId = u1.TeamId
+ u2.Email = model.NewId()
+ Must(store.User().Save(&u2))
+
+ if r1 := <-store.User().GetSystemAdminProfiles(); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) <= 0 {
+ t.Fatal("invalid returned system admin users")
+ }
+ }
+}
+
func TestUserStoreGetByEmail(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 887913bc6..fc088ce74 100644
--- a/store/store.go
+++ b/store/store.go
@@ -104,6 +104,7 @@ type UserStore interface {
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
GetTotalUsersCount() StoreChannel
+ GetSystemAdminProfiles() StoreChannel
}
type SessionStore interface {
diff --git a/utils/config.go b/utils/config.go
index 90e44259a..0eea6dd6a 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -191,6 +191,7 @@ func getClientProperties(c *model.Config) map[string]string {
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
+ props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable)
diff --git a/utils/diagnostic.go b/utils/diagnostic.go
index 9a61ae934..e40948f62 100644
--- a/utils/diagnostic.go
+++ b/utils/diagnostic.go
@@ -5,41 +5,31 @@ package utils
import (
"net/http"
-
- l4g "code.google.com/p/log4go"
+ "net/url"
"github.com/mattermost/platform/model"
)
const (
+ DIAGNOSTIC_URL = "https://d7zmvsa9e04kk.cloudfront.net"
+
PROP_DIAGNOSTIC_ID = "id"
PROP_DIAGNOSTIC_CATEGORY = "c"
- VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d"
+ VAL_DIAGNOSTIC_CATEGORY_DEFAULT = "d"
PROP_DIAGNOSTIC_BUILD = "b"
PROP_DIAGNOSTIC_DATABASE = "db"
PROP_DIAGNOSTIC_OS = "os"
PROP_DIAGNOSTIC_USER_COUNT = "uc"
)
-func SendDiagnostic(data model.StringMap) *model.AppError {
- if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
-
- query := "?"
- for name, value := range data {
- if len(query) > 1 {
- query += "&"
- }
+func SendDiagnostic(values url.Values) {
+ if Cfg.PrivacySettings.EnableSecurityFixAlert && model.IsOfficalBuild() {
- query += name + "=" + UrlEncode(value)
- }
-
- res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query)
+ res, err := http.Get(DIAGNOSTIC_URL + "/i?" + values.Encode())
if err != nil {
- l4g.Error("Failed to send diagnostics %v", err.Error())
+ return
}
res.Body.Close()
}
-
- return nil
}
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx
new file mode 100644
index 000000000..d582f6bc8
--- /dev/null
+++ b/web/react/components/about_build_modal.jsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Modal = ReactBootstrap.Modal;
+
+export default class AboutBuildModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.doHide = this.doHide.bind(this);
+ }
+
+ doHide() {
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ const config = global.window.config;
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{`Mattermost ${config.Version}`}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Build Number:'}</div>
+ <div className='col-sm-9'>{config.BuildNumber}</div>
+ </div>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Build Date:'}</div>
+ <div className='col-sm-9'>{config.BuildDate}</div>
+ </div>
+ <div className='row'>
+ <div className='col-sm-3 info__label'>{'Build Hash:'}</div>
+ <div className='col-sm-9'>{config.BuildHash}</div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doHide}
+ >
+ {'Close'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+AboutBuildModal.defaultProps = {
+ show: false
+};
+
+AboutBuildModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired
+}; \ No newline at end of file
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index c74d321e6..3467e6a40 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -30,7 +30,7 @@ export default class PrivacySettings extends React.Component {
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
- config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked;
+ config.PrivacySettings.EnableSecurityFixAlert = React.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
Client.saveConfig(
config,
@@ -140,7 +140,7 @@ export default class PrivacySettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='EnableDiagnostic'
+ htmlFor='EnableSecurityFixAlert'
>
{'Send Error and Diagnostic: '}
</label>
@@ -148,10 +148,10 @@ export default class PrivacySettings extends React.Component {
<label className='radio-inline'>
<input
type='radio'
- name='EnableDiagnostic'
+ name='EnableSecurityFixAlert'
value='true'
- ref='EnableDiagnostic'
- defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic}
+ ref='EnableSecurityFixAlert'
+ defaultChecked={this.props.config.PrivacySettings.EnableSecurityFixAlert}
onChange={this.handleChange}
/>
{'true'}
@@ -159,14 +159,14 @@ export default class PrivacySettings extends React.Component {
<label className='radio-inline'>
<input
type='radio'
- name='EnableDiagnostic'
+ name='EnableSecurityFixAlert'
value='false'
- defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic}
+ defaultChecked={!this.props.config.PrivacySettings.EnableSecurityFixAlert}
onChange={this.handleChange}
/>
{'false'}
</label>
- <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p>
+ <p className='help-text'>{'When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'}</p>
</div>
</div>
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 9c233ea26..550f85d3d 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -106,10 +106,11 @@ export default class CreateComment extends React.Component {
let state = {};
if (err.message === 'Invalid RootId parameter') {
+ PostStore.removePendingPost(post.channel_id, post.pending_post_id);
+
if ($('#post_deleted').length > 0) {
$('#post_deleted').modal('show');
}
- PostStore.removePendingPost(post.pending_post_id);
} else {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 30c4e94ae..ff7a53848 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -6,6 +6,8 @@ var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
+var AboutBuildModal = require('./about_build_modal.jsx');
+
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
@@ -18,7 +20,9 @@ export default class NavbarDropdown extends React.Component {
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
+ this.handleAboutModal = this.handleAboutModal.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
+ this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
this.state = getStateFromStores();
}
@@ -26,6 +30,12 @@ export default class NavbarDropdown extends React.Component {
e.preventDefault();
client.logout();
}
+ handleAboutModal() {
+ this.setState({showAboutModal: true});
+ }
+ aboutModalDismissed() {
+ this.setState({showAboutModal: false});
+ }
componentDidMount() {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
@@ -228,6 +238,18 @@ export default class NavbarDropdown extends React.Component {
{'Report a Problem'}
</a>
</li>
+ <li>
+ <a
+ href='#'
+ onClick={this.handleAboutModal}
+ >
+ {'About Mattermost'}
+ </a>
+ </li>
+ <AboutBuildModal
+ show={this.state.showAboutModal}
+ onModalDismissed={this.aboutModalDismissed}
+ />
</ul>
</li>
</ul>
diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx
index d284a9d1b..3f487d20f 100644
--- a/web/react/components/post_deleted_modal.jsx
+++ b/web/react/components/post_deleted_modal.jsx
@@ -2,13 +2,41 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var Constants = require('../utils/constants.jsx');
+var ActionTypes = Constants.ActionTypes;
export default class PostDeletedModal extends React.Component {
constructor(props) {
super(props);
+ this.handleClose = this.handleClose.bind(this);
+
this.state = {};
}
+ componentDidMount() {
+ $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => {
+ this.handleClose();
+ });
+ }
+ handleClose() {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_SEARCH,
+ results: null
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_SEARCH_TERM,
+ term: null,
+ do_search: false,
+ is_mention_search: false
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST_SELECTED,
+ results: null
+ });
+ }
render() {
var currentUser = UserStore.getCurrentUser();
@@ -31,17 +59,17 @@ export default class PostDeletedModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
- Comment could not be posted
+ {'Comment could not be posted'}
</h4>
</div>
<div className='modal-body'>
- <p>Someone deleted the message on which you tried to post a comment.</p>
+ <p>{'Someone deleted the message on which you tried to post a comment.'}</p>
</div>
<div className='modal-footer'>
<button
@@ -49,7 +77,7 @@ export default class PostDeletedModal extends React.Component {
className='btn btn-primary'
data-dismiss='modal'
>
- Okay
+ {'Okay'}
</button>
</div>
</div>
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 2f23d80d9..27a784701 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -23,7 +23,7 @@ export default class RhsThread extends React.Component {
}
getStateFromStores() {
var postList = PostStore.getSelectedPost();
- if (!postList || postList.order.length < 1) {
+ if (!postList || postList.order.length < 1 || !postList.posts[postList.order[0]]) {
return {postList: {}};
}
@@ -49,7 +49,10 @@ export default class RhsThread extends React.Component {
}.bind(this));
}
componentDidUpdate() {
- $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ if ($('.post-right__scroll')[0]) {
+ $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ }
+
$('.post-right__scroll').perfectScrollbar('update');
this.resize();
}
@@ -67,7 +70,7 @@ export default class RhsThread extends React.Component {
// if something was changed in the channel like adding a
// comment or post then lets refresh the sidebar list
var currentSelected = PostStore.getSelectedPost();
- if (!currentSelected || currentSelected.order.length === 0) {
+ if (!currentSelected || currentSelected.order.length === 0 || !currentSelected.posts[currentSelected.order[0]]) {
return;
}
@@ -103,7 +106,7 @@ export default class RhsThread extends React.Component {
render() {
var postList = this.state.postList;
- if (postList == null) {
+ if (postList == null || !postList.order) {
return (
<div></div>
);
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index b8264b887..8cdeace03 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -52,7 +52,7 @@ export default class ChooseAuthPage extends React.Component {
<div>
{buttons}
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{'Find my team'}</a></span>
+ <span><a href='/find_team'>{'Find my teams'}</a></span>
</div>
</div>
);
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index ba9d4c3e0..015969dce 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -75,7 +75,7 @@ export default class EmailSignUpPage extends React.Component {
{serverError}
</div>
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{`Find my team`}</a></span>
+ <span><a href='/find_team'>{`Find my teams`}</a></span>
</div>
</form>
);
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 0c064411a..bc7e13738 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -112,7 +112,7 @@ export default class SSOSignUpPage extends React.Component {
{serverError}
</div>
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{'Find my team'}</a></span>
+ <span><a href='/find_team'>{'Find my teams'}</a></span>
</div>
</form>
);
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index c1d4c4ab5..c6c508ad7 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../../stores/user_store.jsx');
+var ErrorStore = require('../../stores/error_store.jsx');
var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var SettingPicture = require('../setting_picture.jsx');
@@ -27,6 +28,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.updateLastName = this.updateLastName.bind(this);
this.updateNickname = this.updateNickname.bind(this);
this.updateEmail = this.updateEmail.bind(this);
+ this.updateConfirmEmail = this.updateConfirmEmail.bind(this);
this.updatePicture = this.updatePicture.bind(this);
this.updateSection = this.updateSection.bind(this);
@@ -96,6 +98,7 @@ export default class UserSettingsGeneralTab extends React.Component {
var user = UserStore.getCurrentUser();
var email = this.state.email.trim().toLowerCase();
+ var confirmEmail = this.state.confirmEmail.trim().toLowerCase();
if (user.email === email) {
return;
@@ -106,8 +109,12 @@ export default class UserSettingsGeneralTab extends React.Component {
return;
}
- user.email = email;
+ if (email !== confirmEmail) {
+ this.setState({emailError: 'The new emails you entered do not match'});
+ return;
+ }
+ user.email = email;
this.submitUser(user);
}
submitUser(user) {
@@ -115,6 +122,13 @@ export default class UserSettingsGeneralTab extends React.Component {
function updateSuccess() {
this.updateSection('');
AsyncClient.getMe();
+ const verificationEnabled = global.window.config.SendEmailNotifications === 'true' && global.window.config.RequireEmailVerification === 'true';
+
+ if (verificationEnabled) {
+ ErrorStore.storeLastError({message: 'Check your email at ' + user.email + ' to verify the address.'});
+ ErrorStore.emitChange();
+ this.setState({emailChangeInProgress: true});
+ }
}.bind(this),
function updateFailure(err) {
var state = this.setupInitialState(this.props);
@@ -177,6 +191,9 @@ export default class UserSettingsGeneralTab extends React.Component {
updateEmail(e) {
this.setState({email: e.target.value});
}
+ updateConfirmEmail(e) {
+ this.setState({confirmEmail: e.target.value});
+ }
updatePicture(e) {
if (e.target.files && e.target.files[0]) {
this.setState({picture: e.target.files[0]});
@@ -188,7 +205,8 @@ export default class UserSettingsGeneralTab extends React.Component {
}
}
updateSection(section) {
- this.setState(assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: ''}));
+ const emailChangeInProgress = this.state.emailChangeInProgress;
+ this.setState(assign({}, this.setupInitialState(this.props), {emailChangeInProgress: emailChangeInProgress, clientError: '', serverError: '', emailError: ''}));
this.submitActive = false;
this.props.updateSection(section);
}
@@ -208,9 +226,9 @@ export default class UserSettingsGeneralTab extends React.Component {
}
setupInitialState(props) {
var user = props.user;
- var emailEnabled = global.window.config.SendEmailNotifications === 'true';
+
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
- email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
+ email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false};
}
render() {
var user = this.props.user;
@@ -434,10 +452,19 @@ export default class UserSettingsGeneralTab extends React.Component {
}
var emailSection;
if (this.props.activeSection === 'email') {
- let helpText = <div>Email is used for notifications, and requires verification if changed.</div>;
+ const emailEnabled = global.window.config.SendEmailNotifications === 'true';
+ const emailVerificationEnabled = global.window.config.RequireEmailVerification === 'true';
+ let helpText = 'Email is used for notifications, and requires verification if changed.';
- if (!this.state.emailEnabled) {
+ if (!emailEnabled) {
helpText = <div className='setting-list__hint text-danger'>{'Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'}</div>;
+ } else if (!emailVerificationEnabled) {
+ helpText = 'Email is used for notifications.';
+ } else if (this.state.emailChangeInProgress) {
+ const newEmail = UserStore.getCurrentUser().email;
+ if (newEmail) {
+ helpText = 'A verification email was sent to ' + newEmail + '.';
+ }
}
inputs.push(
@@ -453,6 +480,22 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
</div>
</div>
+ </div>
+ );
+
+ inputs.push(
+ <div key='confirmEmailSetting'>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{'Confirm Email'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateConfirmEmail}
+ value={this.state.confirmEmail}
+ />
+ </div>
+ </div>
{helpText}
</div>
);
@@ -471,10 +514,22 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
} else {
+ let describe = '';
+ if (this.state.emailChangeInProgress) {
+ const newEmail = UserStore.getCurrentUser().email;
+ if (newEmail) {
+ describe = 'New Address: ' + newEmail + '\nCheck your email to verify the above address.';
+ } else {
+ describe = 'Check your email to verify your new address';
+ }
+ } else {
+ describe = UserStore.getCurrentUser().email;
+ }
+
emailSection = (
<SettingItemMin
title='Email'
- describe={UserStore.getCurrentUser().email}
+ describe={describe}
updateSection={function updateEmailSection() {
this.updateSection('email');
}.bind(this)}
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index b59c08af0..4ff4775a7 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -251,17 +251,6 @@ export default class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-dark'/>
- <ul
- className='section-min'
- >
- <li className='col-sm-10 section-title'>{'Version ' + global.window.config.Version}</li>
- <li className='col-sm-7 section-describe'>
- <div className='text-nowrap'>{'Build Number: ' + global.window.config.BuildNumber}</div>
- <div className='text-nowrap'>{'Build Date: ' + global.window.config.BuildDate}</div>
- <div className='text-nowrap'>{'Build Hash: ' + global.window.config.BuildHash}</div>
- </li>
- </ul>
- <div className='divider-dark'/>
<br></br>
<a
data-toggle='modal'
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 9369cc097..8debb0b4e 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -132,6 +132,7 @@
.section-describe {
@include opacity(0.7);
+ white-space:pre;
}
.divider-dark {
diff --git a/web/static/images/Battlehouse-logodark.png b/web/static/images/Battlehouse-logodark.png
deleted file mode 100644
index 1fc5b68ca..000000000
--- a/web/static/images/Battlehouse-logodark.png
+++ /dev/null
Binary files differ
diff --git a/web/static/images/Mattermost-logodark.png b/web/static/images/Mattermost-logodark.png
deleted file mode 100644
index c16978ba8..000000000
--- a/web/static/images/Mattermost-logodark.png
+++ /dev/null
Binary files differ
diff --git a/web/static/images/Bladekick-logodark.png b/web/static/images/logo-email.png
index c16978ba8..c16978ba8 100644
--- a/web/static/images/Bladekick-logodark.png
+++ b/web/static/images/logo-email.png
Binary files differ
diff --git a/web/web.go b/web/web.go
index b87636187..87c96659d 100644
--- a/web/web.go
+++ b/web/web.go
@@ -414,7 +414,12 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
} else {
user := result.Data.(*model.User)
- api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+
+ if user.LastActivityAt > 0 {
+ api.FireAndForgetEmailChangeVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ } else {
+ api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ }
newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
http.Redirect(w, r, newAddress, http.StatusFound)