summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--api/admin.go31
-rw-r--r--api/admin_test.go32
-rw-r--r--api/api_test.go3
-rw-r--r--api/channel_test.go2
-rw-r--r--api/command.go4
-rw-r--r--api/context.go40
-rw-r--r--api/export.go8
-rw-r--r--api/file.go80
-rw-r--r--api/file_benchmark_test.go2
-rw-r--r--api/file_test.go56
-rw-r--r--api/post.go70
-rw-r--r--api/post_test.go92
-rw-r--r--api/server.go6
-rw-r--r--api/team.go100
-rw-r--r--api/team_test.go111
-rw-r--r--api/user.go153
-rw-r--r--api/user_test.go52
-rw-r--r--api/web_socket_test.go2
-rw-r--r--api/webhook.go6
-rw-r--r--api/webhook_test.go6
-rw-r--r--config/config.json116
-rw-r--r--docker/dev/config_docker.json119
-rw-r--r--docker/local/config_docker.json119
-rw-r--r--manualtesting/manual_testing.go2
-rw-r--r--mattermost.go2
-rw-r--r--model/client.go22
-rw-r--r--model/config.go135
-rw-r--r--model/team.go21
-rw-r--r--store/sql_session_store.go18
-rw-r--r--store/sql_session_store_test.go23
-rw-r--r--store/sql_store.go42
-rw-r--r--store/sql_team_store.go5
-rw-r--r--store/sql_user_store.go19
-rw-r--r--store/sql_user_store_test.go20
-rw-r--r--store/store.go4
-rw-r--r--utils/config.go69
-rw-r--r--utils/mail.go56
-rw-r--r--web/react/components/admin_console/admin_controller.jsx105
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx212
-rw-r--r--web/react/components/admin_console/email_settings.jsx617
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx277
-rw-r--r--web/react/components/admin_console/image_settings.jsx496
-rw-r--r--web/react/components/admin_console/log_settings.jsx62
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx163
-rw-r--r--web/react/components/admin_console/rate_settings.jsx272
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx193
-rw-r--r--web/react/components/admin_console/service_settings.jsx296
-rw-r--r--web/react/components/admin_console/sql_settings.jsx283
-rw-r--r--web/react/components/admin_console/team_settings.jsx257
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/components/invite_member_modal.jsx2
-rw-r--r--web/react/components/login.jsx13
-rw-r--r--web/react/components/signup_team.jsx56
-rw-r--r--web/react/components/signup_user_complete.jsx10
-rw-r--r--web/react/components/team_import_tab.jsx2
-rw-r--r--web/react/components/team_signup_choose_auth.jsx20
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx2
-rw-r--r--web/react/components/user_profile.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx2
-rw-r--r--web/react/components/view_image.jsx2
-rw-r--r--web/react/pages/login.jsx1
-rw-r--r--web/react/pages/signup_team.jsx6
-rw-r--r--web/react/pages/signup_user_complete.jsx1
-rw-r--r--web/react/stores/admin_store.jsx40
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx46
-rw-r--r--web/react/utils/constants.jsx2
-rw-r--r--web/react/utils/markdown.jsx18
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss8
-rw-r--r--web/sass-files/sass/partials/_headers.scss1
-rw-r--r--web/sass-files/sass/partials/_markdown.scss34
-rw-r--r--web/sass-files/sass/styles.scss1
-rw-r--r--web/templates/head.html26
-rw-r--r--web/web.go14
-rw-r--r--web/web_test.go4
79 files changed, 4513 insertions, 1290 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18238d9eb..2abbb6460 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,11 +21,6 @@ Admin Console
- (Preview) Ability to view server logs and change config settings
-Integrations
-
-- (Preview) Added API for incoming webhooks
-- (Preview) Added OAuth2 as a service provider to allow for more secure connection to external apps
-
### Improvements
Documentation
diff --git a/api/admin.go b/api/admin.go
index 646597755..568d8f6e8 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -24,6 +24,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
+ sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -34,7 +35,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
var lines []string
- if utils.Cfg.LogSettings.FileEnable {
+ if utils.Cfg.LogSettings.EnableFile {
file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
if err != nil {
@@ -81,7 +82,7 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if len(cfg.ServiceSettings.Port) == 0 {
+ if len(cfg.ServiceSettings.ListenAddress) == 0 {
c.SetInvalidParam("saveConfig", "config")
return
}
@@ -98,3 +99,29 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
json := utils.Cfg.ToJson()
w.Write([]byte(json))
}
+
+func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("testEmail") {
+ return
+ }
+
+ cfg := model.ConfigFromJson(r.Body)
+ if cfg == nil {
+ c.SetInvalidParam("testEmail", "config")
+ return
+ }
+
+ if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ if err := utils.SendMailUsingConfig(result.Data.(*model.User).Email, "Mattermost - Testing Email Settings", "<br/><br/><br/>It appears your Mattermost email is setup correctly!", cfg); err != nil {
+ c.Err = err
+ return
+ }
+ }
+
+ m := make(map[string]string)
+ m["SUCCESS"] = "true"
+ w.Write([]byte(model.MapToJson(m)))
+}
diff --git a/api/admin_test.go b/api/admin_test.go
index e1778b5ac..ad7ac08f8 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -83,7 +83,7 @@ func TestGetConfig(t *testing.T) {
} else {
cfg := result.Data.(*model.Config)
- if len(cfg.ServiceSettings.SiteName) == 0 {
+ if len(cfg.TeamSettings.SiteName) == 0 {
t.Fatal()
}
}
@@ -117,8 +117,36 @@ func TestSaveConfig(t *testing.T) {
} else {
cfg := result.Data.(*model.Config)
- if len(cfg.ServiceSettings.SiteName) == 0 {
+ if len(cfg.TeamSettings.SiteName) == 0 {
t.Fatal()
}
}
}
+
+func TestEmailTest(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() + "corey@test.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.TestEmail(utils.Cfg); 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 _, err := Client.TestEmail(utils.Cfg); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/api/api_test.go b/api/api_test.go
index 642db581e..761f3e33f 100644
--- a/api/api_test.go
+++ b/api/api_test.go
@@ -14,10 +14,11 @@ var Client *model.Client
func Setup() {
if Srv == nil {
utils.LoadConfig("config.json")
+ utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
NewServer()
StartServer()
InitApi()
- Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
+ Client = model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
}
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 14bfe1cf7..7845ac499 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -627,7 +627,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag = cache_result.Etag
}
- Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
+ Client2 := model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "tester2@test.com", Nickname: "Tester 2", Password: "pwd"}
user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User)
diff --git a/api/command.go b/api/command.go
index bc55f206b..0d2f7597b 100644
--- a/api/command.go
+++ b/api/command.go
@@ -215,8 +215,8 @@ func joinCommand(c *Context, command *model.Command) bool {
func loadTestCommand(c *Context, command *model.Command) bool {
cmd := "/loadtest"
- // This command is only available when AllowTesting is true
- if !utils.Cfg.ServiceSettings.AllowTesting {
+ // This command is only available when EnableTesting is true
+ if !utils.Cfg.ServiceSettings.EnableTesting {
return false
}
diff --git a/api/context.go b/api/context.go
index 02716bb33..9a276a1a1 100644
--- a/api/context.go
+++ b/api/context.go
@@ -107,21 +107,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
isTokenFromQueryString = true
}
- protocol := "http"
-
- // If the request came from the ELB then assume this is produciton
- // and redirect all http requests to https
- if utils.Cfg.ServiceSettings.UseSSL {
- forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO)
- if forwardProto == "http" {
- l4g.Info("redirecting http request to https for %v", r.URL.Path)
- http.Redirect(w, r, "https://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- protocol = "https"
- }
- }
-
+ protocol := GetProtocol(r)
c.setSiteURL(protocol + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
@@ -209,6 +195,14 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+func GetProtocol(r *http.Request) string {
+ if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
+ return "https"
+ } else {
+ return "http"
+ }
+}
+
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-Srv.Store.Audit().Save(audit); r.Err != nil {
@@ -385,6 +379,11 @@ func (c *Context) GetSiteURL() string {
func GetIpAddress(r *http.Request) string {
address := r.Header.Get(model.HEADER_FORWARDED)
+
+ if len(address) == 0 {
+ address = r.Header.Get(model.HEADER_REAL_IP)
+ }
+
if len(address) == 0 {
address, _, _ = net.SplitHostPort(r.RemoteAddr)
}
@@ -458,20 +457,13 @@ func IsPrivateIpAddress(ipAddress string) bool {
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
- protocol := "http"
- if utils.Cfg.ServiceSettings.UseSSL {
- forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO)
- if forwardProto != "http" {
- protocol = "https"
- }
- }
-
+ protocol := GetProtocol(r)
SiteURL := protocol + "://" + r.Host
m := make(map[string]string)
m["Message"] = err.Message
m["Details"] = err.DetailedError
- m["SiteName"] = utils.Cfg.ServiceSettings.SiteName
+ m["SiteName"] = utils.Cfg.TeamSettings.SiteName
m["SiteURL"] = SiteURL
w.WriteHeader(err.StatusCode)
diff --git a/api/export.go b/api/export.go
index 9345f892f..aff34073f 100644
--- a/api/export.go
+++ b/api/export.go
@@ -87,7 +87,7 @@ func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
// Get the teams
var teams []*model.Team
if len(options.TeamsToExport) == 0 {
- if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
return result.Err
} else {
teams = result.Data.([]*model.Team)
@@ -278,11 +278,11 @@ func copyDirToExportWriter(writer ExportWriter, inPath string, outPath string) *
}
func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
- teamDir := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + teamId
+ teamDir := utils.Cfg.FileSettings.Directory + "teams/" + teamId
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "")
- } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil {
return err
}
diff --git a/api/file.go b/api/file.go
index 3602b5ed4..694fc734c 100644
--- a/api/file.go
+++ b/api/file.go
@@ -69,8 +69,8 @@ func InitFile(r *mux.Router) {
}
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
- c.Err = model.NewAppError("uploadFile", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "")
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewAppError("uploadFile", "Unable to upload file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -217,8 +217,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create thumbnail
go func() {
- thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth)
- thumbHeight := float64(utils.Cfg.ImageSettings.ThumbnailHeight)
+ thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
+ thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
imgWidth := float64(width)
imgHeight := float64(height)
@@ -226,9 +226,9 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
if imgHeight < thumbHeight && imgWidth < thumbWidth {
thumbnail = img
} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
- thumbnail = resize.Resize(0, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3)
+ thumbnail = resize.Resize(0, utils.Cfg.FileSettings.ThumbnailHeight, img, resize.Lanczos3)
} else {
- thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, 0, img, resize.Lanczos3)
+ thumbnail = resize.Resize(utils.Cfg.FileSettings.ThumbnailWidth, 0, img, resize.Lanczos3)
}
buf := new(bytes.Buffer)
@@ -247,8 +247,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create preview
go func() {
var preview image.Image
- if width > int(utils.Cfg.ImageSettings.PreviewWidth) {
- preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.Lanczos3)
+ if width > int(utils.Cfg.FileSettings.PreviewWidth) {
+ preview = resize.Resize(utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, img, resize.Lanczos3)
} else {
preview = img
}
@@ -294,8 +294,8 @@ type ImageGetResult struct {
}
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
- c.Err = model.NewAppError("getFileInfo", "Unable to get file info. Amazon S3 not configured and local server storage turned off. ", "")
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewAppError("uploadFile", "Unable to get file info. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -357,8 +357,8 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
- c.Err = model.NewAppError("getFile", "Unable to get file. Amazon S3 not configured and local server storage turned off. ", "")
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewAppError("uploadFile", "Unable to get file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -400,7 +400,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
asyncGetFile(path, fileData)
if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 {
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) {
c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "")
return
}
@@ -442,17 +442,17 @@ func asyncGetFile(path string, fileData chan []byte) {
}
func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.TeamSettings.AllowPublicLink {
- c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "")
- c.Err.StatusCode = http.StatusForbidden
- }
-
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
- c.Err = model.NewAppError("getPublicLink", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "")
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewAppError("uploadFile", "Unable to get link. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
+ if !utils.Cfg.FileSettings.EnablePublicLink {
+ c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "")
+ c.Err.StatusCode = http.StatusForbidden
+ }
+
props := model.MapFromJson(r.Body)
filename := props["filename"]
@@ -478,7 +478,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
url := fmt.Sprintf("%s/api/v1/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL(), channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId)
@@ -511,13 +511,13 @@ func getExport(c *Context, w http.ResponseWriter, r *http.Request) {
func writeFile(f []byte, path string) *model.AppError {
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
ext := filepath.Ext(path)
@@ -534,12 +534,12 @@ func writeFile(f []byte, path string) *model.AppError {
if err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error())
}
- } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
- if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil {
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error())
}
- if err := ioutil.WriteFile(utils.Cfg.ServiceSettings.StorageDirectory+path, f, 0644); err != nil {
+ if err := ioutil.WriteFile(utils.Cfg.FileSettings.Directory+path, f, 0644); err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
}
} else {
@@ -551,13 +551,13 @@ func writeFile(f []byte, path string) *model.AppError {
func readFile(path string) ([]byte, *model.AppError) {
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
// try to get the file from S3 with some basic retry logic
tries := 0
@@ -573,8 +573,8 @@ func readFile(path string) ([]byte, *model.AppError) {
}
time.Sleep(3000 * time.Millisecond)
}
- } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
- if f, err := ioutil.ReadFile(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil {
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewAppError("readFile", "Encountered an error reading from local server storage", err.Error())
} else {
return f, nil
@@ -585,14 +585,14 @@ func readFile(path string) ([]byte, *model.AppError) {
}
func openFileWriteStream(path string) (io.Writer, *model.AppError) {
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "")
- } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
- if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil {
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error())
}
- if fileHandle, err := os.Create(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil {
+ if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error())
} else {
fileHandle.Chmod(0644)
diff --git a/api/file_benchmark_test.go b/api/file_benchmark_test.go
index 251ff7793..47f8bff43 100644
--- a/api/file_benchmark_test.go
+++ b/api/file_benchmark_test.go
@@ -38,7 +38,7 @@ func BenchmarkGetFile(b *testing.B) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
// wait a bit for files to ready
time.Sleep(5 * time.Second)
diff --git a/api/file_test.go b/api/file_test.go
index eb1fcf2ce..657c08131 100644
--- a/api/file_test.go
+++ b/api/file_test.go
@@ -68,7 +68,7 @@ func TestUploadFile(t *testing.T) {
}
resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType())
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
if appErr != nil {
t.Fatal(appErr)
}
@@ -81,11 +81,11 @@ func TestUploadFile(t *testing.T) {
fileId := strings.Split(filename, ".")[0]
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
// wait a bit for files to ready
time.Sleep(5 * time.Second)
@@ -104,7 +104,7 @@ func TestUploadFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
if strings.Contains(filename, "../") {
@@ -115,17 +115,17 @@ func TestUploadFile(t *testing.T) {
// wait a bit for files to ready
time.Sleep(5 * time.Second)
- path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
+ path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
@@ -151,7 +151,7 @@ func TestGetFile(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -222,7 +222,7 @@ func TestGetFile(t *testing.T) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
Client.LoginByEmail(team2.Name, user2.Email, "pwd")
@@ -262,13 +262,13 @@ func TestGetFile(t *testing.T) {
t.Fatal("Should have errored - user not logged in and link not public")
}
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
@@ -293,17 +293,17 @@ func TestGetFile(t *testing.T) {
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
fileId := strings.Split(filename, ".")[0]
- path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
+ path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
@@ -334,7 +334,7 @@ func TestGetPublicLink(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -410,14 +410,14 @@ func TestGetPublicLink(t *testing.T) {
t.Fatal("should have errored, user not member of channel")
}
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
// perform clean-up on s3
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
@@ -442,17 +442,17 @@ func TestGetPublicLink(t *testing.T) {
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
fileId := strings.Split(filename, ".")[0]
- path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
+ path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
- path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
+ path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
diff --git a/api/post.go b/api/post.go
index bbbc93115..0379f6af5 100644
--- a/api/post.go
+++ b/api/post.go
@@ -25,7 +25,6 @@ func InitPost(r *mux.Router) {
sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter()
sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
- sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST")
sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET")
sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET")
@@ -60,75 +59,6 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) {
- tchan := Srv.Store.Team().Get(c.Session.TeamId)
-
- post := model.PostFromJson(r.Body)
- if post == nil {
- c.SetInvalidParam("createValetPost", "post")
- return
- }
-
- cchan := Srv.Store.Channel().CheckOpenChannelPermissions(c.Session.TeamId, post.ChannelId)
-
- // Any one with access to the team can post as valet to any open channel
- if !c.HasPermissionsToChannel(cchan, "createValetPost") {
- return
- }
-
- // Make sure this team has the valet feature enabled
- if tResult := <-tchan; tResult.Err != nil {
- c.Err = model.NewAppError("createValetPost", "Could not find the team for this session, team_id="+c.Session.TeamId, "")
- return
- } else {
- if !tResult.Data.(*model.Team).AllowValet {
- c.Err = model.NewAppError("createValetPost", "The valet feature is currently turned off. Please contact your team administrator for details.", "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
- }
-
- if rp, err := CreateValetPost(c, post); err != nil {
- c.Err = err
-
- if strings.Contains(c.Err.Message, "parameter") {
- c.Err.StatusCode = http.StatusBadRequest
- }
-
- return
- } else {
- w.Write([]byte(rp.ToJson()))
- }
-}
-
-func CreateValetPost(c *Context, post *model.Post) (*model.Post, *model.AppError) {
- post.Hashtags, _ = model.ParseHashtags(post.Message)
-
- post.Filenames = []string{} // no files allowed in valet posts yet
-
- if result := <-Srv.Store.User().GetByUsername(c.Session.TeamId, "valet"); result.Err != nil {
- // if the bot doesn't exist, create it
- if tresult := <-Srv.Store.Team().Get(c.Session.TeamId); tresult.Err != nil {
- return nil, tresult.Err
- } else {
- post.UserId = (CreateValet(c, tresult.Data.(*model.Team))).Id
- }
- } else {
- post.UserId = result.Data.(*model.User).Id
- }
-
- var rpost *model.Post
- if result := <-Srv.Store.Post().Save(post); result.Err != nil {
- return nil, result.Err
- } else {
- rpost = result.Data.(*model.Post)
- }
-
- fireAndForgetNotifications(rpost, c.Session.TeamId, c.GetSiteURL())
-
- return rpost, nil
-}
-
func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) {
var pchan store.StoreChannel
if len(post.RootId) > 0 {
diff --git a/api/post_test.go b/api/post_test.go
index 4cccfd62a..358611240 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -123,98 +123,6 @@ func TestCreatePost(t *testing.T) {
}
}
-func TestCreateValetPost(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)
-
- team2 := &model.Team{DisplayName: "Name Team 2", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
-
- user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
- user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
-
- user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
- user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
-
- Client.LoginByEmail(team.Name, user1.Email, "pwd")
-
- channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
-
- channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
-
- if utils.Cfg.TeamSettings.AllowValetDefault {
- post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"}
- rpost1, err := Client.CreateValetPost(post1)
- if err != nil {
- t.Fatal(err)
- }
-
- if rpost1.Data.(*model.Post).Message != post1.Message {
- t.Fatal("message didn't match")
- }
-
- if rpost1.Data.(*model.Post).Hashtags != "#hashtag" {
- t.Fatal("hashtag didn't match")
- }
-
- post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id}
- rpost2, err := Client.CreateValetPost(post2)
- if err != nil {
- t.Fatal(err)
- }
-
- post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id, ParentId: rpost2.Data.(*model.Post).Id}
- _, err = Client.CreateValetPost(post3)
- if err != nil {
- t.Fatal(err)
- }
-
- post4 := &model.Post{ChannelId: "junk", Message: "a" + model.NewId() + "a"}
- _, err = Client.CreateValetPost(post4)
- if err.StatusCode != http.StatusForbidden {
- t.Fatal("Should have been forbidden")
- }
-
- Client.LoginByEmail(team.Name, user2.Email, "pwd")
- post5 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
- _, err = Client.CreateValetPost(post5)
- if err != nil {
- t.Fatal(err)
- }
-
- user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
- user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user3.Id))
-
- Client.LoginByEmail(team2.Name, user3.Email, "pwd")
-
- channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id}
- channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel)
-
- post6 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
- _, err = Client.CreateValetPost(post6)
- if err.StatusCode != http.StatusForbidden {
- t.Fatal("Should have been forbidden")
- }
-
- if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
- t.Fatal("should have been an error")
- }
- } else {
- post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"}
- _, err := Client.CreateValetPost(post1)
- if err.StatusCode != http.StatusNotImplemented {
- t.Fatal("Should have failed with 501 - Not Implemented")
- }
- }
-}
-
func TestUpdatePost(t *testing.T) {
Setup()
diff --git a/api/server.go b/api/server.go
index 3273e766c..3f23d8df6 100644
--- a/api/server.go
+++ b/api/server.go
@@ -38,11 +38,11 @@ func NewServer() {
func StartServer() {
l4g.Info("Starting Server...")
- l4g.Info("Server is listening on " + utils.Cfg.ServiceSettings.Port)
+ l4g.Info("Server is listening on " + utils.Cfg.ServiceSettings.ListenAddress)
var handler http.Handler = Srv.Router
- if utils.Cfg.RateLimitSettings.UseRateLimiter {
+ if utils.Cfg.RateLimitSettings.EnableRateLimiter {
l4g.Info("RateLimiter is enabled")
vary := throttled.VaryBy{}
@@ -71,7 +71,7 @@ func StartServer() {
}
go func() {
- err := Srv.Server.ListenAndServe(":"+utils.Cfg.ServiceSettings.Port, handler)
+ err := Srv.Server.ListenAndServe(utils.Cfg.ServiceSettings.ListenAddress, handler)
if err != nil {
l4g.Critical("Error starting server, err:%v", err)
time.Sleep(time.Second)
diff --git a/api/team.go b/api/team.go
index 92fcbff93..d59b2b484 100644
--- a/api/team.go
+++ b/api/team.go
@@ -25,6 +25,7 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
+ sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
@@ -38,7 +39,7 @@ func InitTeam(r *mux.Router) {
}
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
- if utils.Cfg.ServiceSettings.DisableEmailSignUp {
+ if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
@@ -60,14 +61,13 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
subjectPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage := NewServerTemplatePage("signup_team_body")
bodyPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink
props := make(map[string]string)
props["email"] = email
props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_team_complete/?d=%s&h=%s", c.GetSiteURL(), url.QueryEscape(data), url.QueryEscape(hash))
@@ -76,10 +76,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV || utils.Cfg.EmailSettings.ByPassEmail {
- m["follow_link"] = bodyPage.Props["Link"]
- }
-
+ m["follow_link"] = bodyPage.Props["Link"]
w.Header().Set("Access-Control-Allow-Origin", " *")
w.Write([]byte(model.MapToJson(m)))
}
@@ -88,7 +85,8 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
- if !utils.IsServiceAllowed(service) {
+ sso := utils.Cfg.GetSSOService(service)
+ if sso != nil && !sso.Enable {
c.SetInvalidParam("createTeamFromSSO", "service")
return
}
@@ -126,8 +124,6 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault
-
if result := <-Srv.Store.Team().Save(team); result.Err != nil {
c.Err = result.Err
return
@@ -147,7 +143,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
}
func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
- if utils.Cfg.ServiceSettings.DisableEmailSignUp {
+ if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
@@ -188,7 +184,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
teamSignup.User.TeamId = ""
teamSignup.User.Password = password
- if !model.ComparePassword(teamSignup.Hash, fmt.Sprintf("%v:%v", teamSignup.Data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ if !model.ComparePassword(teamSignup.Hash, fmt.Sprintf("%v:%v", teamSignup.Data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewAppError("createTeamFromSignup", "The signup link does not appear to be valid", "")
return
}
@@ -209,8 +205,6 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- teamSignup.Team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault
-
if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil {
c.Err = result.Err
return
@@ -230,13 +224,6 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if teamSignup.Team.AllowValet {
- CreateValet(c, rteam)
- if c.Err != nil {
- return
- }
- }
-
InviteMembers(c, rteam, ruser, teamSignup.Invites)
teamSignup.Team = *rteam
@@ -257,7 +244,7 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
func CreateTeam(c *Context, team *model.Team) *model.Team {
- if utils.Cfg.ServiceSettings.DisableEmailSignUp {
+ if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented
return nil
@@ -272,11 +259,6 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
return nil
}
- if utils.Cfg.ServiceSettings.Mode != utils.MODE_DEV {
- c.Err = model.NewAppError("CreateTeam", "The mode does not allow network creation without a valid invite", "")
- return nil
- }
-
if result := <-Srv.Store.Team().Save(team); result.Err != nil {
c.Err = result.Err
return nil
@@ -288,13 +270,6 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
return nil
}
- if rteam.AllowValet {
- CreateValet(c, rteam)
- if c.Err != nil {
- return nil
- }
- }
-
return rteam
}
}
@@ -303,7 +278,7 @@ func isTreamCreationAllowed(c *Context, email string) bool {
email = strings.ToLower(email)
- if utils.Cfg.TeamSettings.DisableTeamCreation {
+ if !utils.Cfg.TeamSettings.EnableTeamCreation {
c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
return false
}
@@ -328,6 +303,53 @@ func isTreamCreationAllowed(c *Context, email string) bool {
return true
}
+func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getLogs") {
+ return
+ }
+
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teams := result.Data.([]*model.Team)
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
+ }
+
+ w.Write([]byte(model.TeamMapToJson(m)))
+ }
+}
+
+func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ id := props["id"]
+
+ if result := <-Srv.Store.Session().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ session := result.Data.(*model.Session)
+
+ c.LogAudit("revoked_all=" + id)
+
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ sessionCache.Remove(session.Token)
+
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(model.MapToJson(props)))
+ return
+ }
+ }
+ }
+}
+
func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
@@ -509,10 +531,10 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
props["name"] = team.Name
props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", c.GetSiteURL(), url.QueryEscape(data), url.QueryEscape(hash))
- if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV {
+ if !utils.Cfg.EmailSettings.SendEmailNotifications {
l4g.Info("sending invitation to %v %v", invite, bodyPage.Props["Link"])
}
@@ -569,8 +591,6 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- allowValet := allowValetStr == "true"
-
teamId := props["team_id"]
if len(teamId) > 0 && len(teamId) != 26 {
c.SetInvalidParam("updateValetFeature", "team_id")
@@ -599,8 +619,6 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
team = tResult.Data.(*model.Team)
}
- team.AllowValet = allowValet
-
if result := <-Srv.Store.Team().Update(team); result.Err != nil {
c.Err = result.Err
return
diff --git a/api/team_test.go b/api/team_test.go
index 4f1b9e5f0..e2a7cf430 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -30,7 +30,7 @@ func TestCreateFromSignupTeam(t *testing.T) {
props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
user := model.User{Email: props["email"], Nickname: "Corey Hulen", Password: "hello"}
@@ -132,6 +132,39 @@ func TestFindTeamByEmail(t *testing.T) {
}
}
+func TestGetAllTeams(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() + "corey@test.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.GetAllTeams(); err == nil {
+ t.Fatal("you 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 r1, err := Client.GetAllTeams(); err != nil {
+ t.Fatal(err)
+ } else {
+ teams := r1.Data.(map[string]*model.Team)
+ if teams[team.Id].Name != team.Name {
+ t.Fatal()
+ }
+ }
+}
+
/*
XXXXXX investigate and fix failing test
@@ -330,79 +363,3 @@ func TestGetMyTeam(t *testing.T) {
}
}
}
-
-func TestUpdateValetFeature(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: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
-
- user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
- user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
-
- team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
-
- user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
- user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user3.Id))
-
- Client.LoginByEmail(team.Name, user2.Email, "pwd")
-
- data := make(map[string]string)
- data["allow_valet"] = "true"
- if _, err := Client.UpdateValetFeature(data); err == nil {
- t.Fatal("Should have errored, not admin")
- }
-
- Client.LoginByEmail(team.Name, user.Email, "pwd")
-
- data["allow_valet"] = ""
- if _, err := Client.UpdateValetFeature(data); err == nil {
- t.Fatal("Should have errored, empty allow_valet field")
- }
-
- data["allow_valet"] = "true"
- if _, err := Client.UpdateValetFeature(data); err != nil {
- t.Fatal(err)
- }
-
- rteam := Client.Must(Client.GetMyTeam("")).Data.(*model.Team)
- if rteam.AllowValet != true {
- t.Fatal("Should have errored - allow valet property not updated")
- }
-
- data["team_id"] = "junk"
- if _, err := Client.UpdateValetFeature(data); err == nil {
- t.Fatal("Should have errored, junk team id")
- }
-
- data["team_id"] = "12345678901234567890123456"
- if _, err := Client.UpdateValetFeature(data); err == nil {
- t.Fatal("Should have errored, bad team id")
- }
-
- data["team_id"] = team.Id
- data["allow_valet"] = "false"
- if _, err := Client.UpdateValetFeature(data); err != nil {
- t.Fatal(err)
- }
-
- rteam = Client.Must(Client.GetMyTeam("")).Data.(*model.Team)
- if rteam.AllowValet != false {
- t.Fatal("Should have errored - allow valet property not updated")
- }
-
- Client.LoginByEmail(team2.Name, user3.Email, "pwd")
-
- data["team_id"] = team.Id
- data["allow_valet"] = "true"
- if _, err := Client.UpdateValetFeature(data); err == nil {
- t.Fatal("Should have errored, not part of team")
- }
-}
diff --git a/api/user.go b/api/user.go
index 2740696e1..3cce3cdd3 100644
--- a/api/user.go
+++ b/api/user.go
@@ -51,6 +51,7 @@ func InitUser(r *mux.Router) {
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
+ sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET")
@@ -58,7 +59,7 @@ func InitUser(r *mux.Router) {
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
- if utils.Cfg.ServiceSettings.DisableEmailSignUp {
+ if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
@@ -90,7 +91,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data))
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "")
return
}
@@ -155,23 +156,30 @@ func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool
return shouldVerifyHash
}
-func CreateValet(c *Context, team *model.Team) *model.User {
- valet := &model.User{}
- valet.TeamId = team.Id
- valet.Email = utils.Cfg.EmailSettings.FeedbackEmail
- valet.EmailVerified = true
- valet.Username = model.BOT_USERNAME
- valet.Password = model.NewId()
-
- return CreateUser(c, team, valet)
-}
-
func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
+ if !utils.Cfg.TeamSettings.EnableUserCreation {
+ c.Err = model.NewAppError("CreateUser", "User creation has been disabled. Please ask your systems administrator for details.", "")
+ return nil
+ }
+
channelRole := ""
if team.Email == user.Email {
user.Roles = model.ROLE_TEAM_ADMIN
channelRole = model.CHANNEL_ROLE_ADMIN
+
+ // Below is a speical case where the first user in the entire
+ // system is granted the system_admin role instead of admin
+ if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ user.Roles = model.ROLE_SYSTEM_ADMIN
+ }
+ }
+
} else {
user.Roles = ""
}
@@ -290,7 +298,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
func checkUserPassword(c *Context, user *model.User, password string) bool {
- if user.FailedAttempts >= utils.Cfg.ServiceSettings.AllowedLoginAttempts {
+ if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts {
c.LogAuditWithUserId(user.Id, "fail")
c.Err = model.NewAppError("checkUserPassword", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden
@@ -321,7 +329,7 @@ func checkUserPassword(c *Context, user *model.User, password string) bool {
func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
c.LogAuditWithUserId(user.Id, "attempt")
- if !user.EmailVerified && !utils.Cfg.EmailSettings.ByPassEmail {
+ if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification {
c.Err = model.NewAppError("Login", "Login failed because email address has not been verified", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden
return
@@ -556,13 +564,26 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id, ok := params["id"]
+ if ok {
+ // You must be system admin to access another team
+ if id != c.Session.TeamId {
+ if !c.HasSystemAdminPermissions("getProfiles") {
+ return
+ }
+ }
+
+ } else {
+ id = c.Session.TeamId
+ }
- etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string)
+ etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string)
if HandleEtag(etag, w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -652,7 +673,7 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError
initial := string(strings.ToUpper(username)[0])
- fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.ImageSettings.InitialFont)
+ fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.FileSettings.InitialFont)
if err != nil {
return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error())
}
@@ -661,8 +682,8 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError
return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error())
}
- width := int(utils.Cfg.ImageSettings.ProfileWidth)
- height := int(utils.Cfg.ImageSettings.ProfileHeight)
+ width := int(utils.Cfg.FileSettings.ProfileWidth)
+ height := int(utils.Cfg.FileSettings.ProfileHeight)
color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
srcImg := image.White
@@ -701,7 +722,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
var img []byte
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
var err *model.AppError
if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil {
c.Err = err
@@ -738,8 +759,8 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
}
func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
- c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "")
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -781,7 +802,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
}
// Scale profile image
- img = resize.Resize(utils.Cfg.ImageSettings.ProfileWidth, utils.Cfg.ImageSettings.ProfileHeight, img, resize.Lanczos3)
+ img = resize.Resize(utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, img, resize.Lanczos3)
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
@@ -930,8 +951,8 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) {
- c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set from the command line", "")
+ if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set by another system admin", "")
c.Err.StatusCode = http.StatusForbidden
return
}
@@ -1132,7 +1153,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt))
link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
@@ -1161,29 +1182,35 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := props["hash"]
- if len(hash) == 0 {
- c.SetInvalidParam("resetPassword", "hash")
+ name := props["name"]
+ if len(name) == 0 {
+ c.SetInvalidParam("resetPassword", "name")
return
}
- data := model.MapFromJson(strings.NewReader(props["data"]))
+ userId := props["user_id"]
+ hash := props["hash"]
+ timeStr := ""
- userId := data["user_id"]
- if len(userId) != 26 {
- c.SetInvalidParam("resetPassword", "data:user_id")
- return
- }
+ if !c.IsSystemAdmin() {
+ if len(hash) == 0 {
+ c.SetInvalidParam("resetPassword", "hash")
+ return
+ }
- timeStr := data["time"]
- if len(timeStr) == 0 {
- c.SetInvalidParam("resetPassword", "data:time")
- return
+ data := model.MapFromJson(strings.NewReader(props["data"]))
+
+ userId = data["user_id"]
+
+ timeStr = data["time"]
+ if len(timeStr) == 0 {
+ c.SetInvalidParam("resetPassword", "data:time")
+ return
+ }
}
- name := props["name"]
- if len(name) == 0 {
- c.SetInvalidParam("resetPassword", "name")
+ if len(userId) != 26 {
+ c.SetInvalidParam("resetPassword", "user_id")
return
}
@@ -1211,15 +1238,17 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.ServiceSettings.ResetSalt)) {
- c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
- return
- }
+ if !c.IsSystemAdmin() {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
+ c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
+ return
+ }
- t, err := strconv.ParseInt(timeStr, 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
- return
+ t, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
+ c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
+ return
+ }
}
if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
@@ -1359,15 +1388,16 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri, loginHint string) {
- if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
+ sso := utils.Cfg.GetSSOService(service)
+ if sso != nil && !sso.Enable {
c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
c.Err.StatusCode = http.StatusBadRequest
return
}
- clientId := utils.Cfg.SSOSettings[service].Id
- endpoint := utils.Cfg.SSOSettings[service].AuthEndpoint
- scope := utils.Cfg.SSOSettings[service].Scope
+ clientId := sso.Id
+ endpoint := sso.AuthEndpoint
+ scope := sso.Scope
stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)}
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
@@ -1386,7 +1416,8 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te
}
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) {
- if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
+ sso := utils.Cfg.GetSSOService(service)
+ if sso != nil && !sso.Enable {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
}
@@ -1399,7 +1430,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
stateProps := model.MapFromJson(strings.NewReader(stateStr))
- if !model.ComparePassword(stateProps["hash"], utils.Cfg.SSOSettings[service].Id) {
+ if !model.ComparePassword(stateProps["hash"], sso.Id) {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
}
@@ -1411,14 +1442,14 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
tchan := Srv.Store.Team().GetByName(teamName)
p := url.Values{}
- p.Set("client_id", utils.Cfg.SSOSettings[service].Id)
- p.Set("client_secret", utils.Cfg.SSOSettings[service].Secret)
+ p.Set("client_id", sso.Id)
+ p.Set("client_secret", sso.Secret)
p.Set("code", code)
p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
p.Set("redirect_uri", redirectUri)
client := &http.Client{}
- req, _ := http.NewRequest("POST", utils.Cfg.SSOSettings[service].TokenEndpoint, strings.NewReader(p.Encode()))
+ req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
@@ -1440,7 +1471,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
p = url.Values{}
p.Set("access_token", ar.AccessToken)
- req, _ = http.NewRequest("GET", utils.Cfg.SSOSettings[service].UserApiEndpoint, strings.NewReader(""))
+ req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader(""))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
diff --git a/api/user_test.go b/api/user_test.go
index 986365bd0..baa567dec 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -151,7 +151,7 @@ func TestLogin(t *testing.T) {
props["display_name"] = rteam2.Data.(*model.Team).DisplayName
props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt))
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash)
@@ -228,6 +228,13 @@ func TestGetUser(t *testing.T) {
ruser2, _ := Client.CreateUser(&user2, "")
store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam2, _ := Client.CreateTeam(&team2)
+
+ user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser3, _ := Client.CreateUser(&user3, "")
+ store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
+
Client.LoginByEmail(team.Name, user.Email, user.Password)
rId := ruser.Data.(*model.User).Id
@@ -276,13 +283,27 @@ func TestGetUser(t *testing.T) {
t.Log(cache_result.Data)
t.Fatal("cache should be empty")
}
+ }
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil {
+ t.Fatal("shouldn't have access")
}
Client.AuthToken = ""
if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil {
t.Fatal("shouldn't have accss")
}
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil {
+ t.Fatal(err)
+ }
}
func TestGetAudits(t *testing.T) {
@@ -352,19 +373,19 @@ func TestUserCreateImage(t *testing.T) {
Client.DoApiGet("/users/"+user.Id+"/image", "", "")
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil {
t.Fatal(err)
}
} else {
- path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
+ path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
@@ -382,7 +403,7 @@ func TestUserUploadProfileImage(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
- if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -450,19 +471,19 @@ func TestUserUploadProfileImage(t *testing.T) {
Client.DoApiGet("/users/"+user.Id+"/image", "", "")
- if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth
- auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
- auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
- bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+ s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil {
t.Fatal(err)
}
} else {
- path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
+ path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path)
}
@@ -814,7 +835,7 @@ func TestResetPassword(t *testing.T) {
props["user_id"] = user.Id
props["time"] = fmt.Sprintf("%v", model.GetMillis())
data["data"] = model.MapToJson(props)
- data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.ServiceSettings.ResetSalt))
+ data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.EmailSettings.PasswordResetSalt))
data["name"] = team.Name
if _, err := Client.ResetPassword(data); err != nil {
@@ -952,6 +973,7 @@ func TestUserUpdateNotify(t *testing.T) {
}
func TestFuzzyUserCreate(t *testing.T) {
+ Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
diff --git a/api/web_socket_test.go b/api/web_socket_test.go
index 7f9ce024b..49a4c6870 100644
--- a/api/web_socket_test.go
+++ b/api/web_socket_test.go
@@ -16,7 +16,7 @@ import (
func TestSocket(t *testing.T) {
Setup()
- url := "ws://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1/websocket"
+ url := "ws://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/websocket"
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)
diff --git a/api/webhook.go b/api/webhook.go
index 9ca725d45..b67655ff5 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -21,7 +21,7 @@ func InitWebhook(r *mux.Router) {
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
@@ -66,7 +66,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
@@ -103,7 +103,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
diff --git a/api/webhook_test.go b/api/webhook_test.go
index fd4c723b7..22883f8ca 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -31,7 +31,7 @@ func TestCreateIncomingHook(t *testing.T) {
hook := &model.IncomingWebhook{ChannelId: channel1.Id}
- if utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
var rhook *model.IncomingWebhook
if result, err := Client.CreateIncomingWebhook(hook); err != nil {
t.Fatal(err)
@@ -89,7 +89,7 @@ func TestListIncomingHooks(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- if utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook1 := &model.IncomingWebhook{ChannelId: channel1.Id}
hook1 = Client.Must(Client.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook)
@@ -127,7 +127,7 @@ func TestDeleteIncomingHook(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- if utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook := &model.IncomingWebhook{ChannelId: channel1.Id}
hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook)
diff --git a/config/config.json b/config/config.json
index c573a299f..aa92ccf4e 100644
--- a/config/config.json
+++ b/config/config.json
@@ -1,71 +1,74 @@
{
- "LogSettings": {
- "ConsoleEnable": true,
- "ConsoleLevel": "DEBUG",
- "FileEnable": true,
- "FileLevel": "INFO",
- "FileFormat": "",
- "FileLocation": ""
- },
"ServiceSettings": {
- "SiteName": "Mattermost",
- "Mode": "dev",
- "AllowTesting": false,
- "UseSSL": false,
- "Port": "8065",
- "Version": "developer",
- "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
- "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
- "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
- "AnalyticsUrl": "",
- "UseLocalStorage": true,
- "StorageDirectory": "./data/",
- "AllowedLoginAttempts": 10,
- "DisableEmailSignUp": false,
+ "ListenAddress": ":8065",
+ "MaximumLoginAttempts": 10,
+ "SegmentDeveloperKey": "",
+ "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "AllowIncomingWebhooks": false
+ "EnableIncomingWebhooks": false,
+ "EnableTesting": false
+ },
+ "TeamSettings": {
+ "SiteName": "Mattermost",
+ "MaxUsersPerTeam": 50,
+ "DefaultThemeColor": "#2389D7",
+ "EnableTeamCreation": true,
+ "EnableUserCreation": true,
+ "RestrictCreationToDomains": ""
},
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
- "DataSourceReplicas": [
- "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"
- ],
+ "DataSourceReplicas": [],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
- "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS"
+ "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg"
},
- "AWSSettings": {
- "S3AccessKeyId": "",
- "S3SecretAccessKey": "",
- "S3Bucket": "",
- "S3Region": ""
+ "LogSettings": {
+ "EnableConsole": true,
+ "ConsoleLevel": "DEBUG",
+ "EnableFile": true,
+ "FileLevel": "INFO",
+ "FileFormat": "",
+ "FileLocation": ""
},
- "ImageSettings": {
+ "FileSettings": {
+ "DriverName": "local",
+ "Directory": "./data/",
+ "EnablePublicLink": true,
+ "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
"ThumbnailWidth": 120,
"ThumbnailHeight": 100,
"PreviewWidth": 1024,
"PreviewHeight": 0,
"ProfileWidth": 128,
"ProfileHeight": 128,
- "InitialFont": "luximbi.ttf"
+ "InitialFont": "luximbi.ttf",
+ "AmazonS3AccessKeyId": "",
+ "AmazonS3SecretAccessKey": "",
+ "AmazonS3Bucket": "",
+ "AmazonS3Region": ""
},
"EmailSettings": {
- "ByPassEmail": true,
+ "EnableSignUpWithEmail": true,
+ "SendEmailNotifications": false,
+ "RequireEmailVerification": false,
+ "FeedbackName": "",
+ "FeedbackEmail": "",
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
- "UseTLS": false,
- "UseStartTLS": false,
- "FeedbackEmail": "",
- "FeedbackName": "",
+ "SMTPPort": "",
+ "ConnectionSecurity": "",
+ "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
+ "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
"ApplePushServer": "",
"ApplePushCertPublic": "",
"ApplePushCertPrivate": ""
},
"RateLimitSettings": {
- "UseRateLimiter": true,
+ "EnableRateLimiter": true,
"PerSec": 10,
"MemoryStoreSize": 10000,
"VaryByRemoteAddr": true,
@@ -73,32 +76,15 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowPhoneNumber": true,
- "ShowSkypeId": true,
"ShowFullName": true
},
- "ClientSettings": {
- "SegmentDeveloperKey": "",
- "GoogleDeveloperKey": ""
- },
- "TeamSettings": {
- "MaxUsersPerTeam": 150,
- "AllowPublicLink": true,
- "AllowValetDefault": false,
- "TourLink": "",
- "DefaultThemeColor": "#2389D7",
- "DisableTeamCreation": false,
- "RestrictCreationToDomains": ""
- },
- "SSOSettings": {
- "gitlab": {
- "Allow": false,
- "Secret": "",
- "Id": "",
- "Scope": "",
- "AuthEndpoint": "",
- "TokenEndpoint": "",
- "UserApiEndpoint": ""
- }
+ "GitLabSettings": {
+ "Enable": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
}
} \ No newline at end of file
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index d50439f2c..16a4007fa 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -1,82 +1,74 @@
{
- "LogSettings": {
- "ConsoleEnable": true,
- "ConsoleLevel": "INFO",
- "FileEnable": true,
- "FileLevel": "INFO",
- "FileFormat": "",
- "FileLocation": ""
- },
"ServiceSettings": {
- "SiteName": "Mattermost",
- "Mode" : "dev",
- "AllowTesting" : true,
- "UseSSL": false,
- "Port": "80",
- "Version": "developer",
- "Shards": {
- },
- "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
- "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
- "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
- "AnalyticsUrl": "",
- "UseLocalStorage": true,
- "StorageDirectory": "/mattermost/data/",
- "AllowedLoginAttempts": 10,
- "DisableEmailSignUp": false,
+ "ListenAddress": ":80",
+ "MaximumLoginAttempts": 10,
+ "SegmentDeveloperKey": "",
+ "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "AllowIncomingWebhooks": false
+ "EnableIncomingWebhooks": false,
+ "EnableTesting": false
},
- "SSOSettings": {
- "gitlab": {
- "Allow": false,
- "Secret" : "",
- "Id": "",
- "Scope": "",
- "AuthEndpoint": "",
- "TokenEndpoint": "",
- "UserApiEndpoint": ""
- }
+ "TeamSettings": {
+ "SiteName": "Mattermost",
+ "MaxUsersPerTeam": 50,
+ "DefaultThemeColor": "#2389D7",
+ "EnableTeamCreation": true,
+ "EnableUserCreation": true,
+ "RestrictCreationToDomains": ""
},
"SqlSettings": {
"DriverName": "mysql",
- "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8",
- "DataSourceReplicas": ["mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
+ "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
+ "DataSourceReplicas": [],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
- "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS"
+ "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"
},
- "AWSSettings": {
- "S3AccessKeyId": "",
- "S3SecretAccessKey": "",
- "S3Bucket": "",
- "S3Region": ""
+ "LogSettings": {
+ "EnableConsole": false,
+ "ConsoleLevel": "INFO",
+ "EnableFile": true,
+ "FileLevel": "INFO",
+ "FileFormat": "",
+ "FileLocation": ""
},
- "ImageSettings": {
+ "FileSettings": {
+ "DriverName": "local",
+ "Directory": "/mattermost/data/",
+ "EnablePublicLink": true,
+ "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX",
"ThumbnailWidth": 120,
"ThumbnailHeight": 100,
"PreviewWidth": 1024,
"PreviewHeight": 0,
"ProfileWidth": 128,
"ProfileHeight": 128,
- "InitialFont": "luximbi.ttf"
+ "InitialFont": "luximbi.ttf",
+ "AmazonS3AccessKeyId": "",
+ "AmazonS3SecretAccessKey": "",
+ "AmazonS3Bucket": "",
+ "AmazonS3Region": ""
},
"EmailSettings": {
- "ByPassEmail" : true,
+ "EnableSignUpWithEmail": true,
+ "SendEmailNotifications": false,
+ "RequireEmailVerification": false,
+ "FeedbackName": "",
+ "FeedbackEmail": "",
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
- "UseTLS": false,
- "UseStartTLS": false,
- "FeedbackEmail": "",
- "FeedbackName": "",
+ "SMTPPort": "",
+ "ConnectionSecurity": "",
+ "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo",
+ "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e",
"ApplePushServer": "",
"ApplePushCertPublic": "",
"ApplePushCertPrivate": ""
},
"RateLimitSettings": {
- "UseRateLimiter": true,
+ "EnableRateLimiter": true,
"PerSec": 10,
"MemoryStoreSize": 10000,
"VaryByRemoteAddr": true,
@@ -84,22 +76,15 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowPhoneNumber": true,
- "ShowSkypeId": true,
"ShowFullName": true
},
- "TeamSettings": {
- "MaxUsersPerTeam": 150,
- "AllowPublicLink": true,
- "AllowValetDefault": false,
- "TermsLink": "/static/help/configure_links.html",
- "PrivacyLink": "/static/help/configure_links.html",
- "AboutLink": "/static/help/configure_links.html",
- "HelpLink": "/static/help/configure_links.html",
- "ReportProblemLink": "/static/help/configure_links.html",
- "TourLink": "/static/help/configure_links.html",
- "DefaultThemeColor": "#2389D7",
- "DisableTeamCreation": false,
- "RestrictCreationToDomains": ""
+ "GitLabSettings": {
+ "Enable": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
}
-}
+} \ No newline at end of file
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index b6e02635c..16a4007fa 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -1,82 +1,74 @@
{
- "LogSettings": {
- "ConsoleEnable": true,
- "ConsoleLevel": "INFO",
- "FileEnable": true,
- "FileLevel": "INFO",
- "FileFormat": "",
- "FileLocation": ""
- },
"ServiceSettings": {
- "SiteName": "Mattermost",
- "Mode" : "dev",
- "AllowTesting" : true,
- "UseSSL": false,
- "Port": "80",
- "Version": "developer",
- "Shards": {
- },
- "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
- "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
- "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
- "AnalyticsUrl": "",
- "UseLocalStorage": true,
- "StorageDirectory": "/mattermost/data/",
- "AllowedLoginAttempts": 10,
- "DisableEmailSignUp": false,
+ "ListenAddress": ":80",
+ "MaximumLoginAttempts": 10,
+ "SegmentDeveloperKey": "",
+ "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "AllowIncomingWebhooks": false
+ "EnableIncomingWebhooks": false,
+ "EnableTesting": false
},
- "SSOSettings": {
- "gitlab": {
- "Allow": false,
- "Secret" : "",
- "Id": "",
- "Scope": "",
- "AuthEndpoint": "",
- "TokenEndpoint": "",
- "UserApiEndpoint": ""
- }
+ "TeamSettings": {
+ "SiteName": "Mattermost",
+ "MaxUsersPerTeam": 50,
+ "DefaultThemeColor": "#2389D7",
+ "EnableTeamCreation": true,
+ "EnableUserCreation": true,
+ "RestrictCreationToDomains": ""
},
"SqlSettings": {
"DriverName": "mysql",
- "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8",
- "DataSourceReplicas": ["mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
+ "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
+ "DataSourceReplicas": [],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
- "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS"
+ "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"
},
- "AWSSettings": {
- "S3AccessKeyId": "",
- "S3SecretAccessKey": "",
- "S3Bucket": "",
- "S3Region": ""
+ "LogSettings": {
+ "EnableConsole": false,
+ "ConsoleLevel": "INFO",
+ "EnableFile": true,
+ "FileLevel": "INFO",
+ "FileFormat": "",
+ "FileLocation": ""
},
- "ImageSettings": {
+ "FileSettings": {
+ "DriverName": "local",
+ "Directory": "/mattermost/data/",
+ "EnablePublicLink": true,
+ "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX",
"ThumbnailWidth": 120,
"ThumbnailHeight": 100,
"PreviewWidth": 1024,
"PreviewHeight": 0,
"ProfileWidth": 128,
"ProfileHeight": 128,
- "InitialFont": "luximbi.ttf"
+ "InitialFont": "luximbi.ttf",
+ "AmazonS3AccessKeyId": "",
+ "AmazonS3SecretAccessKey": "",
+ "AmazonS3Bucket": "",
+ "AmazonS3Region": ""
},
"EmailSettings": {
- "ByPassEmail" : true,
+ "EnableSignUpWithEmail": true,
+ "SendEmailNotifications": false,
+ "RequireEmailVerification": false,
+ "FeedbackName": "",
+ "FeedbackEmail": "",
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
- "UseTLS": false,
- "UseStartTLS": false,
- "FeedbackEmail": "",
- "FeedbackName": "",
+ "SMTPPort": "",
+ "ConnectionSecurity": "",
+ "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo",
+ "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e",
"ApplePushServer": "",
"ApplePushCertPublic": "",
"ApplePushCertPrivate": ""
},
"RateLimitSettings": {
- "UseRateLimiter": true,
+ "EnableRateLimiter": true,
"PerSec": 10,
"MemoryStoreSize": 10000,
"VaryByRemoteAddr": true,
@@ -84,22 +76,15 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowPhoneNumber": true,
- "ShowSkypeId": true,
"ShowFullName": true
},
- "TeamSettings": {
- "MaxUsersPerTeam": 150,
- "AllowPublicLink": true,
- "AllowValetDefault": false,
- "TermsLink": "/static/help/configure_links.html",
- "PrivacyLink": "/static/help/configure_links.html",
- "AboutLink": "/static/help/configure_links.html",
- "HelpLink": "/static/help/configure_links.html",
- "ReportProblemLink": "/static/help/configure_links.html",
- "TourLink": "/static/help/configure_links.html",
- "DefaultThemeColor": "#2389D7",
- "DisableTeamCreation": false,
- "RestrictCreationToDomains": ""
+ "GitLabSettings": {
+ "Enable": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
}
-}
+} \ No newline at end of file
diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go
index 86b173c6a..a517b0c0e 100644
--- a/manualtesting/manual_testing.go
+++ b/manualtesting/manual_testing.go
@@ -53,7 +53,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
// Create a client for tests to use
- client := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
+ client := model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
// Check for username parameter and create a user if present
username, ok1 := params["username"]
diff --git a/mattermost.go b/mattermost.go
index f54bcf15f..4608bfff3 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -57,7 +57,7 @@ func main() {
api.StartServer()
// If we allow testing then listen for manual testing URL hits
- if utils.Cfg.ServiceSettings.AllowTesting {
+ if utils.Cfg.ServiceSettings.EnableTesting {
manualtesting.InitManualTesting()
}
diff --git a/model/client.go b/model/client.go
index 6817a80f6..ca17da6d2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -21,6 +21,7 @@ const (
HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For"
+ HEADER_REAL_IP = "X-Real-IP"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token"
HEADER_BEARER = "BEARER"
@@ -149,6 +150,16 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
}
}
+func (c *Client) GetAllTeams() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/teams/all", "", ""); err != nil {
+ return nil, err
+ } else {
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@@ -253,7 +264,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil {
+ if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -403,6 +414,15 @@ func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
}
}
+func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/admin/test_email", config.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
diff --git a/model/config.go b/model/config.go
index 436f063c8..69f2127b2 100644
--- a/model/config.go
+++ b/model/config.go
@@ -8,27 +8,29 @@ import (
"io"
)
+const (
+ CONN_SECURITY_NONE = ""
+ CONN_SECURITY_TLS = "TLS"
+ CONN_SECURITY_STARTTLS = "STARTTLS"
+
+ IMAGE_DRIVER_LOCAL = "local"
+ IMAGE_DRIVER_S3 = "amazons3"
+
+ SERVICE_GITLAB = "gitlab"
+)
+
type ServiceSettings struct {
- SiteName string
- Mode string
- AllowTesting bool
- UseSSL bool
- Port string
- Version string
- InviteSalt string
- PublicLinkSalt string
- ResetSalt string
- AnalyticsUrl string
- UseLocalStorage bool
- StorageDirectory string
- AllowedLoginAttempts int
- DisableEmailSignUp bool
+ ListenAddress string
+ MaximumLoginAttempts int
+ SegmentDeveloperKey string
+ GoogleDeveloperKey string
EnableOAuthServiceProvider bool
- AllowIncomingWebhooks bool
+ EnableIncomingWebhooks bool
+ EnableTesting bool
}
-type SSOSetting struct {
- Allow bool
+type SSOSettings struct {
+ Enable bool
Secret string
Id string
Scope string
@@ -48,87 +50,84 @@ type SqlSettings struct {
}
type LogSettings struct {
- ConsoleEnable bool
+ EnableConsole bool
ConsoleLevel string
- FileEnable bool
+ EnableFile bool
FileLevel string
FileFormat string
FileLocation string
}
-type AWSSettings struct {
- S3AccessKeyId string
- S3SecretAccessKey string
- S3Bucket string
- S3Region string
-}
-
-type ImageSettings struct {
- ThumbnailWidth uint
- ThumbnailHeight uint
- PreviewWidth uint
- PreviewHeight uint
- ProfileWidth uint
- ProfileHeight uint
- InitialFont string
+type FileSettings struct {
+ DriverName string
+ Directory string
+ EnablePublicLink bool
+ PublicLinkSalt string
+ ThumbnailWidth uint
+ ThumbnailHeight uint
+ PreviewWidth uint
+ PreviewHeight uint
+ ProfileWidth uint
+ ProfileHeight uint
+ InitialFont string
+ AmazonS3AccessKeyId string
+ AmazonS3SecretAccessKey string
+ AmazonS3Bucket string
+ AmazonS3Region string
}
type EmailSettings struct {
- ByPassEmail bool
- SMTPUsername string
- SMTPPassword string
- SMTPServer string
- UseTLS bool
- UseStartTLS bool
- FeedbackEmail string
- FeedbackName string
+ EnableSignUpWithEmail bool
+ SendEmailNotifications bool
+ RequireEmailVerification bool
+ FeedbackName string
+ FeedbackEmail string
+ SMTPUsername string
+ SMTPPassword string
+ SMTPServer string
+ SMTPPort string
+ ConnectionSecurity string
+ InviteSalt string
+ PasswordResetSalt string
+
+ // For Future Use
ApplePushServer string
ApplePushCertPublic string
ApplePushCertPrivate string
}
type RateLimitSettings struct {
- UseRateLimiter bool
- PerSec int
- MemoryStoreSize int
- VaryByRemoteAddr bool
- VaryByHeader string
+ EnableRateLimiter bool
+ PerSec int
+ MemoryStoreSize int
+ VaryByRemoteAddr bool
+ VaryByHeader string
}
type PrivacySettings struct {
ShowEmailAddress bool
- ShowPhoneNumber bool
- ShowSkypeId bool
ShowFullName bool
}
-type ClientSettings struct {
- SegmentDeveloperKey string
- GoogleDeveloperKey string
-}
-
type TeamSettings struct {
+ SiteName string
MaxUsersPerTeam int
- AllowPublicLink bool
- AllowValetDefault bool
- TourLink string
DefaultThemeColor string
- DisableTeamCreation bool
+ EnableTeamCreation bool
+ EnableUserCreation bool
RestrictCreationToDomains string
}
type Config struct {
- LogSettings LogSettings
ServiceSettings ServiceSettings
+ TeamSettings TeamSettings
SqlSettings SqlSettings
- AWSSettings AWSSettings
- ImageSettings ImageSettings
+ LogSettings LogSettings
+ FileSettings FileSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
- ClientSettings ClientSettings
- TeamSettings TeamSettings
- SSOSettings map[string]SSOSetting
+ GitLabSettings SSOSettings
}
func (o *Config) ToJson() string {
@@ -140,6 +139,14 @@ func (o *Config) ToJson() string {
}
}
+func (o *Config) GetSSOService(service string) *SSOSettings {
+ if service == SERVICE_GITLAB {
+ return &o.GitLabSettings
+ }
+
+ return nil
+}
+
func ConfigFromJson(data io.Reader) *Config {
decoder := json.NewDecoder(data)
var o Config
diff --git a/model/team.go b/model/team.go
index 8b4f82830..f80fa3b11 100644
--- a/model/team.go
+++ b/model/team.go
@@ -27,7 +27,6 @@ type Team struct {
Type string `json:"type"`
CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"`
- AllowValet bool `json:"allow_valet"`
}
type Invites struct {
@@ -74,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team {
}
}
+func TeamMapToJson(u map[string]*Team) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamMapFromJson(data io.Reader) map[string]*Team {
+ decoder := json.NewDecoder(data)
+ var teams map[string]*Team
+ err := decoder.Decode(&teams)
+ if err == nil {
+ return teams
+ } else {
+ return nil
+ }
+}
+
func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index c1d2c852b..22411389d 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -140,6 +140,24 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel {
return storeChannel
}
+func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForTeam", "We couldn't remove all the sessions for the team", "id="+teamId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 4ae680556..3d8aafe25 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -80,6 +80,29 @@ func TestSessionRemove(t *testing.T) {
}
}
+func TestSessionRemoveAll(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId))
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
func TestSessionRemoveToken(t *testing.T) {
Setup()
diff --git a/store/sql_store.go b/store/sql_store.go
index 98703841a..6dcf2e8cd 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -51,11 +51,18 @@ func NewSqlStore() Store {
utils.Cfg.SqlSettings.DataSource, utils.Cfg.SqlSettings.MaxIdleConns,
utils.Cfg.SqlSettings.MaxOpenConns, utils.Cfg.SqlSettings.Trace)
- sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas))
- for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas {
- sqlStore.replicas[i] = setupConnection(fmt.Sprintf("replica-%v", i), utils.Cfg.SqlSettings.DriverName, replica,
+ if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
+ sqlStore.replicas = make([]*gorp.DbMap, 1)
+ sqlStore.replicas[0] = setupConnection(fmt.Sprintf("replica-%v", 0), utils.Cfg.SqlSettings.DriverName, utils.Cfg.SqlSettings.DataSource,
utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
utils.Cfg.SqlSettings.Trace)
+ } else {
+ sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas))
+ for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas {
+ sqlStore.replicas[i] = setupConnection(fmt.Sprintf("replica-%v", i), utils.Cfg.SqlSettings.DriverName, replica,
+ utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
+ utils.Cfg.SqlSettings.Trace)
+ }
}
schemaVersion := sqlStore.GetCurrentSchemaVersion()
@@ -308,26 +315,21 @@ func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
}
}
-// func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool {
+func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool {
-// // XXX TODO FIXME this should be removed after 0.6.0
-// if utils.Cfg.SqlSettings.DriverName == "postgres" {
-// return false
-// }
-
-// if !ss.DoesColumnExist(tableName, columnName) {
-// return false
-// }
+ if !ss.DoesColumnExist(tableName, columnName) {
+ return false
+ }
-// _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName)
-// if err != nil {
-// l4g.Critical("Failed to drop column %v", err)
-// time.Sleep(time.Second)
-// panic("Failed to drop column " + err.Error())
-// }
+ _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName)
+ if err != nil {
+ l4g.Critical("Failed to drop column %v", err)
+ time.Sleep(time.Second)
+ panic("Failed to drop column " + err.Error())
+ }
-// return true
-// }
+ return true
+}
// func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool {
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index d2148c2e3..109fe5401 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -28,6 +28,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
}
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
+ s.RemoveColumnIfExists("Teams", "AllowValet")
}
func (s SqlTeamStore) CreateIndexesIfNotExists() {
@@ -195,7 +196,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
return storeChannel
}
-func (s SqlTeamStore) GetForExport() StoreChannel {
+func (s SqlTeamStore) GetAll() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -203,7 +204,7 @@ func (s SqlTeamStore) GetForExport() StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error())
}
result.Data = data
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 3fd1c82b5..0a723d965 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -481,3 +481,22 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users"); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetTotalUsersCount", "We could not count the users", err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index ddd7e5bb8..e2a454023 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -42,7 +42,7 @@ func TestUserStoreSave(t *testing.T) {
t.Fatal("should be unique username")
}
- for i := 0; i < 150; i++ {
+ for i := 0; i < 50; i++ {
u1.Id = ""
u1.Email = model.NewId()
u1.Username = model.NewId()
@@ -206,6 +206,24 @@ func TestUserStoreGet(t *testing.T) {
}
}
+func TestUserCountt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if result := <-store.User().GetTotalUsersCount(); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ t.Fatal()
+ }
+ }
+}
+
func TestUserStoreGetProfiles(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index c9d40cfa5..23580f452 100644
--- a/store/store.go
+++ b/store/store.go
@@ -47,7 +47,7 @@ type TeamStore interface {
Get(id string) StoreChannel
GetByName(name string) StoreChannel
GetTeamsForEmail(domain string) StoreChannel
- GetForExport() StoreChannel
+ GetAll() StoreChannel
}
type ChannelStore interface {
@@ -103,6 +103,7 @@ type UserStore interface {
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
+ GetTotalUsersCount() StoreChannel
}
type SessionStore interface {
@@ -110,6 +111,7 @@ type SessionStore interface {
Get(sessionIdOrToken string) StoreChannel
GetSessions(userId string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
+ RemoveAllSessionsForTeam(teamId string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
diff --git a/utils/config.go b/utils/config.go
index 35631358b..5d786699b 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -58,9 +58,9 @@ func FindDir(dir string) string {
func ConfigureCmdLineLog() {
ls := model.LogSettings{}
- ls.ConsoleEnable = true
+ ls.EnableConsole = true
ls.ConsoleLevel = "ERROR"
- ls.FileEnable = false
+ ls.EnableFile = false
configureLog(&ls)
}
@@ -68,7 +68,7 @@ func configureLog(s *model.LogSettings) {
l4g.Close()
- if s.ConsoleEnable {
+ if s.EnableConsole {
level := l4g.DEBUG
if s.ConsoleLevel == "INFO" {
level = l4g.INFO
@@ -79,7 +79,7 @@ func configureLog(s *model.LogSettings) {
l4g.AddFilter("stdout", level, l4g.NewConsoleLogWriter())
}
- if s.FileEnable {
+ if s.EnableFile {
var fileFormat = s.FileFormat
@@ -161,8 +161,6 @@ func getSanitizeOptions(c *model.Config) map[string]bool {
options := map[string]bool{}
options["fullname"] = c.PrivacySettings.ShowFullName
options["email"] = c.PrivacySettings.ShowEmailAddress
- options["skypeid"] = c.PrivacySettings.ShowSkypeId
- options["phonenumber"] = c.PrivacySettings.ShowPhoneNumber
return options
}
@@ -175,57 +173,24 @@ func getClientProperties(c *model.Config) map[string]string {
props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash
- props["SiteName"] = c.ServiceSettings.SiteName
- props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail)
- props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
- props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
- props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink)
- props["SegmentDeveloperKey"] = c.ClientSettings.SegmentDeveloperKey
- props["GoogleDeveloperKey"] = c.ClientSettings.GoogleDeveloperKey
- props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl
- props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail)
- props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight)
- props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
- props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
+ props["SiteName"] = c.TeamSettings.SiteName
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
- props["AllowIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.AllowIncomingWebhooks)
-
- return props
-}
-func IsS3Configured() bool {
- if Cfg.AWSSettings.S3AccessKeyId == "" || Cfg.AWSSettings.S3SecretAccessKey == "" || Cfg.AWSSettings.S3Region == "" || Cfg.AWSSettings.S3Bucket == "" {
- return false
- }
-
- return true
-}
+ props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
+ props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
+ props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
-func GetAllowedAuthServices() []string {
- authServices := []string{}
- for name, service := range Cfg.SSOSettings {
- if service.Allow {
- authServices = append(authServices, name)
- }
- }
+ props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
+ props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
+ props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
- if !Cfg.ServiceSettings.DisableEmailSignUp {
- authServices = append(authServices, "email")
- }
+ props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable)
- return authServices
-}
-
-func IsServiceAllowed(s string) bool {
- if len(s) == 0 {
- return false
- }
+ props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
- if service, ok := Cfg.SSOSettings[s]; ok {
- if service.Allow {
- return true
- }
- }
+ props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink)
+ props["ProfileHeight"] = fmt.Sprintf("%v", c.FileSettings.ProfileHeight)
+ props["ProfileWidth"] = fmt.Sprintf("%v", c.FileSettings.ProfileWidth)
- return false
+ return props
}
diff --git a/utils/mail.go b/utils/mail.go
index 7cb178626..dd975155d 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -15,43 +15,22 @@ import (
"time"
)
-func CheckMailSettings() *model.AppError {
- if len(Cfg.EmailSettings.SMTPServer) == 0 || Cfg.EmailSettings.ByPassEmail {
- return model.NewAppError("CheckMailSettings", "No email settings present, mail will not be sent", "")
- }
- conn, err := connectToSMTPServer()
- if err != nil {
- return err
- }
- defer conn.Close()
- c, err2 := newSMTPClient(conn)
- if err2 != nil {
- return err
- }
- defer c.Quit()
- defer c.Close()
-
- return nil
-}
-
-func connectToSMTPServer() (net.Conn, *model.AppError) {
- host, _, _ := net.SplitHostPort(Cfg.EmailSettings.SMTPServer)
-
+func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
var conn net.Conn
var err error
- if Cfg.EmailSettings.UseTLS {
+ if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
- ServerName: host,
+ ServerName: config.EmailSettings.SMTPServer,
}
- conn, err = tls.Dial("tcp", Cfg.EmailSettings.SMTPServer, tlsconfig)
+ conn, err = tls.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort, tlsconfig)
if err != nil {
return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
}
} else {
- conn, err = net.Dial("tcp", Cfg.EmailSettings.SMTPServer)
+ conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if err != nil {
return nil, model.NewAppError("SendMail", "Failed to open connection", err.Error())
}
@@ -60,24 +39,23 @@ func connectToSMTPServer() (net.Conn, *model.AppError) {
return conn, nil
}
-func newSMTPClient(conn net.Conn) (*smtp.Client, *model.AppError) {
- host, _, _ := net.SplitHostPort(Cfg.EmailSettings.SMTPServer)
- c, err := smtp.NewClient(conn, host)
+func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
+ c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if err != nil {
l4g.Error("Failed to open a connection to SMTP server %v", err)
return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
}
// GO does not support plain auth over a non encrypted connection.
// so if not tls then no auth
- auth := smtp.PlainAuth("", Cfg.EmailSettings.SMTPUsername, Cfg.EmailSettings.SMTPPassword, host)
- if Cfg.EmailSettings.UseTLS {
+ auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
+ if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
if err = c.Auth(auth); err != nil {
return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error())
}
- } else if Cfg.EmailSettings.UseStartTLS {
+ } else if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
- ServerName: host,
+ ServerName: config.EmailSettings.SMTPServer,
}
c.StartTLS(tlsconfig)
if err = c.Auth(auth); err != nil {
@@ -88,12 +66,16 @@ func newSMTPClient(conn net.Conn) (*smtp.Client, *model.AppError) {
}
func SendMail(to, subject, body string) *model.AppError {
+ return SendMailUsingConfig(to, subject, body, Cfg)
+}
+
+func SendMailUsingConfig(to, subject, body string, config *model.Config) *model.AppError {
- if len(Cfg.EmailSettings.SMTPServer) == 0 || Cfg.EmailSettings.ByPassEmail {
+ if !config.EmailSettings.SendEmailNotifications {
return nil
}
- fromMail := mail.Address{Cfg.EmailSettings.FeedbackName, Cfg.EmailSettings.FeedbackEmail}
+ fromMail := mail.Address{config.EmailSettings.FeedbackName, config.EmailSettings.FeedbackEmail}
toMail := mail.Address{"", to}
headers := make(map[string]string)
@@ -110,13 +92,13 @@ func SendMail(to, subject, body string) *model.AppError {
}
message += "\r\n<html><body>" + body + "</body></html>"
- conn, err1 := connectToSMTPServer()
+ conn, err1 := connectToSMTPServer(config)
if err1 != nil {
return err1
}
defer conn.Close()
- c, err2 := newSMTPClient(conn)
+ c, err2 := newSMTPClient(conn, config)
if err2 != nil {
return err2
}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index e82fe1b76..92f0bbdce 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -3,44 +3,118 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
var LogsTab = require('./logs.jsx');
+var FileSettingsTab = require('./image_settings.jsx');
+var PrivacySettingsTab = require('./privacy_settings.jsx');
+var RateSettingsTab = require('./rate_settings.jsx');
+var GitLabSettingsTab = require('./gitlab_settings.jsx');
+var SqlSettingsTab = require('./sql_settings.jsx');
+var TeamSettingsTab = require('./team_settings.jsx');
+var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
this.state = {
- config: null,
- selected: 'email_settings'
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
- selected: this.state.selected
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
});
}
- selectTab(tab) {
- this.setState({selected: tab});
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
}
render() {
@@ -53,6 +127,22 @@ export default class AdminController extends React.Component {
tab = <LogSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
+ } else if (this.state.selected === 'image_settings') {
+ tab = <FileSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'privacy_settings') {
+ tab = <PrivacySettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'rate_settings') {
+ tab = <RateSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'gitlab_settings') {
+ tab = <GitLabSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'sql_settings') {
+ tab = <SqlSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_settings') {
+ tab = <TeamSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'service_settings') {
+ tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
}
@@ -64,7 +154,12 @@ export default class AdminController extends React.Component {
/>
<AdminSidebar
selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index a6e689490..cebb3ff20 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component {
this.isSelected = this.isSelected.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.state = {
+ showSelectModal: false
};
}
- handleClick(name, e) {
+ handleClick(name, teamId, e) {
e.preventDefault();
- this.props.selectTab(name);
+ this.props.selectTab(name, teamId);
}
- isSelected(name) {
+ isSelected(name, teamId) {
if (this.props.selected === name) {
- return 'active';
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
}
return '';
}
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
componentDidMount() {
}
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -39,10 +133,43 @@ export default class AdminSidebar extends React.Component {
<li>
<ul className='nav nav__sub-menu'>
<li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
+ >
+ {'Service Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
+ >
+ {'Team Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
+ >
+ {'SQL Settings'}
+ </a>
+ </li>
+ <li>
<a
href='#'
className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
>
{'Email Settings'}
</a>
@@ -50,8 +177,17 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
+ className={this.isSelected('image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
+ >
+ {'File Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
>
{'Log Settings'}
</a>
@@ -59,8 +195,58 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
+ className={this.isSelected('rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
+ >
+ {'Rate Limit Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
+ >
+ {'Privacy Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
+ >
+ {'GitLab Settings'}
+ </a>
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ <li>
+ {teams}
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
>
{'Logs'}
</a>
@@ -69,12 +255,24 @@ export default class AdminSidebar extends React.Component {
</li>
</ul>
</div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
</div>
);
}
}
AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
}; \ No newline at end of file
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index e8fb25858..854988947 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -1,15 +1,179 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
export default class EmailSettings extends React.Component {
constructor(props) {
super(props);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleTestConnection = this.handleTestConnection.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.buildConfig = this.buildConfig.bind(this);
+ this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
+ this.handleGenerateReset = this.handleGenerateReset.bind(this);
+
this.state = {
+ sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ saveNeeded: false,
+ serverError: null,
+ emailSuccess: null,
+ emailFail: null
};
}
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'sendEmailNotifications_true') {
+ s.sendEmailNotifications = true;
+ }
+
+ if (action === 'sendEmailNotifications_false') {
+ s.sendEmailNotifications = false;
+ }
+
+ this.setState(s);
+ }
+
+ buildConfig() {
+ var config = this.props.config;
+ config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim();
+ config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim();
+ config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim();
+ config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim();
+ config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim();
+ config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim();
+
+ config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim();
+ if (config.EmailSettings.InviteSalt === '') {
+ config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
+ }
+
+ config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim();
+ if (config.EmailSettings.PasswordResetSalt === '') {
+ config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
+ }
+
+ return config;
+ }
+
+ handleGenerateInvite(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleGenerateReset(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleTestConnection(e) {
+ e.preventDefault();
+ $('#connection-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.testEmail(
+ config,
+ () => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: true,
+ emailFail: null
+ });
+ $('#connection-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: err.message + ' - ' + err.detailed_error
+ });
+ $('#connection-button').button('reset');
+ }
+ );
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: false,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: err.message,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var emailSuccess = '';
+ if (this.state.emailSuccess) {
+ emailSuccess = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>{'No errors were reported while sending an email. Please check your inbox to make sure.'}
+ </div>
+ );
+ }
+
+ var emailFail = '';
+ if (this.state.emailFail) {
+ emailSuccess = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>{'Connection unsuccessful: ' + this.state.emailFail}
+ </div>
+ );
+ }
+
return (
<div className='wrapper--fixed'>
<h3>{'Email Settings'}</h3>
@@ -17,295 +181,370 @@ export default class EmailSettings extends React.Component {
className='form-horizontal'
role='form'
>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='email'
+ htmlFor='allowSignUpWithEmail'
>
- {'Bypass Email: '}
- <a
- href='#'
- data-trigger='hover click'
- data-toggle='popover'
- data-position='bottom'
- data-content={'Here\'s some more help text inside a popover for the Bypass Email field just to show how popovers look.'}
- >
- {'(?)'}
- </a>
+ {'Allow Sign Up With Email: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
- name='byPassEmail'
- value='option1'
+ name='allowSignUpWithEmail'
+ value='true'
+ ref='allowSignUpWithEmail'
+ defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
/>
- {'True'}
+ {'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
- name='byPassEmail'
- value='option2'
+ name='allowSignUpWithEmail'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
/>
- {'False'}
+ {'false'}
</label>
- <p className='help-text'>{'This is some sample help text for the Bypass Email field'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true Mattermost will allow team creation and account signup utilizing email and password. You would set this to false if you only wanted to allow signup from a service like OAuth or LDAP.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpUsername'
+ htmlFor='sendEmailNotifications'
>
- {'SMTP Username:'}
+ {'Send Email Notifications: '}
</label>
<div className='col-sm-8'>
- <input
- type='email'
- className='form-control'
- id='smtpUsername'
- placeholder='Enter your SMTP username'
- value=''
- />
- <div className='help-text'>
- <div className='alert alert-warning'><i className='fa fa-warning'></i>{' This is some error text for the Bypass Email field'}</div>
- </div>
- <p className='help-text'>{'This is some sample help text for the SMTP username field'}</p>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='true'
+ ref='sendEmailNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true Mattermost will attempt to send email notifications. Developers may set this field to false skipping sending emails for faster development.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpPassword'
+ htmlFor='requireEmailVerification'
>
- {'SMTP Password:'}
+ {'Require Email Verification: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='true'
+ ref='requireEmailVerification'
+ defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true Mattermost will not allow a user to login without first having recieved an email with a verification link. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='feedbackName'
+ >
+ {'Feedback Name:'}
</label>
<div className='col-sm-8'>
<input
- type='password'
+ type='text'
className='form-control'
- id='smtpPassword'
- placeholder='Enter your SMTP password'
- value=''
+ id='feedbackName'
+ ref='feedbackName'
+ placeholder='Ex: "Mattermost", "System", "John Smith"'
+ defaultValue={this.props.config.EmailSettings.FeedbackName}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Name displayed on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpServer'
+ htmlFor='feedbackEmail'
>
- {'SMTP Server:'}
+ {'Feedback Email:'}
</label>
<div className='col-sm-8'>
<input
- type='text'
+ type='email'
className='form-control'
- id='smtpServer'
- placeholder='Enter your SMTP server'
- value=''
+ id='feedbackEmail'
+ ref='feedbackEmail'
+ placeholder='Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
+ defaultValue={this.props.config.EmailSettings.FeedbackEmail}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- <div className='help-text'>
- <a
- href='#'
- className='help-link'
- >
- {'Test Connection'}
- </a>
- <div className='alert alert-success'><i className='fa fa-check'></i>{' Connection successful'}</div>
- <div className='alert alert-warning hide'><i className='fa fa-warning'></i>{' Connection unsuccessful'}</div>
- </div>
+ <p className='help-text'>{'Email displayed on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
+
<div className='form-group'>
- <label className='control-label col-sm-4'>{'Use TLS:'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPUsername'
+ >
+ {'SMTP Username:'}
+ </label>
<div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='tls'
- value='option1'
- />
- {'True'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='tls'
- value='option2'
- />
- {'False'}
- </label>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPUsername'
+ ref='SMTPUsername'
+ placeholder='Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.EmailSettings.SMTPUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div>
</div>
+
<div className='form-group'>
- <label className='control-label col-sm-4'>{'Use Start TLS:'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPPassword'
+ >
+ {'SMTP Password:'}
+ </label>
<div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='starttls'
- value='option1'
- />
- {'True'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='starttls'
- value='option2'
- />
- {'False'}
- </label>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPPassword'
+ ref='SMTPPassword'
+ placeholder='Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.EmailSettings.SMTPPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='feedbackEmail'
+ htmlFor='SMTPServer'
>
- {'Feedback Email:'}
+ {'SMTP Server:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='feedbackEmail'
- placeholder='Enter your feedback email'
- value=''
+ id='SMTPServer'
+ ref='SMTPServer'
+ placeholder='Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
+ defaultValue={this.props.config.EmailSettings.SMTPServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Location of SMTP email server.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='feedbackUsername'
+ htmlFor='SMTPPort'
>
- {'Feedback Username:'}
+ {'SMTP Port:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='feedbackUsername'
- placeholder='Enter your feedback username'
- value=''
+ id='SMTPPort'
+ ref='SMTPPort'
+ placeholder='Ex: "25", "465"'
+ defaultValue={this.props.config.EmailSettings.SMTPPort}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Port of SMTP email server.'}</p>
</div>
</div>
+
<div className='form-group'>
- <div className='col-sm-offset-4 col-sm-8'>
- <div className='checkbox'>
- <label><input type='checkbox' />{'Remember me'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ConnectionSecurity'
+ >
+ {'Connection Security:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='ConnectionSecurity'
+ ref='ConnectionSecurity'
+ defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ <option value=''>{'None'}</option>
+ <option value='TLS'>{'TLS (Recommended)'}</option>
+ <option value='STARTTLS'>{'STARTTLS'}</option>
+ </select>
+ <div className='help-text'>
+ <table
+ className='table-bordered'
+ cellPadding='5'
+ >
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </table>
+ </div>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleTestConnection}
+ disabled={!this.state.sendEmailNotifications}
+ id='connection-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Testing...'}
+ >
+ {'Test Connection'}
+ </button>
+ {emailSuccess}
+ {emailFail}
</div>
</div>
</div>
- <div
- className='panel-group'
- id='accordion'
- role='tablist'
- aria-multiselectable='true'
- >
- <div className='panel panel-default'>
- <div
- className='panel-heading'
- role='tab'
- id='headingOne'
- >
- <h3 className='panel-title'>
- <a
- className='collapsed'
- role='button'
- data-toggle='collapse'
- data-parent='#accordion'
- href='#collapseOne'
- aria-expanded='true'
- aria-controls='collapseOne'
- >
- {'Advanced Settings '}
- <i className='fa fa-plus'></i>
- <i className='fa fa-minus'></i>
- </a>
- </h3>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='InviteSalt'
+ >
+ {'Invite Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='InviteSalt'
+ ref='InviteSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.InviteSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of email invites.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateInvite}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
</div>
- <div
- id='collapseOne'
- className='panel-collapse collapse'
- role='tabpanel'
- aria-labelledby='headingOne'
- >
- <div className='panel-body'>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push server:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your Apple push server'
- value=''
- />
- <p className='help-text'>{'This is some sample help text for the Apple push server field'}</p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push certificate public:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your public apple push certificate'
- value=''
- />
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push certificate private:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your private apple push certificate'
- value=''
- />
- </div>
- </div>
- </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PasswordResetSalt'
+ >
+ {'Password Reset Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PasswordResetSalt'
+ ref='PasswordResetSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of password reset emails.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateReset}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
</div>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
+ {serverError}
<button
+ disabled={!this.state.saveNeeded}
type='submit'
- className='btn btn-primary'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
+
</form>
</div>
);
}
-} \ No newline at end of file
+}
+
+EmailSettings.propTypes = {
+ config: React.PropTypes.object
+}; \ No newline at end of file
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
new file mode 100644
index 000000000..f76655b89
--- /dev/null
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -0,0 +1,277 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class GitLabSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ Allow: this.props.config.GitLabSettings.Allow,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'AllowTrue') {
+ s.Allow = true;
+ }
+
+ if (action === 'AllowFalse') {
+ s.Allow = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.GitLabSettings.Allow = React.findDOMNode(this.refs.Allow).checked;
+ config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
+ config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
+ config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+ config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
+ config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
+ config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'GitLab Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Allow'
+ >
+ {'Enable Sign Up With GitLab: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='true'
+ ref='Allow'
+ defaultChecked={this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='false'
+ defaultChecked={!this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true Mattermost will allow team creation and account signup utilizing GitLab OAuth.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Secret'
+ >
+ {'Secret:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Secret'
+ ref='Secret'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Secret}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Id'
+ >
+ {'Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Id'
+ ref='Id'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Id}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Scope'
+ >
+ {'Scope:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Scope'
+ ref='Scope'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.Scope}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AuthEndpoint'
+ >
+ {'Auth Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AuthEndpoint'
+ ref='AuthEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='TokenEndpoint'
+ >
+ {'Token Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='TokenEndpoint'
+ ref='TokenEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='UserApiEndpoint'
+ >
+ {'User API Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='UserApiEndpoint'
+ ref='UserApiEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Need help text.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+GitLabSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
new file mode 100644
index 000000000..25d5ad857
--- /dev/null
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -0,0 +1,496 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class FileSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null,
+ DriverName: this.props.config.FileSettings.DriverName
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'DriverName') {
+ s.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ }
+
+ this.setState(s);
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value;
+ config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
+ config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
+ config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value;
+ config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value;
+ config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked;
+
+ config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+
+ if (config.FileSettings.PublicLinkSalt === '') {
+ config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
+ }
+
+ var thumbnailWidth = 120;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
+ thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10);
+ }
+ config.FileSettings.ThumbnailWidth = thumbnailWidth;
+ React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
+
+ var thumbnailHeight = 100;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
+ thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10);
+ }
+ config.FileSettings.ThumbnailHeight = thumbnailHeight;
+ React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
+
+ var previewWidth = 1024;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) {
+ previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10);
+ }
+ config.FileSettings.PreviewWidth = previewWidth;
+ React.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
+
+ var previewHeight = 0;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) {
+ previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10);
+ }
+ config.FileSettings.PreviewHeight = previewHeight;
+ React.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
+
+ var profileWidth = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) {
+ profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10);
+ }
+ config.FileSettings.ProfileWidth = profileWidth;
+ React.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
+
+ var profileHeight = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) {
+ profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10);
+ }
+ config.FileSettings.ProfileHeight = profileHeight;
+ React.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var enableFile = false;
+ var enableS3 = false;
+
+ if (this.state.DriverName === 'local') {
+ enableFile = true;
+ }
+
+ if (this.state.DriverName === 'amazons3') {
+ enableS3 = true;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'File Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Store Files In:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='DriverName'
+ ref='DriverName'
+ defaultValue={this.props.config.FileSettings.DriverName}
+ onChange={this.handleChange.bind(this, 'DriverName')}
+ >
+ <option value=''>{'Disable File Storage'}</option>
+ <option value='local'>{'Local File System'}</option>
+ <option value='amazons3'>{'Amazon S3'}</option>
+ </select>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Directory'
+ >
+ {'Local Directory Location:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Directory'
+ ref='Directory'
+ placeholder='Ex "./data/"'
+ defaultValue={this.props.config.FileSettings.Directory}
+ onChange={this.handleChange}
+ disabled={!enableFile}
+ />
+ <p className='help-text'>{'Directory to which image files are written. If blank, will be set to ./data/.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3AccessKeyId'
+ >
+ {'Amazon S3 Access Key Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3AccessKeyId'
+ ref='AmazonS3AccessKeyId'
+ placeholder='Ex "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3SecretAccessKey'
+ >
+ {'Amazon S3 Secret Access Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3SecretAccessKey'
+ ref='AmazonS3SecretAccessKey'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Bucket'
+ >
+ {'Amazon S3 Bucket:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Bucket'
+ ref='AmazonS3Bucket'
+ placeholder='Ex "mattermost-media"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Name you selected for your S3 bucket in AWS.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Region'
+ >
+ {'Amazon S3 Region:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Region'
+ ref='AmazonS3Region'
+ placeholder='Ex "us-east-1"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Region}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'AWS region you selected for creating your S3 bucket.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailWidth'
+ >
+ {'Thumbnail Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailWidth'
+ ref='ThumbnailWidth'
+ placeholder='Ex "120"'
+ defaultValue={this.props.config.FileSettings.ThumbnailWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailHeight'
+ >
+ {'Thumbnail Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailHeight'
+ ref='ThumbnailHeight'
+ placeholder='Ex "100"'
+ defaultValue={this.props.config.FileSettings.ThumbnailHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewWidth'
+ >
+ {'Preview Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewWidth'
+ ref='PreviewWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.PreviewWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewHeight'
+ >
+ {'Preview Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewHeight'
+ ref='PreviewHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.PreviewHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileWidth'
+ >
+ {'Profile Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileWidth'
+ ref='ProfileWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.ProfileWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileHeight'
+ >
+ {'Profile Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileHeight'
+ ref='ProfileHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.ProfileHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePublicLink'
+ >
+ {'Share Public File Link: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='true'
+ ref='EnablePublicLink'
+ defaultChecked={this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='false'
+ defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Allow users to share public links to files and images.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PublicLinkSalt'
+ >
+ {'Public Link Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PublicLinkSalt'
+ ref='PublicLinkSalt'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.FileSettings.PublicLinkSalt}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt added to signing of public image links.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+FileSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 4e3db8f68..d66801431 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -12,13 +12,33 @@ export default class LogSettings extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
+ consoleEnable: this.props.config.LogSettings.EnableConsole,
+ fileEnable: this.props.config.LogSettings.EnableFile,
saveNeeded: false,
serverError: null
};
}
- handleChange() {
- this.setState({saveNeeded: true, serverError: this.state.serverError});
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'console_true') {
+ s.consoleEnable = true;
+ }
+
+ if (action === 'console_false') {
+ s.consoleEnable = false;
+ }
+
+ if (action === 'file_true') {
+ s.fileEnable = true;
+ }
+
+ if (action === 'file_false') {
+ s.fileEnable = false;
+ }
+
+ this.setState(s);
}
handleSubmit(e) {
@@ -26,9 +46,9 @@ export default class LogSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.LogSettings.ConsoleEnable = React.findDOMNode(this.refs.consoleEnable).checked;
+ config.LogSettings.EnableConsole = React.findDOMNode(this.refs.consoleEnable).checked;
config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
- config.LogSettings.FileEnable = React.findDOMNode(this.refs.fileEnable).checked;
+ config.LogSettings.EnableFile = React.findDOMNode(this.refs.fileEnable).checked;
config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
@@ -37,11 +57,21 @@ export default class LogSettings extends React.Component {
config,
() => {
AsyncClient.getConfig();
- this.setState({serverError: null, saveNeeded: false});
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: null,
+ saveNeeded: false
+ });
$('#save-button').button('reset');
},
(err) => {
- this.setState({serverError: err.message, saveNeeded: true});
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: err.message,
+ saveNeeded: true
+ });
$('#save-button').button('reset');
}
);
@@ -80,8 +110,8 @@ export default class LogSettings extends React.Component {
name='consoleEnable'
value='true'
ref='consoleEnable'
- defaultChecked={this.props.config.LogSettings.ConsoleEnable}
- onChange={this.handleChange}
+ defaultChecked={this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_true')}
/>
{'true'}
</label>
@@ -90,8 +120,8 @@ export default class LogSettings extends React.Component {
type='radio'
name='consoleEnable'
value='false'
- defaultChecked={!this.props.config.LogSettings.ConsoleEnable}
- onChange={this.handleChange}
+ defaultChecked={!this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_false')}
/>
{'false'}
</label>
@@ -113,6 +143,7 @@ export default class LogSettings extends React.Component {
ref='consoleLevel'
defaultValue={this.props.config.LogSettings.consoleLevel}
onChange={this.handleChange}
+ disabled={!this.state.consoleEnable}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
@@ -135,8 +166,8 @@ export default class LogSettings extends React.Component {
name='fileEnable'
ref='fileEnable'
value='true'
- defaultChecked={this.props.config.LogSettings.FileEnable}
- onChange={this.handleChange}
+ defaultChecked={this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_true')}
/>
{'true'}
</label>
@@ -145,8 +176,8 @@ export default class LogSettings extends React.Component {
type='radio'
name='fileEnable'
value='false'
- defaultChecked={!this.props.config.LogSettings.FileEnable}
- onChange={this.handleChange}
+ defaultChecked={!this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_false')}
/>
{'false'}
</label>
@@ -168,6 +199,7 @@ export default class LogSettings extends React.Component {
ref='fileLevel'
defaultValue={this.props.config.LogSettings.FileLevel}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
@@ -193,6 +225,7 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file location'
defaultValue={this.props.config.LogSettings.FileLocation}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
/>
<p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
</div>
@@ -214,6 +247,7 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file format'
defaultValue={this.props.config.LogSettings.FileFormat}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
/>
<p className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
new file mode 100644
index 000000000..8ce693925
--- /dev/null
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -0,0 +1,163 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class PrivacySettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
+ config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Privacy Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowEmailAddress'
+ >
+ {'Show Email Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='true'
+ ref='ShowEmailAddress'
+ defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Hides email address of users from other users including team administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowFullName'
+ >
+ {'Show Full Name: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='true'
+ ref='ShowFullName'
+ defaultChecked={this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Hides full name of users from other users including team administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+PrivacySettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
new file mode 100644
index 000000000..c05bf4a82
--- /dev/null
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -0,0 +1,272 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class RateSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
+ VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'EnableRateLimiterTrue') {
+ s.EnableRateLimiter = true;
+ }
+
+ if (action === 'EnableRateLimiterFalse') {
+ s.EnableRateLimiter = false;
+ }
+
+ if (action === 'VaryByRemoteAddrTrue') {
+ s.VaryByRemoteAddr = true;
+ }
+
+ if (action === 'VaryByRemoteAddrFalse') {
+ s.VaryByRemoteAddr = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.RateLimitSettings.EnableRateLimiter = React.findDOMNode(this.refs.EnableRateLimiter).checked;
+ config.RateLimitSettings.VaryByRemoteAddr = React.findDOMNode(this.refs.VaryByRemoteAddr).checked;
+ config.RateLimitSettings.VaryByHeader = React.findDOMNode(this.refs.VaryByHeader).value.trim();
+
+ var PerSec = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PerSec).value, 10))) {
+ PerSec = parseInt(React.findDOMNode(this.refs.PerSec).value, 10);
+ }
+ config.RateLimitSettings.PerSec = PerSec;
+ React.findDOMNode(this.refs.PerSec).value = PerSec;
+
+ var MemoryStoreSize = 10000;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
+ MemoryStoreSize = parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10);
+ }
+ config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
+ React.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'Rate Limit Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableRateLimiter'
+ >
+ {'Enable Rate Limiter: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='true'
+ ref='EnableRateLimiter'
+ defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When enabled throttles rate at which APIs respond.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PerSec'
+ >
+ {'Number Of Queries Per Second:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PerSec'
+ ref='PerSec'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.RateLimitSettings.PerSec}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Throttles API at this number of requests per second.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MemoryStoreSize'
+ >
+ {'Memory Store Size:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MemoryStoreSize'
+ ref='MemoryStoreSize'
+ placeholder='Ex "10000"'
+ defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Maximum number of users sessions connected to the system as determined by VaryByRemoteAddr and VaryByHeader variables.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByRemoteAddr'
+ >
+ {'Limit By Remote Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='true'
+ ref='VaryByRemoteAddr'
+ defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Rate limit API access by IP address.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByHeader'
+ >
+ {'Limit By Http Header:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='VaryByHeader'
+ ref='VaryByHeader'
+ placeholder='Ex "X-Real-IP", "X-Forwarded-For"'
+ defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
+ />
+ <p className='help-text'>{'When filled in, vary rate limiting by http header field specified (e.g. when configuring ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+RateSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index fa30de7b2..343f65131 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -1,124 +1,99 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-export default class SelectTeam extends React.Component {
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
}
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
return (
- <div className='modal fade'
- id='select-team'
- tabIndex='-1'
- role='dialog'
- aria-labelledby='teamsModalLabel'
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
>
- <div className='modal-dialog'
- role='document'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='teamsModalLabel'
- >
- {'Select a team'}
- </h4>
- </div>
- <div className='modal-body'>
- <table className='more-channel-table table'>
- <tbody>
- <tr>
- <td>
- <p className='more-channel-name'>{'Descartes'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Grouping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Adventure'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Crossroads'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Sky scraping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Outdoors'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Microsoft'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Apple'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Close'}
- </button>
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
);
}
-} \ No newline at end of file
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
new file mode 100644
index 000000000..1bb1f053b
--- /dev/null
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -0,0 +1,296 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class ServiceSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.ServiceSettings.ListenAddress = React.findDOMNode(this.refs.ListenAddress).value.trim();
+ if (config.ServiceSettings.ListenAddress === '') {
+ config.ServiceSettings.ListenAddress = ':8065';
+ React.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
+ }
+
+ config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
+ config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
+ config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
+ config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
+
+ var MaximumLoginAttempts = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
+ MaximumLoginAttempts = parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
+ }
+ config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
+ React.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'Service Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ListenAddress'
+ >
+ {'Listen Address:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ListenAddress'
+ ref='ListenAddress'
+ placeholder='Ex ":8065"'
+ defaultValue={this.props.config.ServiceSettings.ListenAddress}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'The address to bind to and listen. ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaximumLoginAttempts'
+ >
+ {'Maximum Login Attempts:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaximumLoginAttempts'
+ ref='MaximumLoginAttempts'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Login attempts allowed before user is locked out and required to reset password via email.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SegmentDeveloperKey'
+ >
+ {'Segment Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SegmentDeveloperKey'
+ ref='SegmentDeveloperKey'
+ placeholder='Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
+ defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'For users running a SaaS services, sign up for a key at Segment.com to track metrics.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='GoogleDeveloperKey'
+ >
+ {'Google Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='GoogleDeveloperKey'
+ ref='GoogleDeveloperKey'
+ placeholder='Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
+ defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '}<a href='https://www.youtube.com/watch?v=Im69kzhpR3I'>{'https://www.youtube.com/watch?v=Im69kzhpR3I'}</a>{'. Leaving field blank disables the automatic generation of YouTube video previews from links.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOAuthServiceProvider'
+ >
+ {'Enable OAuth Service Provider: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOAuthServiceProvider'
+ value='true'
+ ref='EnableOAuthServiceProvider'
+ defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOAuthServiceProvider'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When enabled Mattermost will act as an Oauth2 Provider. Changing this will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableIncomingWebhooks'
+ >
+ {'EnableIncomingWebhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='true'
+ ref='EnableIncomingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true incomming web hooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTesting'
+ >
+ {'Enable Testing: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='true'
+ ref='EnableTesting'
+ defaultChecked={this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true slash commands like /loadtest are enabled in the add comment box. Changing this will require a server restart before taking effect. Typically used for development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+ServiceSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
new file mode 100644
index 000000000..ad01b5963
--- /dev/null
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -0,0 +1,283 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class SqlSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.SqlSettings.Trace = React.findDOMNode(this.refs.Trace).checked;
+ config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
+
+ if (config.SqlSettings.AtRestEncryptKey === '') {
+ config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
+ }
+
+ var MaxOpenConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
+ MaxOpenConns = parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10);
+ }
+ config.SqlSettings.MaxOpenConns = MaxOpenConns;
+ React.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
+
+ var MaxIdleConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
+ MaxIdleConns = parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10);
+ }
+ config.SqlSettings.MaxIdleConns = MaxIdleConns;
+ React.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
+
+ var dataSourceReplicas = '';
+ this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'SQL Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Driver Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ {'Data Source:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ {'Data Source Replicas:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxIdleConns'
+ >
+ {'Maximum Idle Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxIdleConns'
+ ref='MaxIdleConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxIdleConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of idle connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxOpenConns'
+ >
+ {'Maximum Open Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxOpenConns'
+ ref='MaxOpenConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxOpenConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of open connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AtRestEncryptKey'
+ >
+ {'At Rest Encrypt Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AtRestEncryptKey'
+ ref='AtRestEncryptKey'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt available to encrypt and decrypt sensitive fields in database.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Trace'
+ >
+ {'Trace: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='true'
+ ref='Trace'
+ defaultChecked={this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='false'
+ defaultChecked={!this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Output executing SQL statements to the log. Typically used for development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+SqlSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
new file mode 100644
index 000000000..fefc0e936
--- /dev/null
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -0,0 +1,257 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class TeamSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
+ config.TeamSettings.DefaultThemeColor = React.findDOMNode(this.refs.DefaultThemeColor).value.trim();
+ config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
+ config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
+ config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
+
+ var MaxUsersPerTeam = 50;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
+ MaxUsersPerTeam = parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
+ }
+ config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
+ React.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'Team Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SiteName'
+ >
+ {'Site Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SiteName'
+ ref='SiteName'
+ placeholder='Ex "Mattermost"'
+ defaultValue={this.props.config.TeamSettings.SiteName}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Name of service shown in login screens and UI.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxUsersPerTeam'
+ >
+ {'Max Users Per Team:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxUsersPerTeam'
+ ref='MaxUsersPerTeam'
+ placeholder='Ex "25"'
+ defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of users per team.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DefaultThemeColor'
+ >
+ {'Default Theme Color:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='DefaultThemeColor'
+ ref='DefaultThemeColor'
+ placeholder='Ex "#2389D7"'
+ defaultValue={this.props.config.TeamSettings.DefaultThemeColor}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Default theme color for team sites.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamCreation'
+ >
+ {'Enable Team Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='true'
+ ref='EnableTeamCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableUserCreation'
+ >
+ {'Enable User Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='true'
+ ref='EnableUserCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictCreationToDomains'
+ >
+ {'Restrict Creation To Domains:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='RestrictCreationToDomains'
+ ref='RestrictCreationToDomains'
+ placeholder='Ex "corp.mattermost.com, mattermost.org"'
+ defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Teams can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+TeamSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ // this.setState({
+ // teamId: this.state.teamId,
+ // users: this.state.users,
+ // serverError: this.state.serverError,
+ // showPasswordModal: this.state.showPasswordModal,
+ // user: this.state.user
+ // });
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 650a72516..395b98630 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component {
emailErrors: {},
firstNameErrors: {},
lastNameErrors: {},
- emailEnabled: !global.window.config.ByPassEmail
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
}
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index ffc07a4dd..8cc4f1483 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -5,7 +5,6 @@ const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const UserStore = require('../stores/user_store.jsx');
const BrowserStore = require('../stores/browser_store.jsx');
-const Constants = require('../utils/constants.jsx');
export default class Login extends React.Component {
constructor(props) {
@@ -95,10 +94,8 @@ export default class Login extends React.Component {
focusEmail = true;
}
- const authServices = JSON.parse(this.props.authServices);
-
let loginMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -116,7 +113,7 @@ export default class Login extends React.Component {
}
let emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className={'form-group' + errorClass}>
@@ -205,11 +202,9 @@ export default class Login extends React.Component {
Login.defaultProps = {
teamName: '',
- teamDisplayName: '',
- authServices: ''
+ teamDisplayName: ''
};
Login.propTypes = {
teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- authServices: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index bf08e6508..4112138fa 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -12,38 +12,42 @@ export default class TeamSignUp extends React.Component {
this.updatePage = this.updatePage.bind(this);
- if (props.services.length === 1) {
- if (props.services[0] === Constants.EMAIL_SERVICE) {
- this.state = {page: 'email', service: ''};
- } else {
- this.state = {page: 'service', service: props.services[0]};
- }
- } else {
- this.state = {page: 'choose', service: ''};
+ var count = 0;
+
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
+ count = count + 1;
+ }
+
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ count = count + 1;
+ }
+
+ if (count > 1) {
+ this.state = {page: 'choose'};
+ } else if (global.window.config.EnableSignUpWithEmail === 'true') {
+ this.state = {page: 'email'};
+ } else if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ this.state = {page: 'gitlab'};
}
}
- updatePage(page, service) {
- this.setState({page: page, service: service});
+
+ updatePage(page) {
+ this.setState({page});
}
+
render() {
+ if (this.state.page === 'choose') {
+ return (
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ );
+ }
+
if (this.state.page === 'email') {
return <EmailSignUpPage />;
- } else if (this.state.page === 'service' && this.state.service !== '') {
- return <SSOSignupPage service={this.state.service} />;
+ } else if (this.state.page === 'gitlab') {
+ return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
}
-
- return (
- <ChoosePage
- services={this.props.services}
- updatePage={this.updatePage}
- />
- );
}
}
-
-TeamSignUp.defaultProps = {
- services: []
-};
-TeamSignUp.propTypes = {
- services: React.PropTypes.array
-};
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index e77bde861..8311747ee 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -5,7 +5,6 @@ var Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
-var Constants = require('../utils/constants.jsx');
export default class SignupUserComplete extends React.Component {
constructor(props) {
@@ -168,11 +167,8 @@ export default class SignupUserComplete extends React.Component {
</div>
);
- // add options to log in using another service
- var authServices = JSON.parse(this.props.authServices);
-
var signupMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -185,7 +181,7 @@ export default class SignupUserComplete extends React.Component {
}
var emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className='inner__content'>
@@ -269,7 +265,6 @@ SignupUserComplete.defaultProps = {
teamId: '',
email: '',
data: null,
- authServices: '',
teamDisplayName: ''
};
SignupUserComplete.propTypes = {
@@ -278,6 +273,5 @@ SignupUserComplete.propTypes = {
teamId: React.PropTypes.string,
email: React.PropTypes.string,
data: React.PropTypes.string,
- authServices: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 8315430e4..79f03510f 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
var uploadHelpText = (
<div>
<p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
- <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p>
+ <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index d3107c5c7..b8264b887 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Constants = require('../utils/constants.jsx');
-
export default class ChooseAuthPage extends React.Component {
constructor(props) {
super(props);
@@ -10,7 +8,7 @@ export default class ChooseAuthPage extends React.Component {
}
render() {
var buttons = [];
- if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
buttons.push(
<a
className='btn btn-custom-login gitlab btn-full'
@@ -18,17 +16,17 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickGit(e) {
e.preventDefault();
- this.props.updatePage('service', Constants.GITLAB_SERVICE);
+ this.props.updatePage('gitlab');
}.bind(this)
}
>
<span className='icon' />
- <span>Create new team with GitLab Account</span>
+ <span>{'Create new team with GitLab Account'}</span>
</a>
);
}
- if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
buttons.push(
<a
className='btn btn-custom-login email btn-full'
@@ -36,18 +34,18 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickEmail(e) {
e.preventDefault();
- this.props.updatePage('email', '');
+ this.props.updatePage('email');
}.bind(this)
}
>
<span className='fa fa-envelope' />
- <span>Create new team with email address</span>
+ <span>{'Create new team with email address'}</span>
</a>
);
}
if (buttons.length === 0) {
- buttons = <span>No sign-up methods configured, please contact your system administrator.</span>;
+ buttons = <span>{'No sign-up methods configured, please contact your system administrator.'}</span>;
}
return (
@@ -61,10 +59,6 @@ export default class ChooseAuthPage extends React.Component {
}
}
-ChooseAuthPage.defaultProps = {
- services: []
-};
ChooseAuthPage.propTypes = {
- services: React.PropTypes.array,
updatePage: React.PropTypes.func
};
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index 41ac98303..524bd5b50 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -13,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.submitSkip = this.submitSkip.bind(this);
this.keySubmit = this.keySubmit.bind(this);
this.state = {
- emailEnabled: !global.window.config.ByPassEmail
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
if (!this.state.emailEnabled) {
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 7cfac69e7..c5d028d31 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -57,7 +57,7 @@ export default class UserProfile extends React.Component {
}
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
- if (!global.window.config.ShowEmailAddress) {
+ if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
} else {
dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 5d9d9bfde..c1d4c4ab5 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -208,7 +208,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}
setupInitialState(props) {
var user = props.user;
- var emailEnabled = !global.window.config.ByPassEmail;
+ 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};
}
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 430a7ec7c..5113d2429 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -38,7 +38,7 @@ export default class UserSettingsModal extends React.Component {
if (global.window.config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
- if (global.window.config.AllowIncomingWebhooks === 'true') {
+ if (global.window.config.EnableIncomingWebhooks === 'true') {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index f7c980396..dafcdd9f9 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -300,7 +300,7 @@ export default class ViewImageModal extends React.Component {
}
var publicLink = '';
- if (global.window.config.AllowPublicLink) {
+ if (global.window.config.EnablePublicLink === 'true') {
publicLink = (
<div>
<a
diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx
index 830f622fa..f78e0f37a 100644
--- a/web/react/pages/login.jsx
+++ b/web/react/pages/login.jsx
@@ -8,7 +8,6 @@ function setupLoginPage(props) {
<Login
teamDisplayName={props.TeamDisplayName}
teamName={props.TeamName}
- authServices={props.AuthServices}
/>,
document.getElementById('login')
);
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
index 427daf577..d0e08f446 100644
--- a/web/react/pages/signup_team.jsx
+++ b/web/react/pages/signup_team.jsx
@@ -3,11 +3,9 @@
var SignupTeam = require('../components/signup_team.jsx');
-function setupSignupTeamPage(props) {
- var services = JSON.parse(props.AuthServices);
-
+function setupSignupTeamPage() {
React.render(
- <SignupTeam services={services} />,
+ <SignupTeam />,
document.getElementById('signup-team')
);
}
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
index 112aaa3f2..cc7607187 100644
--- a/web/react/pages/signup_user_complete.jsx
+++ b/web/react/pages/signup_user_complete.jsx
@@ -12,7 +12,6 @@ function setupSignupUserCompletePage(props) {
email={props.Email}
hash={props.Hash}
data={props.Data}
- authServices={props.AuthServices}
/>,
document.getElementById('signup-user-complete')
);
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index dd5b60a24..7b2aeb631 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -4,11 +4,14 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
+var BrowserStore = require('../stores/browser_store.jsx');
+
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
+var ALL_TEAMS_EVENT = 'all_team_change';
class AdminStoreClass extends EventEmitter {
constructor() {
@@ -16,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.config = null;
+ this.teams = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
@@ -24,6 +28,10 @@ class AdminStoreClass extends EventEmitter {
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
+
+ this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
+ this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
+ this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
}
emitLogChange() {
@@ -50,6 +58,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
+ emitAllTeamsChange() {
+ this.emit(ALL_TEAMS_EVENT);
+ }
+
+ addAllTeamsChangeListener(callback) {
+ this.on(ALL_TEAMS_EVENT, callback);
+ }
+
+ removeAllTeamsChangeListener(callback) {
+ this.removeListener(ALL_TEAMS_EVENT, callback);
+ }
+
getLogs() {
return this.logs;
}
@@ -65,6 +85,22 @@ class AdminStoreClass extends EventEmitter {
saveConfig(config) {
this.config = config;
}
+
+ getAllTeams() {
+ return this.teams;
+ }
+
+ saveAllTeams(teams) {
+ this.teams = teams;
+ }
+
+ getSelectedTeams() {
+ return BrowserStore.getItem('seleted_teams');
+ }
+
+ saveSelectedTeams(teams) {
+ BrowserStore.setItem('seleted_teams', teams);
+ }
}
var AdminStore = new AdminStoreClass();
@@ -81,6 +117,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
+ case ActionTypes.RECIEVED_ALL_TEAMS:
+ AdminStore.saveAllTeams(action.teams);
+ AdminStore.emitAllTeamsChange();
+ break;
default:
}
});
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ed228f6c4..ab2965000 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -371,6 +371,32 @@ export function getConfig() {
);
}
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 531e4fdae..63924bff2 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -4,12 +4,10 @@ var BrowserStore = require('../stores/browser_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
export function track(category, action, label, prop, val) {
- global.window.snowplow('trackStructEvent', category, action, label, prop, val);
global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
}
export function trackPage() {
- global.window.snowplow('trackPageView');
global.window.analytics.page();
}
@@ -334,6 +332,35 @@ export function saveConfig(config, success, error) {
});
}
+export function testEmail(config, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/test_email',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(config),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('testEmail', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -877,6 +904,21 @@ export function getProfiles(success, error) {
});
}
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function uploadFile(formData, success, error) {
var request = $.ajax({
url: '/api/v1/files/upload',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 41e9e9ca6..f58816862 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -35,8 +35,8 @@ module.exports = {
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null,
-
RECIEVED_LOGS: null,
+ RECIEVED_ALL_TEAMS: null,
TOGGLE_IMPORT_THEME_MODAL: null
}),
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 347024e1a..7e88f8644 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -15,6 +15,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
heading(text, level, raw) {
const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
@@ -31,11 +39,19 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
if (title) {
output += ' title="' + title + '"';
}
- output += '>' + text + '</a>';
+ output += ' target="_blank">' + text + '</a>';
return output;
}
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ return super.paragraph(text);
+ }
+
table(header, body) {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 9823d2611..e0019eb9b 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -19,6 +19,11 @@
background-color: $primary-color;
}
}
+ > h4 {
+ background: #333;
+ padding: 10px 10px;
+ margin-top: 1px;
+ }
}
.menu-icon--right {
vertical-align: top;
@@ -29,10 +34,10 @@
font-size: 13px;
right: -2px;
position: relative;
+ color: #fff;
}
}
&.nav__sub-menu {
- padding: 5px 0;
background: #111;
-webkit-font-smoothing: auto;
li {
@@ -88,6 +93,7 @@
overflow: auto;
background-color: #f1f1f1;
padding: 0 20px 20px;
+ min-height: 600px;
}
.wrapper--fixed {
max-width: 800px;
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index a20d1f48b..b5fcb6145 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -39,6 +39,7 @@
text-overflow: ellipsis;
color: #888;
margin-top: 2px;
+ max-height: 45px;
}
&.popover {
white-space: normal;
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
new file mode 100644
index 000000000..bccea6e0e
--- /dev/null
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -0,0 +1,34 @@
+.markdown__heading {
+ font-weight: bold;
+}
+.markdown__paragraph-inline {
+ display: inline;
+ + .markdown__paragraph-inline {
+ margin-left: 4px;
+ }
+}
+.markdown__table {
+ background: #fff;
+ margin: 5px 0 10px;
+ th, td {
+ padding: 6px 13px;
+ border: 1px solid #ddd;
+ }
+ tbody tr {
+ background: #fff;
+ &:nth-child(2n) {
+ background-color: #f8f8f8;
+ }
+ }
+}
+pre {
+ border: none;
+ background-color: #f7f7f7;
+ margin: 5px 0;
+ .current--user & {
+ background: #fff;
+ }
+ code {
+ color: #c7254e;
+ }
+}
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index 923a6e99b..e704536a8 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -37,6 +37,7 @@
@import "partials/error-bar";
@import "partials/loading";
@import "partials/get-link";
+@import "partials/markdown";
// Responsive Css
@import "partials/responsive";
diff --git a/web/templates/head.html b/web/templates/head.html
index bbd4c2f1a..2b83119d8 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -53,7 +53,7 @@
<script type="text/javascript">
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") {
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
- analytics.load(window.config.SegmentWriteKey);
+ analytics.load(window.config.SegmentDeveloperKey);
var user = window.UserStore.getCurrentUser(true);
if (user) {
analytics.identify(user.id, {
@@ -62,7 +62,6 @@
createdAt: user.create_at,
username: user.username,
team_id: user.team_id,
- team_domain: window.getSubDomain(),
id: user.id
});
}
@@ -74,28 +73,5 @@
analytics.track = function(){};
}
</script>
- <!-- Snowplow starts plowing -->
- <script type="text/javascript">
- if ('{{ .Props.AnalyticsUrl }}'.trim() !== '') {
- ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[];
- p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments)
- };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1;
- n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","//d1fc8wv8zag5ca.cloudfront.net/2.4.2/sp.js","snowplow"));
-
- window.snowplow('newTracker', 'cf', '{{ .Props.AnalyticsUrl }}', {
- appId: window.config.SiteName
- });
-
- var user = window.UserStore.getCurrentUser(true);
- if (user) {
- window.snowplow('setUserId', user.id);
- }
-
- window.snowplow('trackPageView');
- } else {
- window.snowplow = function(){};
- }
- </script>
- <!-- Snowplow stops plowing -->
</head>
{{end}}
diff --git a/web/web.go b/web/web.go
index 0cf45766e..3b36f3d56 100644
--- a/web/web.go
+++ b/web/web.go
@@ -27,7 +27,7 @@ type HtmlTemplatePage api.Page
func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage {
if len(title) > 0 {
- title = utils.Cfg.ServiceSettings.SiteName + " - " + title
+ title = utils.Cfg.TeamSettings.SiteName + " - " + title
}
props := make(map[string]string)
@@ -147,7 +147,6 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.UserId) == 0 {
page := NewHtmlTemplatePage("signup_team", "Signup")
- page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w)
} else {
page := NewHtmlTemplatePage("home", "Home")
@@ -163,7 +162,6 @@ func signup(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
page := NewHtmlTemplatePage("signup_team", "Signup")
- page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w)
}
@@ -195,7 +193,6 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = teamName
- page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w)
}
@@ -211,7 +208,7 @@ func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
data := r.FormValue("d")
hash := r.FormValue("h")
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "")
return
}
@@ -260,7 +257,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
}
} else {
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "")
return
}
@@ -281,7 +278,6 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
page.Props["TeamId"] = props["id"]
page.Props["Data"] = data
page.Props["Hash"] = hash
- page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w)
}
@@ -426,7 +422,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
if len(hash) == 0 || len(data) == 0 {
isResetLink = false
} else {
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) {
c.Err = model.NewAppError("resetPassword", "The reset link does not appear to be valid", "")
return
}
@@ -488,7 +484,7 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data))
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "")
return
}
diff --git a/web/web_test.go b/web/web_test.go
index 1cb1c0a34..165c2ba09 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -25,7 +25,7 @@ func Setup() {
api.StartServer()
api.InitApi()
InitWeb()
- URL = "http://localhost:" + utils.Cfg.ServiceSettings.Port
+ URL = "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress
ApiClient = model.NewClient(URL)
}
}
@@ -195,7 +195,7 @@ func TestIncomingWebhook(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel)
- if utils.Cfg.ServiceSettings.AllowIncomingWebhooks {
+ if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook1 := &model.IncomingWebhook{ChannelId: channel1.Id}
hook1 = ApiClient.Must(ApiClient.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook)