diff --git a/Makefile b/Makefile
index 9c4e6ee1fa..eccdf39ba3 100644
--- a/Makefile
+++ b/Makefile
@@ -151,7 +151,7 @@ package:
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
-
+
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
@@ -283,7 +283,7 @@ run: start-docker .prepare-go .prepare-jsx
$(GO) run $(GOFLAGS) mattermost.go -config=config.json &
@echo Starting compass watch
- cd web/sass-files && compass watch &
+ cd web/sass-files && compass compile && compass watch &
stop:
@for PID in $$(ps -ef | grep [c]ompass | awk '{ print $$2 }'); do \
@@ -296,7 +296,7 @@ stop:
kill $$PID; \
done
- @for PID in $$(ps -ef | grep [m]atterm | awk '{ print $$2 }'); do \
+ @for PID in $$(ps -ef | grep [m]atterm | grep -v VirtualBox | awk '{ print $$2 }'); do \
echo stopping go web $$PID; \
kill $$PID; \
done
diff --git a/api/admin.go b/api/admin.go
index 0ea6341e28..e8cb8b3c7f 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
@@ -21,6 +21,7 @@ func InitAdmin(r *mux.Router) {
sr := r.PathPrefix("/admin").Subrouter()
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
+ sr.Handle("/audits", ApiUserRequired(getAllAudits)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
@@ -58,6 +59,32 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.ArrayToJson(lines)))
}
+func getAllAudits(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ if !c.HasSystemAdminPermissions("getAllAudits") {
+ return
+ }
+
+ if result := <-Srv.Store.Audit().Get("", 200); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ audits := result.Data.(model.Audits)
+ etag := audits.Etag()
+
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ if len(etag) > 0 {
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ }
+
+ w.Write([]byte(audits.ToJson()))
+ return
+ }
+}
+
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientCfg)))
}
@@ -161,9 +188,10 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
rows[2] = &model.AnalyticsRow{"post_count", 0}
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
+
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
- postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
+ postChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
if r := <-openChan; r.Err != nil {
@@ -209,6 +237,47 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
}
+ } else if name == "extra_counts" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
+ rows[0] = &model.AnalyticsRow{"file_post_count", 0}
+ rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
+ rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
+ rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
+
+ fileChan := Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
+ hashtagChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
+ iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
+ oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
+
+ if r := <-fileChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-hashtagChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-iHookChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-oHookChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+
+ w.Write([]byte(rows.ToJson()))
} else {
c.SetInvalidParam("getAnalytics", "name")
}
diff --git a/api/admin_test.go b/api/admin_test.go
index 2552e642c8..8a9c82b443 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -41,6 +41,36 @@ func TestGetLogs(t *testing.T) {
}
}
+func TestGetAllAudits(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetAllAudits(); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if audits, err := Client.GetAllAudits(); err != nil {
+ t.Fatal(err)
+ } else if len(audits.Data.(model.Audits)) <= 0 {
+ t.Fatal()
+ }
+}
+
func TestGetClientProperties(t *testing.T) {
Setup()
@@ -362,3 +392,113 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
}
}
}
+
+func TestGetTeamAnalyticsExtra(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "#test a" + model.NewId() + "a"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ if _, err := Client.GetTeamAnalytics("", "extra_counts"); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if result, err := Client.GetTeamAnalytics(team.Id, "extra_counts"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Name != "file_post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[0].Value != 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Name != "hashtag_post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Name != "incoming_webhook_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Value != 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Name != "outgoing_webhook_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Value != 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+
+ if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Name != "file_post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Name != "hashtag_post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Value < 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Name != "incoming_webhook_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Name != "outgoing_webhook_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+}
diff --git a/api/user.go b/api/user.go
index 27afbd4692..a6817caa29 100644
--- a/api/user.go
+++ b/api/user.go
@@ -444,6 +444,38 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
return nil
}
+func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User {
+ var team *model.Team
+
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ if result := <-Srv.Store.User().GetByUsername(team.Id, username); result.Err != nil {
+ c.Err = result.Err
+ c.Err.StatusCode = http.StatusForbidden
+ return nil
+ } else {
+ user := result.Data.(*model.User)
+
+ if len(user.AuthData) != 0 {
+ c.Err = model.NewLocAppError("LoginByUsername", "api.user.login_by_email.sign_in.app_error",
+ map[string]interface{}{"AuthService": user.AuthService}, "")
+ return nil
+ }
+
+ if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
+ Login(c, w, r, user, deviceId)
+ return user
+ }
+ }
+
+ return nil
+}
+
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
authData := ""
provider := einterfaces.GetOauthProvider(service)
@@ -629,6 +661,8 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
+ } else if len(props["username"]) != 0 && len(props["name"]) != 0 {
+ user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"])
} else {
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
diff --git a/api/user_test.go b/api/user_test.go
index b2ae113f15..1a1cf9634a 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -99,7 +99,7 @@ func TestLogin(t *testing.T) {
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
- user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey", Password: "pwd"}
ruser, _ := Client.CreateUser(&user, "")
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
@@ -107,7 +107,7 @@ func TestLogin(t *testing.T) {
t.Fatal(err)
} else {
if result.Data.(*model.User).Email != user.Email {
- t.Fatal("email's didn't match")
+ t.Fatal("emails didn't match")
}
}
@@ -119,14 +119,30 @@ func TestLogin(t *testing.T) {
}
}
+ if result, err := Client.LoginByUsername(team.Name, user.Username, user.Password); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.User).Email != user.Email {
+ t.Fatal("emails didn't match")
+ }
+ }
+
if _, err := Client.LoginByEmail(team.Name, user.Email, user.Password+"invalid"); err == nil {
t.Fatal("Invalid Password")
}
+ if _, err := Client.LoginByUsername(team.Name, user.Username, user.Password+"invalid"); err == nil {
+ t.Fatal("Invalid Password")
+ }
+
if _, err := Client.LoginByEmail(team.Name, "", user.Password); err == nil {
t.Fatal("should have failed")
}
+ if _, err := Client.LoginByUsername(team.Name, "", user.Password); err == nil {
+ t.Fatal("should have failed")
+ }
+
authToken := Client.AuthToken
Client.AuthToken = "invalid"
diff --git a/config/config.json b/config/config.json
index 1415603668..5ed05fecde 100644
--- a/config/config.json
+++ b/config/config.json
@@ -68,6 +68,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/doc/developer/tests/test-attachments.md b/doc/developer/tests/test-attachments.md
index e2fda0eb60..75a2285f8c 100644
--- a/doc/developer/tests/test-attachments.md
+++ b/doc/developer/tests/test-attachments.md
@@ -7,8 +7,9 @@ This test contains instructions for the core team to manually test common attach
**Notes:**
- All file types should upload and post.
-- Read the expected for details on the behavior of the thumbnail and preview window.
+- Read the expected for details on the behavior of the thumbnail and preview window.
- The expected behavior of video and audio formats depends on the operating system, browser and plugins. View the permalinks to the Public Test Channel on Pre-Release Core to see the expected cases.
+- If the browser can play the media file, media player controls should appear. If the browser cannot play the file, it should show appear as a regular attachment without the media controls.
### Images
@@ -72,7 +73,7 @@ Expected: Generic Word thumbnail & preview window.
**MP4**
`Videos/MP4.mp4`
-Expected: Generic video thumbnail & playable preview window. View Permalink.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/5dx5qx9t9brqfnhohccxjynx7c)
**AVI**
@@ -114,7 +115,7 @@ Expected: Generic audio thumbnail & playable preview window
**M4A**
`Audio/M4a.m4a`
-Expected: Generic audio thumbnail & playable preview window
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/6c7qsw48ybd88bktgeykodsrrc)
**AAC**
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index cbd771b719..e831bbb3a4 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -68,6 +68,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index cbd771b719..e831bbb3a4 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -68,6 +68,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/i18n/en.json b/i18n/en.json
index 8a3e993d25..d72d6dca57 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1667,6 +1667,38 @@
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
"translation": "Inappropriate permissions to regenerate outcoming webhook token"
},
+ {
+ "id": "ent.ldap.do_login.bind_admin_user.app_error",
+ "translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword."
+ },
+ {
+ "id": "ent.ldap.do_login.invalid_password.app_error",
+ "translation": "Invalid Password"
+ },
+ {
+ "id": "ent.ldap.do_login.licence_disable.app_error",
+ "translation": "LDAP functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license."
+ },
+ {
+ "id": "ent.ldap.do_login.matched_to_many_users.app_error",
+ "translation": "Username given matches multiple users"
+ },
+ {
+ "id": "ent.ldap.do_login.search_ldap_server.app_error",
+ "translation": "Failed to search LDAP server"
+ },
+ {
+ "id": "ent.ldap.do_login.unable_to_connect.app_error",
+ "translation": "Unable to connect to LDAP server"
+ },
+ {
+ "id": "ent.ldap.do_login.unable_to_create_user.app_error",
+ "translation": "Credentials valid but unable to create user."
+ },
+ {
+ "id": "ent.ldap.do_login.user_not_registered.app_error",
+ "translation": "User not registered on LDAP server"
+ },
{
"id": "manaultesting.get_channel_id.no_found.debug",
"translation": "Could not find channel: %v, %v possibilites searched"
@@ -3131,6 +3163,14 @@
"id": "store.sql_webhooks.update_outgoing.app_error",
"translation": "We couldn't update the webhook"
},
+ {
+ "id": "store.sql_webhooks.analytics_incoming_count.app_error",
+ "translation": "We couldn't count the incoming webhooks"
+ },
+ {
+ "id": "store.sql_webhooks.analytics_outgoing_count.app_error",
+ "translation": "We couldn't count the outgoing webhooks"
+ },
{
"id": "utils.config.load_config.decoding.panic",
"translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
@@ -3491,4 +3531,4 @@
"id": "web.watcher_fail.error",
"translation": "Failed to add directory to watcher %v"
}
-]
+]
\ No newline at end of file
diff --git a/i18n/es.json b/i18n/es.json
index a79048652c..2c51581851 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -1531,6 +1531,38 @@
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
"translation": "Permisos inapropiados para regenerar un token para el Webhook saliente"
},
+ {
+ "id": "ent.ldap.do_login.bind_admin_user.app_error",
+ "translation": "No se pudo enlazar con el servidor LDAP. Revisa las opciones de BindUsername y BindPassword."
+ },
+ {
+ "id": "ent.ldap.do_login.invalid_password.app_error",
+ "translation": "Contraseña inválida"
+ },
+ {
+ "id": "ent.ldap.do_login.licence_disable.app_error",
+ "translation": "Las funcionalidades de LDAP están deshabilitadas con la licencia actual. Por favor contacta a un administrador del sistema acerca de mejorar la licencia enterprise."
+ },
+ {
+ "id": "ent.ldap.do_login.matched_to_many_users.app_error",
+ "translation": "Nombre de usuario dado coincide con varios usuarios"
+ },
+ {
+ "id": "ent.ldap.do_login.search_ldap_server.app_error",
+ "translation": "Falla al buscar en el servidor LDAP"
+ },
+ {
+ "id": "ent.ldap.do_login.unable_to_connect.app_error",
+ "translation": "No se pudo conectar con el servidor LDAP"
+ },
+ {
+ "id": "ent.ldap.do_login.unable_to_create_user.app_error",
+ "translation": "Credenciales válidas pero no se pudo crear el usuario."
+ },
+ {
+ "id": "ent.ldap.do_login.user_not_registered.app_error",
+ "translation": "Usuario no registrado en el servidor LDAP"
+ },
{
"id": "manaultesting.get_channel_id.no_found.debug",
"translation": "No pudimos encontrar el canal: %v, búsqueda realizada con estas posibilidades %v"
@@ -3287,4 +3319,4 @@
"id": "web.watcher_fail.error",
"translation": "Falla al agregar el directorio a ser vigilado %v"
}
-]
+]
\ No newline at end of file
diff --git a/mattermost.go b/mattermost.go
index b6652d812f..43fa06601c 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -31,6 +31,8 @@ import (
_ "github.com/go-ldap/ldap"
)
+//ENTERPRISE_IMPORTS
+
var flagCmdCreateTeam bool
var flagCmdCreateUser bool
var flagCmdAssignRole bool
diff --git a/model/client.go b/model/client.go
index a271e61623..560e47b76c 100644
--- a/model/client.go
+++ b/model/client.go
@@ -280,6 +280,14 @@ func (c *Client) LoginByEmail(name string, email string, password string) (*Resu
return c.login(m)
}
+func (c *Client) LoginByUsername(name string, username string, password string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["name"] = name
+ m["username"] = username
+ m["password"] = password
+ return c.login(m)
+}
+
func (c *Client) LoginByEmailWithDevice(name string, email string, password string, deviceId string) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@@ -443,6 +451,15 @@ func (c *Client) GetLogs() (*Result, *AppError) {
}
}
+func (c *Client) GetAllAudits() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/audits", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) GetClientProperties() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil {
return nil, err
diff --git a/model/config.go b/model/config.go
index f518e8f8d6..acb525abf7 100644
--- a/model/config.go
+++ b/model/config.go
@@ -99,6 +99,8 @@ type FileSettings struct {
type EmailSettings struct {
EnableSignUpWithEmail bool
+ EnableSignInWithEmail *bool
+ EnableSignInWithUsername *bool
SendEmailNotifications bool
RequireEmailVerification bool
FeedbackName string
@@ -260,6 +262,21 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.EnableTeamListing = false
}
+ if o.EmailSettings.EnableSignInWithEmail == nil {
+ o.EmailSettings.EnableSignInWithEmail = new(bool)
+
+ if o.EmailSettings.EnableSignUpWithEmail == true {
+ *o.EmailSettings.EnableSignInWithEmail = true
+ } else {
+ *o.EmailSettings.EnableSignInWithEmail = false
+ }
+ }
+
+ if o.EmailSettings.EnableSignInWithUsername == nil {
+ o.EmailSettings.EnableSignInWithUsername = new(bool)
+ *o.EmailSettings.EnableSignInWithUsername = false
+ }
+
if o.EmailSettings.SendPushNotifications == nil {
o.EmailSettings.SendPushNotifications = new(bool)
*o.EmailSettings.SendPushNotifications = false
diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go
index 97df5f7e71..dbcb9a616c 100644
--- a/store/sql_audit_store.go
+++ b/store/sql_audit_store.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package store
@@ -72,9 +72,16 @@ func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel {
return
}
+ query := "SELECT * FROM Audits"
+
+ if len(user_id) != 0 {
+ query += " WHERE UserId = :user_id"
+ }
+
+ query += " ORDER BY CreateAt DESC LIMIT :limit"
+
var audits model.Audits
- if _, err := s.GetReplica().Select(&audits, "SELECT * FROM Audits WHERE UserId = :user_id ORDER BY CreateAt DESC LIMIT :limit",
- map[string]interface{}{"user_id": user_id, "limit": limit}); err != nil {
+ if _, err := s.GetReplica().Select(&audits, query, map[string]interface{}{"user_id": user_id, "limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlAuditStore.Get", "store.sql_audit.get.finding.app_error", nil, "user_id="+user_id)
} else {
result.Data = audits
diff --git a/store/sql_audit_store_test.go b/store/sql_audit_store_test.go
index b395631f1e..841b79627a 100644
--- a/store/sql_audit_store_test.go
+++ b/store/sql_audit_store_test.go
@@ -45,6 +45,14 @@ func TestSqlAuditStore(t *testing.T) {
t.Fatal("Should have returned empty because user_id is missing")
}
+ c = store.Audit().Get("", 100)
+ result = <-c
+ audits = result.Data.(model.Audits)
+
+ if len(audits) <= 4 {
+ t.Fatal("Failed to save and retrieve 4 audit logs")
+ }
+
if r2 := <-store.Audit().PermanentDeleteByUser(audit.UserId); r2.Err != nil {
t.Fatal(r2.Err)
}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index aeaa5922c6..dfb9563eb3 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -629,7 +629,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
- termMap[term] = true
+ termMap[strings.ToUpper(term)] = true
}
}
@@ -748,7 +748,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
if searchType == "Hashtags" {
exactMatch := false
for _, tag := range strings.Split(p.Hashtags, " ") {
- if termMap[tag] {
+ if termMap[strings.ToUpper(tag)] {
exactMatch = true
}
}
@@ -940,7 +940,7 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
return storeChannel
}
-func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
+func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -959,8 +959,15 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
query += " AND Channels.TeamId = :TeamId"
}
- v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
- if err != nil {
+ if mustHaveFile {
+ query += " AND Posts.Filenames != '[]'"
+ }
+
+ if mustHaveHashtag {
+ query += " AND Posts.Hashtags != ''"
+ }
+
+ if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
result.Err = model.NewLocAppError("SqlPostStore.AnalyticsPostCount", "store.sql_post.analytics_posts_count.app_error", nil, err.Error())
} else {
result.Data = v
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 46b8d7678c..d69f7906c6 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -887,7 +887,7 @@ func TestPostCountsByDay(t *testing.T) {
}
}
- if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
+ if r1 := <-store.Post().AnalyticsPostCount(t1.Id, false, false); r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.(int64) != 4 {
diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go
index 939574b9f5..740c9a33f0 100644
--- a/store/sql_webhook_store.go
+++ b/store/sql_webhook_store.go
@@ -329,3 +329,65 @@ func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) StoreChanne
return storeChannel
}
+
+func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ query :=
+ `SELECT
+ COUNT(*)
+ FROM
+ IncomingWebhooks
+ WHERE
+ DeleteAt = 0`
+
+ if len(teamId) > 0 {
+ query += " AND TeamId = :TeamId"
+ }
+
+ if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsIncomingCount", "store.sql_webhooks.analytics_incoming_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) AnalyticsOutgoingCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ query :=
+ `SELECT
+ COUNT(*)
+ FROM
+ OutgoingWebhooks
+ WHERE
+ DeleteAt = 0`
+
+ if len(teamId) > 0 {
+ query += " AND TeamId = :TeamId"
+ }
+
+ if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsOutgoingCount", "store.sql_webhooks.analytics_outgoing_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go
index 5b43d07307..251ecf5971 100644
--- a/store/sql_webhook_store_test.go
+++ b/store/sql_webhook_store_test.go
@@ -304,3 +304,42 @@ func TestWebhookStoreUpdateOutgoing(t *testing.T) {
t.Fatal(r2.Err)
}
}
+
+func TestWebhookStoreCountIncoming(t *testing.T) {
+ Setup()
+
+ o1 := &model.IncomingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.TeamId = model.NewId()
+
+ o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
+
+ if r := <-store.Webhook().AnalyticsIncomingCount(""); r.Err != nil {
+ t.Fatal(r.Err)
+ } else {
+ if r.Data.(int64) == 0 {
+ t.Fatal("should have at least 1 incoming hook")
+ }
+ }
+}
+
+func TestWebhookStoreCountOutgoing(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r := <-store.Webhook().AnalyticsOutgoingCount(""); r.Err != nil {
+ t.Fatal(r.Err)
+ } else {
+ if r.Data.(int64) == 0 {
+ t.Fatal("should have at least 1 outgoing hook")
+ }
+ }
+}
diff --git a/store/store.go b/store/store.go
index a25d8b204a..95df368edb 100644
--- a/store/store.go
+++ b/store/store.go
@@ -101,7 +101,7 @@ type PostStore interface {
GetForExport(channelId string) StoreChannel
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
AnalyticsPostCountsByDay(teamId string) StoreChannel
- AnalyticsPostCount(teamId string) StoreChannel
+ AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel
}
type UserStore interface {
@@ -182,6 +182,8 @@ type WebhookStore interface {
DeleteOutgoing(webhookId string, time int64) StoreChannel
PermanentDeleteOutgoingByUser(userId string) StoreChannel
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
+ AnalyticsIncomingCount(teamId string) StoreChannel
+ AnalyticsOutgoingCount(teamId string) StoreChannel
}
type CommandStore interface {
diff --git a/utils/config.go b/utils/config.go
index a2d341cd2a..3e4ba5c5bd 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -210,6 +210,8 @@ func getClientConfig(c *model.Config) map[string]string {
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
+ props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
+ props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 6319b56813..98b1d7cc12 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -1,204 +1,24 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
-import UserStore from '../stores/user_store.jsx';
-import ChannelStore from '../stores/channel_store.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
import LoadingScreen from './loading_screen.jsx';
+import AuditTable from './audit_table.jsx';
+
+import UserStore from '../stores/user_store.jsx';
+
+import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
-
-const holders = defineMessages({
- sessionRevoked: {
- id: 'access_history.sessionRevoked',
- defaultMessage: 'The session with id {sessionId} was revoked'
- },
- channelCreated: {
- id: 'access_history.channelCreated',
- defaultMessage: 'Created the {channelName} channel/group'
- },
- establishedDM: {
- id: 'access_history.establishedDM',
- defaultMessage: 'Established a direct message channel with {username}'
- },
- nameUpdated: {
- id: 'access_history.nameUpdated',
- defaultMessage: 'Updated the {channelName} channel/group name'
- },
- headerUpdated: {
- id: 'access_history.headerUpdated',
- defaultMessage: 'Updated the {channelName} channel/group header'
- },
- channelDeleted: {
- id: 'access_history.channelDeleted',
- defaultMessage: 'Deleted the channel/group with the URL {url}'
- },
- userAdded: {
- id: 'access_history.userAdded',
- defaultMessage: 'Added {username} to the {channelName} channel/group'
- },
- userRemoved: {
- id: 'access_history.userRemoved',
- defaultMessage: 'Removed {username} to the {channelName} channel/group'
- },
- attemptedRegisterApp: {
- id: 'access_history.attemptedRegisterApp',
- defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
- },
- attemptedAllowOAuthAccess: {
- id: 'access_history.attemptedAllowOAuthAccess',
- defaultMessage: 'Attempted to allow a new OAuth service access'
- },
- successfullOAuthAccess: {
- id: 'access_history.successfullOAuthAccess',
- defaultMessage: 'Successfully gave a new OAuth service access'
- },
- failedOAuthAccess: {
- id: 'access_history.failedOAuthAccess',
- defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
- },
- attemptedOAuthToken: {
- id: 'access_history.attemptedOAuthToken',
- defaultMessage: 'Attempted to get an OAuth access token'
- },
- successfullOAuthToken: {
- id: 'access_history.successfullOAuthToken',
- defaultMessage: 'Successfully added a new OAuth service'
- },
- oauthTokenFailed: {
- id: 'access_history.oauthTokenFailed',
- defaultMessage: 'Failed to get an OAuth access token - {token}'
- },
- attemptedLogin: {
- id: 'access_history.attemptedLogin',
- defaultMessage: 'Attempted to login'
- },
- successfullLogin: {
- id: 'access_history.successfullLogin',
- defaultMessage: 'Successfully logged in'
- },
- failedLogin: {
- id: 'access_history.failedLogin',
- defaultMessage: 'FAILED login attempt'
- },
- updatePicture: {
- id: 'access_history.updatePicture',
- defaultMessage: 'Updated your profile picture'
- },
- updateGeneral: {
- id: 'access_history.updateGeneral',
- defaultMessage: 'Updated the general settings of your account'
- },
- attemptedPassword: {
- id: 'access_history.attemptedPassword',
- defaultMessage: 'Attempted to change password'
- },
- successfullPassword: {
- id: 'access_history.successfullPassword',
- defaultMessage: 'Successfully changed password'
- },
- failedPassword: {
- id: 'access_history.failedPassword',
- defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
- },
- updatedRol: {
- id: 'access_history.updatedRol',
- defaultMessage: 'Updated user role(s) to '
- },
- member: {
- id: 'access_history.member',
- defaultMessage: 'member'
- },
- accountActive: {
- id: 'access_history.accountActive',
- defaultMessage: 'Account made active'
- },
- accountInactive: {
- id: 'access_history.accountInactive',
- defaultMessage: 'Account made inactive'
- },
- by: {
- id: 'access_history.by',
- defaultMessage: ' by {username}'
- },
- byAdmin: {
- id: 'access_history.byAdmin',
- defaultMessage: ' by an admin'
- },
- sentEmail: {
- id: 'access_history.sentEmail',
- defaultMessage: 'Sent an email to {email} to reset your password'
- },
- attemptedReset: {
- id: 'access_history.attemptedReset',
- defaultMessage: 'Attempted to reset password'
- },
- successfullReset: {
- id: 'access_history.successfullReset',
- defaultMessage: 'Successfully reset password'
- },
- updateGlobalNotifications: {
- id: 'access_history.updateGlobalNotifications',
- defaultMessage: 'Updated your global notification settings'
- },
- attemptedWebhookCreate: {
- id: 'access_history.attemptedWebhookCreate',
- defaultMessage: 'Attempted to create a webhook'
- },
- succcessfullWebhookCreate: {
- id: 'access_history.successfullWebhookCreate',
- defaultMessage: 'Successfully created a webhook'
- },
- failedWebhookCreate: {
- id: 'access_history.failedWebhookCreate',
- defaultMessage: 'Failed to create a webhook - bad channel permissions'
- },
- attemptedWebhookDelete: {
- id: 'access_history.attemptedWebhookDelete',
- defaultMessage: 'Attempted to delete a webhook'
- },
- successfullWebhookDelete: {
- id: 'access_history.successfullWebhookDelete',
- defaultMessage: 'Successfully deleted a webhook'
- },
- failedWebhookDelete: {
- id: 'access_history.failedWebhookDelete',
- defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
- },
- logout: {
- id: 'access_history.logout',
- defaultMessage: 'Logged out of your account'
- },
- verified: {
- id: 'access_history.verified',
- defaultMessage: 'Sucessfully verified your email address'
- },
- revokedAll: {
- id: 'access_history.revokedAll',
- defaultMessage: 'Revoked all current sessions for the team'
- },
- loginAttempt: {
- id: 'access_history.loginAttempt',
- defaultMessage: ' (Login attempt)'
- },
- loginFailure: {
- id: 'access_history.loginFailure',
- defaultMessage: ' (Login failure)'
- }
-});
+import {intlShape, injectIntl, FormattedMessage} from 'mm-intl';
class AccessHistoryModal extends React.Component {
constructor(props) {
super(props);
this.onAuditChange = this.onAuditChange.bind(this);
- this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
- this.formatAuditInfo = this.formatAuditInfo.bind(this);
- this.handleRevokedSession = this.handleRevokedSession.bind(this);
const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
@@ -245,359 +65,17 @@ class AccessHistoryModal extends React.Component {
this.setState(newState);
}
}
- handleMoreInfo(index) {
- var newMoreInfo = this.state.moreInfo;
- newMoreInfo[index] = true;
- this.setState({moreInfo: newMoreInfo});
- }
- handleRevokedSession(sessionId) {
- return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
- }
- formatAuditInfo(currentAudit) {
- const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
-
- const {formatMessage} = this.props.intl;
- let currentAuditDesc = '';
-
- if (currentActionURL.indexOf('/channels') === 0) {
- const channelInfo = currentAudit.extra_info.split(' ');
- const channelNameField = channelInfo[0].split('=');
-
- let channelURL = '';
- let channelObj;
- let channelName = '';
- if (channelNameField.indexOf('name') >= 0) {
- channelURL = channelNameField[channelNameField.indexOf('name') + 1];
- channelObj = ChannelStore.getByName(channelURL);
- if (channelObj) {
- channelName = channelObj.display_name;
- } else {
- channelName = channelURL;
- }
- }
-
- switch (currentActionURL) {
- case '/channels/create':
- currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
- break;
- case '/channels/create_direct':
- currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
- break;
- case '/channels/update':
- currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
- break;
- case '/channels/update_desc': // support the old path
- case '/channels/update_header':
- currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
- break;
- default: {
- let userIdField = [];
- let userId = '';
- let username = '';
-
- if (channelInfo[1]) {
- userIdField = channelInfo[1].split('=');
-
- if (userIdField.indexOf('user_id') >= 0) {
- userId = userIdField[userIdField.indexOf('user_id') + 1];
- username = UserStore.getProfile(userId).username;
- }
- }
-
- if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
- } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
- } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
- }
-
- break;
- }
- }
- } else if (currentActionURL.indexOf('/oauth') === 0) {
- const oauthInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/oauth/register': {
- const clientIdField = oauthInfo[0].split('=');
-
- if (clientIdField[0] === 'client_id') {
- currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
- }
-
- break;
- }
- case '/oauth/allow':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
- } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
- currentAuditDesc = formatMessage(holders.failedOAuthAccess);
- }
-
- break;
- case '/oauth/access_token':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthToken);
- } else {
- const oauthTokenFailure = oauthInfo[0].split('-');
-
- if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
- currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
- }
- }
-
- break;
- default:
- break;
- }
- } else if (currentActionURL.indexOf('/users') === 0) {
- const userInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/users/login':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedLogin);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullLogin);
- } else if (userInfo[0]) {
- currentAuditDesc = formatMessage(holders.failedLogin);
- }
-
- break;
- case '/users/revoke_session':
- currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
- break;
- case '/users/newimage':
- currentAuditDesc = formatMessage(holders.updatePicture);
- break;
- case '/users/update':
- currentAuditDesc = formatMessage(holders.updateGeneral);
- break;
- case '/users/newpassword':
- if (userInfo[0] === 'attempted') {
- currentAuditDesc = formatMessage(holders.attemptedPassword);
- } else if (userInfo[0] === 'completed') {
- currentAuditDesc = formatMessage(holders.successfullPassword);
- } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
- currentAuditDesc = formatMessage(holders.failedPassword);
- }
-
- break;
- case '/users/update_roles': {
- const userRoles = userInfo[0].split('=')[1];
-
- currentAuditDesc = formatMessage(holders.updatedRol);
- if (userRoles.trim()) {
- currentAuditDesc += userRoles;
- } else {
- currentAuditDesc += formatMessage(holders.member);
- }
-
- break;
- }
- case '/users/update_active': {
- const updateType = userInfo[0].split('=')[0];
- const updateField = userInfo[0].split('=')[1];
-
- /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
- if (updateType === 'active') {
- if (updateField === 'true') {
- currentAuditDesc = formatMessage(holders.accountActive);
- } else if (updateField === 'false') {
- currentAuditDesc = formatMessage(holders.accountInactive);
- }
-
- const actingUserInfo = userInfo[1].split('=');
- if (actingUserInfo[0] === 'session_user') {
- const actingUser = UserStore.getProfile(actingUserInfo[1]);
- const currentUser = UserStore.getCurrentUser();
- if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
- currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
- } else if (currentUser && actingUser) {
- currentAuditDesc += formatMessage(holders.byAdmin);
- }
- }
- } else if (updateType === 'session_id') {
- currentAuditDesc = this.handleRevokedSession(updateField);
- }
-
- break;
- }
- case '/users/send_password_reset':
- currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
- break;
- case '/users/reset_password':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedReset);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullReset);
- }
-
- break;
- case '/users/update_notify':
- currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
- break;
- default:
- break;
- }
- } else if (currentActionURL.indexOf('/hooks') === 0) {
- const webhookInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/hooks/incoming/create':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
- } else if (webhookInfo[0] === 'fail - bad channel permissions') {
- currentAuditDesc = formatMessage(holders.failedWebhookCreate);
- }
-
- break;
- case '/hooks/incoming/delete':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
- } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
- currentAuditDesc = formatMessage(holders.failedWebhookDelete);
- }
-
- break;
- default:
- break;
- }
- } else {
- switch (currentActionURL) {
- case '/logout':
- currentAuditDesc = formatMessage(holders.logout);
- break;
- case '/verify_email':
- currentAuditDesc = formatMessage(holders.verified);
- break;
- default:
- break;
- }
- }
-
- /* If all else fails... */
- if (!currentAuditDesc) {
- /* Currently not called anywhere */
- if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
- currentAuditDesc = formatMessage(holders.revokedAll);
- } else {
- let currentActionDesc = '';
- if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
- currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
- currentActionDesc = Utils.toTitleCase(currentActionDesc);
- }
-
- let currentExtraInfoDesc = '';
- if (currentAudit.extra_info) {
- currentExtraInfoDesc = currentAudit.extra_info;
-
- if (currentExtraInfoDesc.indexOf('=') !== -1) {
- currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
- }
- }
- currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
- }
- }
-
- const currentDate = new Date(currentAudit.create_at);
- const currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' +
- currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
- return currentAuditInfo;
- }
render() {
- var accessList = [];
-
- const {formatMessage} = this.props.intl;
- for (var i = 0; i < this.state.audits.length; i++) {
- const currentAudit = this.state.audits[i];
- const currentAuditInfo = this.formatAuditInfo(currentAudit);
-
- var moreInfo = (
-
-
-
- );
-
- if (this.state.moreInfo[i]) {
- if (!currentAudit.session_id) {
- currentAudit.session_id = 'N/A';
-
- if (currentAudit.action.search('/users/login') >= 0) {
- if (currentAudit.extra_info === 'attempt') {
- currentAudit.session_id += formatMessage(holders.loginAttempt);
- } else {
- currentAudit.session_id += formatMessage(holders.loginFailure);
- }
- }
- }
-
- moreInfo = (
-
- );
- }
-
- var divider = null;
- if (i < this.state.audits.length - 1) {
- divider = ();
- }
-
- accessList[i] = (
-
-
-
{currentAuditInfo}
-
- {moreInfo}
-
- {divider}
-
-
- );
- }
-
var content;
if (this.state.audits.loading) {
content = ();
} else {
- content = ();
+ content = (
+
+ );
}
return (
@@ -628,4 +106,4 @@ AccessHistoryModal.propTypes = {
onHide: React.PropTypes.func.isRequired
};
-export default injectIntl(AccessHistoryModal);
\ No newline at end of file
+export default injectIntl(AccessHistoryModal);
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index efd1630170..360ae3ef31 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -11,6 +11,7 @@ import * as Utils from '../../utils/utils.jsx';
import EmailSettingsTab from './email_settings.jsx';
import LogSettingsTab from './log_settings.jsx';
import LogsTab from './logs.jsx';
+import AuditsTab from './audits.jsx';
import FileSettingsTab from './image_settings.jsx';
import PrivacySettingsTab from './privacy_settings.jsx';
import RateSettingsTab from './rate_settings.jsx';
@@ -138,6 +139,8 @@ export default class AdminController extends React.Component {
tab = ;
} else if (this.state.selected === 'logs') {
tab = ;
+ } else if (this.state.selected === 'audits') {
+ tab = ;
} else if (this.state.selected === 'image_settings') {
tab = ;
} else if (this.state.selected === 'privacy_settings') {
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index d6bae1feb2..642bfe9d7e 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -214,6 +214,24 @@ export default class AdminSidebar extends React.Component {
);
}
+ let audits;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ audits = (
+
+
+
+
+
+ );
+ }
+
return (
@@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component {
/>
+ {audits}
diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx
index a22c26c346..0a159d2e38 100644
--- a/web/react/components/admin_console/analytics.jsx
+++ b/web/react/components/admin_console/analytics.jsx
@@ -4,11 +4,60 @@
import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
+import DoughnutChart from './doughnut_chart.jsx';
+import StatisticCount from './statistic_count.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
-import {FormattedMessage} from 'mm-intl';
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ analyticsTotalUsers: {
+ id: 'admin.analytics.totalUsers',
+ defaultMessage: 'Total Users'
+ },
+ analyticsPublicChannels: {
+ id: 'admin.analytics.publicChannels',
+ defaultMessage: 'Public Channels'
+ },
+ analyticsPrivateGroups: {
+ id: 'admin.analytics.privateGroups',
+ defaultMessage: 'Private Groups'
+ },
+ analyticsTotalPosts: {
+ id: 'admin.analytics.totalPosts',
+ defaultMessage: 'Total Posts'
+ },
+ analyticsFilePosts: {
+ id: 'admin.analytics.totalFilePosts',
+ defaultMessage: 'Posts with Files'
+ },
+ analyticsHashtagPosts: {
+ id: 'admin.analytics.totalHashtagPosts',
+ defaultMessage: 'Posts with Hashtags'
+ },
+ analyticsIncomingHooks: {
+ id: 'admin.analytics.totalIncomingWebhooks',
+ defaultMessage: 'Incoming Webhooks'
+ },
+ analyticsOutgoingHooks: {
+ id: 'admin.analytics.totalOutgoingWebhooks',
+ defaultMessage: 'Outgoing Webhooks'
+ },
+ analyticsChannelTypes: {
+ id: 'admin.analytics.channelTypes',
+ defaultMessage: 'Channel Types'
+ },
+ analyticsTextPosts: {
+ id: 'admin.analytics.textPosts',
+ defaultMessage: 'Posts with Text-only'
+ },
+ analyticsPostTypes: {
+ id: 'admin.analytics.postTypes',
+ defaultMessage: 'Posts, Files and Hashtags'
+ }
+});
export default class Analytics extends React.Component {
constructor(props) {
@@ -18,6 +67,8 @@ export default class Analytics extends React.Component {
}
render() { // in the future, break down these into smaller components
+ const {formatMessage} = this.props.intl;
+
var serverError = '';
if (this.props.serverError) {
serverError =
;
@@ -30,77 +81,129 @@ export default class Analytics extends React.Component {
/>
);
- var totalCount = (
-
-
-
-
-
-
{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}
+ let firstRow;
+ let extraGraphs;
+ if (this.props.showAdvanced) {
+ firstRow = (
+
+
+
+
+
-
- );
+ );
- var openChannelCount = (
-
-
-
-
-
-
{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}
+ const channelTypeData = [
+ {
+ value: this.props.channelOpenCount,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsPublicChannels)
+ },
+ {
+ value: this.props.channelPrivateCount,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsPrivateGroups)
+ }
+ ];
+
+ const postTypeData = [
+ {
+ value: this.props.filePostCount,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsFilePosts)
+ },
+ {
+ value: this.props.filePostCount,
+ color: '#F7464A',
+ highlight: '#FF5A5E',
+ label: formatMessage(holders.analyticsHashtagPosts)
+ },
+ {
+ value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsTextPosts)
+ }
+ ];
+
+ extraGraphs = (
+
+
+
-
- );
-
- var openPrivateCount = (
-
-
-
-
-
-
{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}
+ );
+ } else {
+ firstRow = (
+
+
+
+
+
-
- );
+ );
+ }
- var postCount = (
-
-
-
-
-
-
{this.props.postCount == null ? loading : this.props.postCount}
-
-
- );
-
- var postCountsByDay = (
-
-
-
-
+ let postCountsByDay;
+ if (this.props.postCountsDay == null) {
+ postCountsByDay = (
+
-
- );
-
- if (this.props.postCountsDay != null) {
+ );
+ } else {
let content;
if (this.props.postCountsDay.labels.length === 0) {
content = (
@@ -137,21 +240,22 @@ export default class Analytics extends React.Component {
);
}
- var usersWithPostsByDay = (
-
-
-
-
+ let usersWithPostsByDay;
+ if (this.props.userCountsWithPostsDay == null) {
+ usersWithPostsByDay = (
+
-
- );
-
- if (this.props.userCountsWithPostsDay != null) {
+ );
+ } else {
let content;
if (this.props.userCountsWithPostsDay.labels.length === 0) {
content = (
@@ -312,12 +416,8 @@ export default class Analytics extends React.Component {
/>
{serverError}
-
- {totalCount}
- {postCount}
- {openChannelCount}
- {openPrivateCount}
-
+ {firstRow}
+ {extraGraphs}
{postCountsByDay}
@@ -347,10 +447,16 @@ Analytics.defaultProps = {
};
Analytics.propTypes = {
+ intl: intlShape.isRequired,
title: React.PropTypes.string,
channelOpenCount: React.PropTypes.number,
channelPrivateCount: React.PropTypes.number,
postCount: React.PropTypes.number,
+ showAdvanced: React.PropTypes.bool,
+ filePostCount: React.PropTypes.number,
+ hashtagPostCount: React.PropTypes.number,
+ incomingWebhookCount: React.PropTypes.number,
+ outgoingWebhookCount: React.PropTypes.number,
postCountsDay: React.PropTypes.object,
userCountsWithPostsDay: React.PropTypes.object,
recentActiveUsers: React.PropTypes.array,
@@ -358,3 +464,5 @@ Analytics.propTypes = {
uniqueUserCount: React.PropTypes.number,
serverError: React.PropTypes.string
};
+
+export default injectIntl(Analytics);
diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx
new file mode 100644
index 0000000000..866539b3db
--- /dev/null
+++ b/web/react/components/admin_console/audits.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+import AuditTable from '../audit_table.jsx';
+
+import AdminStore from '../../stores/admin_store.jsx';
+
+import * as AsyncClient from '../../utils/async_client.jsx';
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class Audits extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onAuditListenerChange = this.onAuditListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ audits: AdminStore.getAudits()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addAuditChangeListener(this.onAuditListenerChange);
+ AsyncClient.getServerAudits();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeAuditChangeListener(this.onAuditListenerChange);
+ }
+
+ onAuditListenerChange() {
+ this.setState({
+ audits: AdminStore.getAudits()
+ });
+ }
+
+ reload() {
+ AdminStore.saveAudits(null);
+ this.setState({
+ audits: null
+ });
+
+ AsyncClient.getServerAudits();
+ }
+
+ render() {
+ var content = null;
+
+ if (global.window.mm_license.IsLicensed !== 'true') {
+ return
;
+ }
+
+ if (this.state.audits === null) {
+ content =
;
+ } else {
+ content = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {content}
+
+
+ );
+ }
+}
diff --git a/web/react/components/admin_console/doughnut_chart.jsx b/web/react/components/admin_console/doughnut_chart.jsx
new file mode 100644
index 0000000000..e2dc01528d
--- /dev/null
+++ b/web/react/components/admin_console/doughnut_chart.jsx
@@ -0,0 +1,77 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class DoughnutChart extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.initChart = this.initChart.bind(this);
+ this.chart = null;
+ }
+
+ componentDidMount() {
+ this.initChart(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.chart) {
+ this.chart.destroy();
+ this.initChart(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ initChart(props) {
+ var el = ReactDOM.findDOMNode(this.refs.canvas);
+ var ctx = el.getContext('2d');
+ this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap
+ }
+
+ render() {
+ let content;
+ if (this.props.data == null) {
+ content = (
+
+ );
+ } else {
+ content = (
+
+ );
+ }
+
+ return (
+
+
+
+ {this.props.title}
+
+
+ {content}
+
+
+
+ );
+ }
+}
+
+DoughnutChart.propTypes = {
+ title: React.PropTypes.string,
+ width: React.PropTypes.string,
+ height: React.PropTypes.string,
+ data: React.PropTypes.array,
+ options: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index ce3c8cd122..17f25a04c1 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -112,6 +112,8 @@ class EmailSettings extends React.Component {
buildConfig() {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
+ config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
@@ -317,6 +319,88 @@ class EmailSettings extends React.Component {
+
+
+
+
+ );
+
+ return (
+
+
+
+ {this.props.title}
+
+
+
{this.props.count == null ? loading : this.props.count}
+
+
+ );
+ }
+}
+
+StatisticCount.propTypes = {
+ title: React.PropTypes.string.isRequired,
+ icon: React.PropTypes.string.isRequired,
+ count: React.PropTypes.number
+};
diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx
index 2dd833fb23..f983db177f 100644
--- a/web/react/components/admin_console/system_analytics.jsx
+++ b/web/react/components/admin_console/system_analytics.jsx
@@ -140,6 +140,34 @@ class SystemAnalytics extends React.Component {
this.setState({serverError: err.message});
}
);
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ Client.getSystemAnalytics(
+ 'extra_counts',
+ (data) => {
+ for (var index in data) {
+ if (data[index].name === 'file_post_count') {
+ this.setState({file_post_count: data[index].value});
+ }
+
+ if (data[index].name === 'hashtag_post_count') {
+ this.setState({hashtag_post_count: data[index].value});
+ }
+
+ if (data[index].name === 'incoming_webhook_count') {
+ this.setState({incoming_webhook_count: data[index].value});
+ }
+
+ if (data[index].name === 'outgoing_webhook_count') {
+ this.setState({outgoing_webhook_count: data[index].value});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
}
componentWillReceiveProps() {
@@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component {
return (
= 0) {
+ channelURL = channelNameField[channelNameField.indexOf('name') + 1];
+ channelObj = ChannelStore.getByName(channelURL);
+ if (channelObj) {
+ channelName = channelObj.display_name;
+ } else {
+ channelName = channelURL;
+ }
+ }
+
+ switch (currentActionURL) {
+ case '/channels/create':
+ currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
+ break;
+ case '/channels/create_direct':
+ currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
+ break;
+ case '/channels/update':
+ currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
+ break;
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
+ break;
+ default: {
+ let userIdField = [];
+ let userId = '';
+ let username = '';
+
+ if (channelInfo[1]) {
+ userIdField = channelInfo[1].split('=');
+
+ if (userIdField.indexOf('user_id') >= 0) {
+ userId = userIdField[userIdField.indexOf('user_id') + 1];
+ username = UserStore.getProfile(userId).username;
+ }
+ }
+
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
+ }
+
+ break;
+ }
+ }
+ } else if (currentActionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/oauth/register': {
+ const clientIdField = oauthInfo[0].split('=');
+
+ if (clientIdField[0] === 'client_id') {
+ currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
+ }
+
+ break;
+ }
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ currentAuditDesc = formatMessage(holders.failedOAuthAccess);
+ }
+
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullOAuthToken);
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
+ }
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/users') === 0) {
+ const userInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedLogin);
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullLogin);
+ } else if (userInfo[0]) {
+ currentAuditDesc = formatMessage(holders.failedLogin);
+ }
+
+ break;
+ case '/users/revoke_session':
+ currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
+ break;
+ case '/users/newimage':
+ currentAuditDesc = formatMessage(holders.updatePicture);
+ break;
+ case '/users/update':
+ currentAuditDesc = formatMessage(holders.updateGeneral);
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ currentAuditDesc = formatMessage(holders.attemptedPassword);
+ } else if (userInfo[0] === 'completed') {
+ currentAuditDesc = formatMessage(holders.successfullPassword);
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ currentAuditDesc = formatMessage(holders.failedPassword);
+ }
+
+ break;
+ case '/users/update_roles': {
+ const userRoles = userInfo[0].split('=')[1];
+
+ currentAuditDesc = formatMessage(holders.updatedRol);
+ if (userRoles.trim()) {
+ currentAuditDesc += userRoles;
+ } else {
+ currentAuditDesc += formatMessage(holders.member);
+ }
+
+ break;
+ }
+ case '/users/update_active': {
+ const updateType = userInfo[0].split('=')[0];
+ const updateField = userInfo[0].split('=')[1];
+
+ /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
+ if (updateType === 'active') {
+ if (updateField === 'true') {
+ currentAuditDesc = formatMessage(holders.accountActive);
+ } else if (updateField === 'false') {
+ currentAuditDesc = formatMessage(holders.accountInactive);
+ }
+
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const currentUser = UserStore.getCurrentUser();
+ if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
+ currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
+ } else if (currentUser && actingUser) {
+ currentAuditDesc += formatMessage(holders.byAdmin);
+ }
+ }
+ } else if (updateType === 'session_id') {
+ currentAuditDesc = this.handleRevokedSession(updateField);
+ }
+
+ break;
+ }
+ case '/users/send_password_reset':
+ currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedReset);
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullReset);
+ }
+
+ break;
+ case '/users/update_notify':
+ currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ currentAuditDesc = formatMessage(holders.failedWebhookCreate);
+ }
+
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ currentAuditDesc = formatMessage(holders.failedWebhookDelete);
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (currentActionURL) {
+ case '/logout':
+ currentAuditDesc = formatMessage(holders.logout);
+ break;
+ case '/verify_email':
+ currentAuditDesc = formatMessage(holders.verified);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* If all else fails... */
+ if (!currentAuditDesc) {
+ /* Currently not called anywhere */
+ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
+ currentAuditDesc = formatMessage(holders.revokedAll);
+ } else {
+ let currentActionDesc = '';
+ if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
+ currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ currentActionDesc = Utils.toTitleCase(currentActionDesc);
+ }
+
+ let currentExtraInfoDesc = '';
+ if (currentAudit.extra_info) {
+ currentExtraInfoDesc = currentAudit.extra_info;
+
+ if (currentExtraInfoDesc.indexOf('=') !== -1) {
+ currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ }
+ }
+ currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ }
+ }
+
+ const currentDate = new Date(currentAudit.create_at);
+ let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+
+ if (this.props.showUserId) {
+ currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
+ }
+
+ currentAuditInfo += ' | ' + currentAuditDesc;
+
+ return currentAuditInfo;
+ }
+ render() {
+ var accessList = [];
+
+ const {formatMessage} = this.props.intl;
+ for (var i = 0; i < this.props.audits.length; i++) {
+ const currentAudit = this.props.audits[i];
+ const currentAuditInfo = this.formatAuditInfo(currentAudit);
+
+ let moreInfo;
+ if (!this.props.oneLine) {
+ moreInfo = (
+
+
+
+ );
+ }
+
+ if (this.state.moreInfo[i]) {
+ if (!currentAudit.session_id) {
+ currentAudit.session_id = 'N/A';
+
+ if (currentAudit.action.search('/users/login') >= 0) {
+ if (currentAudit.extra_info === 'attempt') {
+ currentAudit.session_id += formatMessage(holders.loginAttempt);
+ } else {
+ currentAudit.session_id += formatMessage(holders.loginFailure);
+ }
+ }
+ }
+
+ moreInfo = (
+
+ );
+ }
+
+ var divider = null;
+ if (i < this.props.audits.length - 1) {
+ divider = ();
+ }
+
+ accessList[i] = (
+
+
+
{currentAuditInfo}
+
+ {moreInfo}
+
+ {divider}
+
+
+ );
+ }
+
+ return ;
+ }
+}
+
+AuditTable.propTypes = {
+ intl: intlShape.isRequired,
+ audits: React.PropTypes.array.isRequired,
+ oneLine: React.PropTypes.bool,
+ showUserId: React.PropTypes.bool
+};
+
+export default injectIntl(AuditTable);
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 53dad1306e..443ecefde7 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -69,7 +69,7 @@ export default class CenterPanel extends React.Component {
onClick={handleClick}
>
- {'You are viewing the Archives. Click here to jump to recent messages. '}
+ {'Click here to jump to recent messages. '}
{}
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index f64834775b..005a822099 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -24,8 +24,10 @@ import * as TextFormatting from '../utils/text_formatting.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
+import {FormattedMessage} from 'mm-intl';
+
+const ActionTypes = Constants.ActionTypes;
const Popover = ReactBootstrap.Popover;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
const Tooltip = ReactBootstrap.Tooltip;
@@ -124,7 +126,14 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
- const recentMentionsTooltip =
{'Recent Mentions'};
+ const recentMentionsTooltip = (
+
+
+
+ );
const popoverContent = (
+ );
+ if (channel.type === Constants.PRIVATE_CHANNEL) {
+ channelTerm = (
+
+ );
}
const dropdownContents = [];
@@ -174,7 +193,10 @@ export default class ChannelHeader extends React.Component {
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
>
- {'Set Channel Header...'}
+
);
@@ -189,7 +211,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelInfoModal}
dialogProps={{channel}}
>
- {'View Info'}
+
);
@@ -205,7 +230,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelInviteModal}
dialogProps={{channel}}
>
- {'Add Members'}
+
);
@@ -221,7 +249,10 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={() => this.setState({showMembersModal: true})}
>
- {'Manage Members'}
+
);
@@ -238,7 +269,13 @@ export default class ChannelHeader extends React.Component {
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
>
- {`Set ${channelTerm} Header...`}
+
);
@@ -252,7 +289,13 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
>
- {'Set '}{channelTerm}{' Purpose...'}
+
);
@@ -266,7 +309,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
>
- {'Notification Preferences'}
+
);
@@ -286,7 +332,13 @@ export default class ChannelHeader extends React.Component {
data-name={channel.name}
data-channelid={channel.id}
>
- {'Rename '}{channelTerm}{'...'}
+
);
@@ -302,7 +354,13 @@ export default class ChannelHeader extends React.Component {
dialogType={DeleteChannelModal}
dialogProps={{channel}}
>
- {'Delete '}{channelTerm}{'...'}
+
);
@@ -320,7 +378,13 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={this.handleLeave}
>
- {'Leave '}{channelTerm}
+
);
diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx
index 72c7c3daa9..5067f59133 100644
--- a/web/react/components/channel_info_modal.jsx
+++ b/web/react/components/channel_info_modal.jsx
@@ -2,17 +2,28 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
+
+import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+
const Modal = ReactBootstrap.Modal;
-export default class ChannelInfoModal extends React.Component {
+const holders = defineMessages({
+ notFound: {
+ id: 'channel_info.notFound',
+ defaultMessage: 'No Channel Found'
+ }
+});
+
+class ChannelInfoModal extends React.Component {
render() {
+ const {formatMessage} = this.props.intl;
let channel = this.props.channel;
if (!channel) {
channel = {
- display_name: 'No Channel Found',
- name: 'No Channel Found',
- purpose: 'No Channel Found',
- id: 'No Channel Found'
+ display_name: formatMessage(holders.notFound),
+ name: formatMessage(holders.notFound),
+ purpose: formatMessage(holders.notFound),
+ id: formatMessage(holders.notFound)
};
}
@@ -28,19 +39,39 @@ export default class ChannelInfoModal extends React.Component {
-
{'Channel Name:'}
+
+
+
{channel.display_name}
-
{'Channel URL:'}
+
+
+
{channelURL}
-
{'Channel ID:'}
+
+
+
{channel.id}
-
{'Channel Purpose:'}
+
+
+
{channel.purpose}
@@ -50,7 +81,10 @@ export default class ChannelInfoModal extends React.Component {
className='btn btn-default'
onClick={this.props.onHide}
>
- {'Close'}
+
@@ -59,7 +93,10 @@ export default class ChannelInfoModal extends React.Component {
}
ChannelInfoModal.propTypes = {
+ intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
};
+
+export default injectIntl(ChannelInfoModal);
\ No newline at end of file
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 8b7485e5fe..7dc2c0a11c 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -11,6 +11,8 @@ import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
+import {FormattedMessage} from 'mm-intl';
+
const Modal = ReactBootstrap.Modal;
export default class ChannelInviteModal extends React.Component {
@@ -154,7 +156,13 @@ export default class ChannelInviteModal extends React.Component {
onHide={this.props.onHide}
>
- {'Add New Members to '}{this.props.channel.display_name}
+
+
+ {this.props.channel.display_name}
+
- {'Close'}
+
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 513a720e7d..f3cbef7190 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -12,6 +12,8 @@ import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import * as Utils from '../utils/utils.jsx';
+import {FormattedMessage} from 'mm-intl';
+
const Modal = ReactBootstrap.Modal;
export default class ChannelMembersModal extends React.Component {
@@ -191,7 +193,13 @@ export default class ChannelMembersModal extends React.Component {
onHide={this.props.onModalDismissed}
>
- {this.props.channel.display_name}{' Members'}
+
+ {this.props.channel.display_name}
+
+
- {' Add New Members'}
+
+
- {'Close'}
+
diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx
index e70d3a634a..59ef8966e6 100644
--- a/web/react/components/channel_notifications_modal.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -9,6 +9,8 @@ import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
+import {FormattedMessage} from 'mm-intl';
+
export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
@@ -97,13 +99,35 @@ export default class ChannelNotificationsModal extends React.Component {
let globalNotifyLevelName;
if (globalNotifyLevel === 'all') {
- globalNotifyLevelName = 'For all activity';
+ globalNotifyLevelName = (
+
+ );
} else if (globalNotifyLevel === 'mention') {
- globalNotifyLevelName = 'Only for mentions';
+ globalNotifyLevelName = (
+
+ );
} else {
- globalNotifyLevelName = 'Never';
+ globalNotifyLevelName = (
+
+ );
}
+ const sendDesktop = (
+
+ );
+
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false, false];
if (this.state.notifyLevel === 'default') {
@@ -127,7 +151,13 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[0]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
/>
- {`Global default (${globalNotifyLevelName})`}
+
@@ -138,7 +168,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[1]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
/>
- {'For all activity'}
+
@@ -149,7 +179,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[2]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
/>
- {'Only for mentions'}
+
@@ -160,7 +190,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
/>
- {'Never'}
+
@@ -174,13 +204,16 @@ export default class ChannelNotificationsModal extends React.Component {
const extraInfo = (
- {'Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'}
+
);
return (
+ );
} else if (this.state.notifyLevel === 'mention') {
- describe = 'Only for mentions';
+ describe = (
);
} else if (this.state.notifyLevel === 'all') {
- describe = 'For all activity';
+ describe = (
);
} else {
- describe = 'Never';
+ describe = (
);
}
handleUpdateSection = function updateSection(e) {
@@ -208,7 +248,7 @@ export default class ChannelNotificationsModal extends React.Component {
return (
@@ -250,6 +290,12 @@ export default class ChannelNotificationsModal extends React.Component {
createMarkUnreadLevelSection(serverError) {
let content;
+ const markUnread = (
+
+ );
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
@@ -260,7 +306,10 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
- {'For all unread messages'}
+
@@ -271,7 +320,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
/>
- {'Only for mentions'}
+
@@ -284,11 +333,18 @@ export default class ChannelNotificationsModal extends React.Component {
e.preventDefault();
}.bind(this);
- const extraInfo =
{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'};
+ const extraInfo = (
+
+
+
+ );
content = (
+ );
} else {
- describe = 'Only for mentions';
+ describe = (
);
}
const handleUpdateSection = function handleUpdateSection(e) {
@@ -312,7 +373,7 @@ export default class ChannelNotificationsModal extends React.Component {
content = (
@@ -335,7 +396,13 @@ export default class ChannelNotificationsModal extends React.Component {
onHide={this.props.onHide}
>
- {'Notification Preferences for '}{this.props.channel.display_name}
+
+
+ {this.props.channel.display_name}
+
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 1255067fdc..d9113bc9fb 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -5,7 +5,9 @@ import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
const Modal = ReactBootstrap.Modal;
import TeamStore from '../stores/team_store.jsx';
-import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+
+import {FormattedMessage} from 'mm-intl';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -32,7 +34,20 @@ export default class DeleteChannelModal extends React.Component {
}
render() {
- const channelTerm = Utils.getChannelTerm(this.props.channel.type).toLowerCase();
+ let channelTerm = (
+
+ );
+ if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
+ channelTerm = (
+
+ );
+ }
return (
- {'Confirm DELETE Channel'}
+
+
+
- {`Are you sure you wish to delete the ${this.props.channel.display_name} ${channelTerm}?`}
+
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 4cde5feed2..34fd724f57 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -9,6 +9,9 @@ import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
+
+import {FormattedMessage} from 'mm-intl';
+
var ActionTypes = Constants.ActionTypes;
export default class DeletePostModal extends React.Component {
@@ -128,10 +131,28 @@ export default class DeletePostModal extends React.Component {
var commentWarning = '';
if (this.state.commentCount > 0) {
- commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
+ commentWarning = (
+
+ );
}
- const postTerm = Utils.getPostTerm(this.state.post);
+ const postTerm = this.state.post.root_id ? (
+
+ ) : (
+
+ );
return (
- {`Confirm ${postTerm} Delete`}
+
+
+
- {`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
+
{commentWarning}
@@ -154,7 +189,10 @@ export default class DeletePostModal extends React.Component {
className='btn btn-default'
onClick={this.handleHide}
>
- {'Cancel'}
+
diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx
index e4817f6e42..1066d123ea 100644
--- a/web/react/components/edit_channel_header_modal.jsx
+++ b/web/react/components/edit_channel_header_modal.jsx
@@ -6,9 +6,18 @@ import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import * as Utils from '../utils/utils.jsx';
+import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+
const Modal = ReactBootstrap.Modal;
-export default class EditChannelHeaderModal extends React.Component {
+const holders = defineMessages({
+ error: {
+ id: 'edit_channel_header_modal.error',
+ defaultMessage: 'This channel header is too long, please enter a shorter one'
+ }
+});
+
+class EditChannelHeaderModal extends React.Component {
constructor(props) {
super(props);
@@ -64,8 +73,8 @@ export default class EditChannelHeaderModal extends React.Component {
});
},
(err) => {
- if (err.message === 'Invalid channel_header parameter') {
- this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
+ if (err.id === 'api.context.invalid_param.app_error') {
+ this.setState({serverError: this.props.intl.formatMessage(holders.error)});
} else {
this.setState({serverError: err.message});
}
@@ -99,10 +108,23 @@ export default class EditChannelHeaderModal extends React.Component {
onHide={this.onHide}
>
- {'Edit Header for ' + this.props.channel.display_name}
+
+
+
- {'Edit the text appearing next to the channel name in the channel header.'}
+
+
+
- Cancel
+
@@ -188,3 +212,9 @@ export default class EditPostModal extends React.Component {
);
}
}
+
+EditPostModal.propTypes = {
+ intl: intlShape.isRequired
+};
+
+export default injectIntl(EditPostModal);
\ No newline at end of file
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 3fc71ff960..de3387a357 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -41,6 +41,8 @@ export default class GetLinkModal extends React.Component {
}
render() {
+ const userCreationEnabled = global.window.mm_config.EnableUserCreation === 'true';
+
let helpText = null;
if (this.props.helpText) {
helpText = (
@@ -53,7 +55,7 @@ export default class GetLinkModal extends React.Component {
}
let copyLink = null;
- if (document.queryCommandSupported('copy')) {
+ if (userCreationEnabled && document.queryCommandSupported('copy')) {
copyLink = (
+ );
+ }
+
var copyLinkConfirm = null;
if (this.state.copiedLink) {
copyLinkConfirm = (
@@ -92,12 +106,7 @@ export default class GetLinkModal extends React.Component {
{helpText}
-
+ {linkText}
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index c16216c680..c467c0d875 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -7,6 +7,39 @@ import * as AsyncClient from '../utils/async_client.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
+import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ required: {
+ id: 'rename_channel.required',
+ defaultMessage: 'This field is required'
+ },
+ maxLength: {
+ id: 'rename_channel.maxLength',
+ defaultMessage: 'This field must be less than 22 characters'
+ },
+ lowercase: {
+ id: 'rename_channel.lowercase',
+ defaultMessage: 'Must be lowercase alphanumeric characters'
+ },
+ handle: {
+ id: 'rename_channel.handle',
+ defaultMessage: 'Handle'
+ },
+ defaultError: {
+ id: 'rename_channel.defaultError',
+ defaultMessage: ' - Cannot be changed for the default channel'
+ },
+ displayNameHolder: {
+ id: 'rename_channel.displayNameHolder',
+ defaultMessage: 'Enter display name'
+ },
+ handleHolder: {
+ id: 'rename_channel.handleHolder',
+ defaultMessage: 'lowercase alphanumeric's only'
+ }
+});
+
export default class RenameChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -41,13 +74,14 @@ export default class RenameChannelModal extends React.Component {
const oldName = channel.name;
const oldDisplayName = channel.displayName;
const state = {serverError: ''};
+ const {formatMessage} = this.props.intl;
channel.display_name = this.state.displayName.trim();
if (!channel.display_name) {
- state.displayNameError = 'This field is required';
+ state.displayNameError = formatMessage(holders.required);
state.invalid = true;
} else if (channel.display_name.length > 22) {
- state.displayNameError = 'This field must be less than 22 characters';
+ state.displayNameError = formatMessage(holders.maxLength);
state.invalid = true;
} else {
state.displayNameError = '';
@@ -55,17 +89,17 @@ export default class RenameChannelModal extends React.Component {
channel.name = this.state.channelName.trim();
if (!channel.name) {
- state.nameError = 'This field is required';
+ state.nameError = formatMessage(holders.required);
state.invalid = true;
} else if (channel.name.length > 22) {
- state.nameError = 'This field must be less than 22 characters';
+ state.nameError = formatMessage(holders.maxLength);
state.invalid = true;
} else {
const cleanedName = Utils.cleanUpUrlable(channel.name);
if (cleanedName === channel.name) {
state.nameError = '';
} else {
- state.nameError = 'Must be lowercase alphanumeric characters';
+ state.nameError = formatMessage(holders.lowercase);
state.invalid = true;
}
}
@@ -153,11 +187,13 @@ export default class RenameChannelModal extends React.Component {
serverError = ;
}
- let handleInputLabel = 'Handle';
+ const {formatMessage} = this.props.intl;
+
+ let handleInputLabel = formatMessage(holders.handle);
let handleInputClass = 'form-control';
let readOnlyHandleInput = false;
if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
- handleInputLabel += ' - Cannot be changed for the default channel';
+ handleInputLabel += formatMessage(holders.defaultError);
handleInputClass += ' disabled-input';
readOnlyHandleInput = true;
}
@@ -180,14 +216,29 @@ export default class RenameChannelModal extends React.Component {
data-dismiss='modal'
>
{'×'}
- {'Close'}
+
+
+
- {'Rename Channel'}
+
+
+