diff options
author | =Corey Hulen <corey@hulen.com> | 2015-09-22 12:12:50 -0700 |
---|---|---|
committer | =Corey Hulen <corey@hulen.com> | 2015-09-22 12:12:50 -0700 |
commit | 88e5a71e8c93b495cedaa07931a4f8052d9f12ed (patch) | |
tree | 603174fc3758d56b8a027b9e1fbe1a5d8690b3e6 | |
parent | 08a3acbb44b043b9bb56f9b96e91432352d06d1a (diff) | |
download | chat-88e5a71e8c93b495cedaa07931a4f8052d9f12ed.tar.gz chat-88e5a71e8c93b495cedaa07931a4f8052d9f12ed.tar.bz2 chat-88e5a71e8c93b495cedaa07931a4f8052d9f12ed.zip |
Adding service settings to admin console
37 files changed, 623 insertions, 289 deletions
diff --git a/api/admin.go b/api/admin.go index ca66b7cb4..568d8f6e8 100644 --- a/api/admin.go +++ b/api/admin.go @@ -35,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 { @@ -82,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 } diff --git a/api/api_test.go b/api/api_test.go index 490f8ab5b..761f3e33f 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -18,7 +18,7 @@ func Setup() { 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 c4684221d..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,14 +457,7 @@ 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) diff --git a/api/file.go b/api/file.go index 61d0df413..c85d241f3 100644 --- a/api/file.go +++ b/api/file.go @@ -399,7 +399,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.ImageSettings.PublicLinkSalt)) { c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "") return } @@ -477,7 +477,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.ImageSettings.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) diff --git a/api/file_benchmark_test.go b/api/file_benchmark_test.go index 251ff7793..f7d5de1d9 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.ImageSettings.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 a0a2f3255..072a3fab1 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -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.ImageSettings.PublicLinkSalt)) Client.LoginByEmail(team2.Name, user2.Email, "pwd") diff --git a/api/server.go b/api/server.go index a2f5fe552..3f23d8df6 100644 --- a/api/server.go +++ b/api/server.go @@ -38,7 +38,7 @@ 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 @@ -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 8802208f7..c9d2412d3 100644 --- a/api/team.go +++ b/api/team.go @@ -38,7 +38,7 @@ func InitTeam(r *mux.Router) { } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { + if !utils.Cfg.EmailSettings.EnableSignUpWithEmail { c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -66,7 +66,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { 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)) @@ -85,7 +85,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { service := params["service"] sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Allow { + if sso != nil && !sso.Enable { c.SetInvalidParam("createTeamFromSSO", "service") return } @@ -142,7 +142,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { } func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { + if !utils.Cfg.EmailSettings.EnableSignUpWithEmail { c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -183,7 +183,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 } @@ -243,7 +243,7 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { } func CreateTeam(c *Context, team *model.Team) *model.Team { - if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { + if !utils.Cfg.EmailSettings.EnableSignUpWithEmail { c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return nil @@ -258,11 +258,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 @@ -488,10 +483,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"]) } diff --git a/api/team_test.go b/api/team_test.go index 48c73c638..cd39dacfe 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"} diff --git a/api/user.go b/api/user.go index ba5323d77..d61afb027 100644 --- a/api/user.go +++ b/api/user.go @@ -58,7 +58,7 @@ func InitUser(r *mux.Router) { } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { + 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 +90,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 } @@ -287,7 +287,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 @@ -1129,7 +1129,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)) @@ -1208,7 +1208,7 @@ 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)) { + 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 } @@ -1357,7 +1357,7 @@ 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) { sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Allow { + if sso != nil && !sso.Enable { c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) c.Err.StatusCode = http.StatusBadRequest return @@ -1385,7 +1385,7 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) { sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Allow { + if sso != nil && !sso.Enable { return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) } diff --git a/api/user_test.go b/api/user_test.go index 7451cb615..34eefce59 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) @@ -814,7 +814,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 { diff --git a/api/web_socket_test.go b/api/web_socket_test.go index 161274ff7..d086308bf 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/config/config.json b/config/config.json index 12926bee5..4b5a16300 100644 --- a/config/config.json +++ b/config/config.json @@ -1,18 +1,11 @@ { "ServiceSettings": { - "Mode": "dev", - "AllowTesting": false, - "UseSSL": false, - "Port": "8065", - "Version": "developer", - "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6", - "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4", - "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t", - "AnalyticsUrl": "", - "AllowedLoginAttempts": 10, - "EnableOAuthServiceProvider": false, + "ListenAddress": ":8065", + "MaximumLoginAttempts": 10, "SegmentDeveloperKey": "", - "GoogleDeveloperKey": "" + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": false, + "EnableTesting": false }, "TeamSettings": { "SiteName": "Mattermost", @@ -32,9 +25,9 @@ "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV" }, "LogSettings": { - "ConsoleEnable": true, + "EnableConsole": true, "ConsoleLevel": "DEBUG", - "FileEnable": true, + "EnableFile": true, "FileLevel": "INFO", "FileFormat": "", "FileLocation": "" @@ -43,6 +36,7 @@ "DriverName": "local", "Directory": "./data/", "EnablePublicLink": true, + "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX", "ThumbnailWidth": 120, "ThumbnailHeight": 100, "PreviewWidth": 1024, @@ -56,7 +50,7 @@ "AmazonS3Region": "" }, "EmailSettings": { - "AllowSignUpWithEmail": true, + "EnableSignUpWithEmail": true, "SendEmailNotifications": false, "RequireEmailVerification": false, "FeedbackName": "", @@ -66,6 +60,8 @@ "SMTPServer": "", "SMTPPort": "", "ConnectionSecurity": "", + "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo", + "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e", "ApplePushServer": "", "ApplePushCertPublic": "", "ApplePushCertPrivate": "" @@ -82,7 +78,7 @@ "ShowFullName": true }, "GitLabSettings": { - "Allow": false, + "Enable": false, "Secret": "", "Id": "", "Scope": "", diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index aceeb95b4..8bd5f1b0a 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -1,81 +1,73 @@ { - "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, - "EnableOAuthServiceProvider": false + "ListenAddress": ":80", + "MaximumLoginAttempts": 10, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": 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": { + "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, @@ -83,22 +75,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 bc42951b8..8bd5f1b0a 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -1,81 +1,73 @@ { - "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, - "EnableOAuthServiceProvider": false + "ListenAddress": ":80", + "MaximumLoginAttempts": 10, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": 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": { + "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, @@ -83,22 +75,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 823e859cf..d13ffa6cb 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" diff --git a/model/config.go b/model/config.go index 876c36e98..7791c4021 100644 --- a/model/config.go +++ b/model/config.go @@ -20,23 +20,16 @@ const ( ) type ServiceSettings struct { - Mode string - AllowTesting bool - UseSSL bool - Port string - Version string - InviteSalt string - PublicLinkSalt string - ResetSalt string - AnalyticsUrl string - AllowedLoginAttempts int - EnableOAuthServiceProvider bool + ListenAddress string + MaximumLoginAttempts int SegmentDeveloperKey string GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableTesting bool } type SSOSettings struct { - Allow bool + Enable bool Secret string Id string Scope string @@ -56,9 +49,9 @@ type SqlSettings struct { } type LogSettings struct { - ConsoleEnable bool + EnableConsole bool ConsoleLevel string - FileEnable bool + EnableFile bool FileLevel string FileFormat string FileLocation string @@ -68,6 +61,7 @@ type ImageSettings struct { DriverName string Directory string EnablePublicLink bool + PublicLinkSalt string ThumbnailWidth uint ThumbnailHeight uint PreviewWidth uint @@ -82,7 +76,7 @@ type ImageSettings struct { } type EmailSettings struct { - AllowSignUpWithEmail bool + EnableSignUpWithEmail bool SendEmailNotifications bool RequireEmailVerification bool FeedbackName string @@ -92,6 +86,8 @@ type EmailSettings struct { SMTPServer string SMTPPort string ConnectionSecurity string + InviteSalt string + PasswordResetSalt string // For Future Use ApplePushServer string diff --git a/utils/config.go b/utils/config.go index 45f62dc19..c2466800e 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 @@ -174,16 +174,15 @@ func getClientProperties(c *model.Config) map[string]string { props["BuildHash"] = model.BuildHash props["SiteName"] = c.TeamSettings.SiteName - props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications) - props["AllowSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.AllowSignUpWithEmail) + props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail) props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail - props["AllowSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Allow) + props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable) props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 72b5d5c9d..ce7d61ca9 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -15,7 +15,7 @@ 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'); export default class AdminController extends React.Component { constructor(props) { @@ -26,7 +26,7 @@ export default class AdminController extends React.Component { this.state = { config: null, - selected: 'team_settings' + selected: 'service_settings' }; } @@ -72,6 +72,8 @@ export default class AdminController extends React.Component { 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} />; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 2b7159e1d..0983c1276 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -41,6 +41,15 @@ export default class AdminSidebar extends React.Component { <li> <a href='#' + className={this.isSelected('service_settings')} + onClick={this.handleClick.bind(this, 'service_settings')} + > + {'Service Settings'} + </a> + </li> + <li> + <a + href='#' className={this.isSelected('team_settings')} onClick={this.handleClick.bind(this, 'team_settings')} > diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index a87dfc4da..d94859cdd 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -3,6 +3,7 @@ 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) { @@ -12,6 +13,8 @@ export default class EmailSettings extends React.Component { 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, @@ -38,7 +41,7 @@ export default class EmailSettings extends React.Component { buildConfig() { var config = this.props.config; - config.EmailSettings.AllowSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked; + 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; @@ -49,9 +52,36 @@ export default class EmailSettings extends React.Component { 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, 31); + 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, 31); + 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, 31); + 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, 31); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + handleTestConnection(e) { e.preventDefault(); $('#connection-button').button('loading'); @@ -166,7 +196,7 @@ export default class EmailSettings extends React.Component { name='allowSignUpWithEmail' value='true' ref='allowSignUpWithEmail' - defaultChecked={this.props.config.EmailSettings.AllowSignUpWithEmail} + defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail} onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')} /> {'true'} @@ -176,7 +206,7 @@ export default class EmailSettings extends React.Component { type='radio' name='allowSignUpWithEmail' value='false' - defaultChecked={!this.props.config.EmailSettings.AllowSignUpWithEmail} + defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail} onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')} /> {'false'} @@ -432,6 +462,68 @@ export default class EmailSettings extends React.Component { </div> <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> + </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 diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index c0cbb5aa6..80da0a47f 100644 --- a/web/react/components/admin_console/image_settings.jsx +++ b/web/react/components/admin_console/image_settings.jsx @@ -3,6 +3,7 @@ var Client = require('../../utils/client.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); +var crypto = require('crypto'); export default class ImageSettings extends React.Component { constructor(props) { @@ -10,6 +11,7 @@ export default class ImageSettings extends React.Component { this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.handleGenerate = this.handleGenerate.bind(this); this.state = { saveNeeded: false, @@ -28,6 +30,13 @@ export default class ImageSettings extends React.Component { this.setState(s); } + handleGenerate(e) { + e.preventDefault(); + React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 31); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + handleSubmit(e) { e.preventDefault(); $('#save-button').button('loading'); @@ -41,6 +50,13 @@ export default class ImageSettings extends React.Component { config.ImageSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; config.ImageSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; + config.ImageSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); + + if (config.ImageSettings.PublicLinkSalt === '') { + config.ImageSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 31); + React.findDOMNode(this.refs.PublicLinkSalt).value = config.ImageSettings.PublicLinkSalt; + } + var thumbnailWidth = 120; if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) { thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10); @@ -425,6 +441,35 @@ export default class ImageSettings extends React.Component { </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.ImageSettings.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 diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx index 2707ce6b6..d66801431 100644 --- a/web/react/components/admin_console/log_settings.jsx +++ b/web/react/components/admin_console/log_settings.jsx @@ -12,8 +12,8 @@ export default class LogSettings extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.state = { - consoleEnable: this.props.config.LogSettings.ConsoleEnable, - fileEnable: this.props.config.LogSettings.FileEnable, + consoleEnable: this.props.config.LogSettings.EnableConsole, + fileEnable: this.props.config.LogSettings.EnableFile, saveNeeded: false, serverError: null }; @@ -46,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(); @@ -58,8 +58,8 @@ export default class LogSettings extends React.Component { () => { AsyncClient.getConfig(); this.setState({ - consoleEnable: config.LogSettings.ConsoleEnable, - fileEnable: config.LogSettings.FileEnable, + consoleEnable: config.LogSettings.EnableConsole, + fileEnable: config.LogSettings.EnableFile, serverError: null, saveNeeded: false }); @@ -67,8 +67,8 @@ export default class LogSettings extends React.Component { }, (err) => { this.setState({ - consoleEnable: config.LogSettings.ConsoleEnable, - fileEnable: config.LogSettings.FileEnable, + consoleEnable: config.LogSettings.EnableConsole, + fileEnable: config.LogSettings.EnableFile, serverError: err.message, saveNeeded: true }); @@ -110,7 +110,7 @@ export default class LogSettings extends React.Component { name='consoleEnable' value='true' ref='consoleEnable' - defaultChecked={this.props.config.LogSettings.ConsoleEnable} + defaultChecked={this.props.config.LogSettings.EnableConsole} onChange={this.handleChange.bind(this, 'console_true')} /> {'true'} @@ -120,7 +120,7 @@ export default class LogSettings extends React.Component { type='radio' name='consoleEnable' value='false' - defaultChecked={!this.props.config.LogSettings.ConsoleEnable} + defaultChecked={!this.props.config.LogSettings.EnableConsole} onChange={this.handleChange.bind(this, 'console_false')} /> {'false'} @@ -166,7 +166,7 @@ export default class LogSettings extends React.Component { name='fileEnable' ref='fileEnable' value='true' - defaultChecked={this.props.config.LogSettings.FileEnable} + defaultChecked={this.props.config.LogSettings.EnableFile} onChange={this.handleChange.bind(this, 'file_true')} /> {'true'} @@ -176,7 +176,7 @@ export default class LogSettings extends React.Component { type='radio' name='fileEnable' value='false' - defaultChecked={!this.props.config.LogSettings.FileEnable} + defaultChecked={!this.props.config.LogSettings.EnableFile} onChange={this.handleChange.bind(this, 'file_false')} /> {'false'} 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..fcb8f800d --- /dev/null +++ b/web/react/components/admin_console/service_settings.jsx @@ -0,0 +1,262 @@ +// 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.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='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 index 35810f7ee..cac017770 100644 --- a/web/react/components/admin_console/sql_settings.jsx +++ b/web/react/components/admin_console/sql_settings.jsx @@ -207,7 +207,7 @@ export default class SqlSettings extends React.Component { className='form-control' id='AtRestEncryptKey' ref='AtRestEncryptKey' - placeholder='Ex "10"' + placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' defaultValue={this.props.config.SqlSettings.AtRestEncryptKey} onChange={this.handleChange} /> diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index b12c5d988..8cc4f1483 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -95,7 +95,7 @@ export default class Login extends React.Component { } let loginMessage = []; - if (global.window.config.AllowSignUpWithGitLab === 'true') { + if (global.window.config.EnableSignUpWithGitLab === 'true') { loginMessage.push( <a className='btn btn-custom-login gitlab' @@ -113,7 +113,7 @@ export default class Login extends React.Component { } let emailSignup; - if (global.window.config.AllowSignUpWithEmail === 'true') { + if (global.window.config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className={'form-group' + errorClass}> diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index d08608c9b..7f320e0b2 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -14,19 +14,19 @@ export default class TeamSignUp extends React.Component { var count = 0; - if (global.window.config.AllowSignUpWithEmail === 'true') { + if (global.window.config.EnableSignUpWithEmail === 'true') { count = count + 1; } - if (global.window.config.AllowSignUpWithGitLab === 'true') { + if (global.window.config.EnableSignUpWithGitLab === 'true') { count = count + 1; } if (count > 1) { this.state = {page: 'choose'}; - } else if (global.window.config.AllowSignUpWithEmail === 'true') { + } else if (global.window.config.EnableSignUpWithEmail === 'true') { this.state = {page: 'email'}; - } else if (global.window.config.AllowSignUpWithGitLab === 'true') { + } else if (global.window.config.EnableSignUpWithGitLab === 'true') { this.state = {page: 'gitlab'}; } } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 237169f17..4dad1ef4f 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -161,7 +161,7 @@ export default class SignupUserComplete extends React.Component { ); var signupMessage = []; - if (global.window.config.AllowSignUpWithGitLab === 'true') { + if (global.window.config.EnableSignUpWithGitLab === 'true') { signupMessage.push( <a className='btn btn-custom-login gitlab' @@ -174,7 +174,7 @@ export default class SignupUserComplete extends React.Component { } var emailSignup; - if (global.window.config.AllowSignUpWithEmail === 'true') { + if (global.window.config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className='inner__content'> diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index 4aeae8f08..b8264b887 100644 --- a/web/react/components/team_signup_choose_auth.jsx +++ b/web/react/components/team_signup_choose_auth.jsx @@ -8,7 +8,7 @@ export default class ChooseAuthPage extends React.Component { } render() { var buttons = []; - if (global.window.config.AllowSignUpWithGitLab === 'true') { + if (global.window.config.EnableSignUpWithGitLab === 'true') { buttons.push( <a className='btn btn-custom-login gitlab btn-full' @@ -26,7 +26,7 @@ export default class ChooseAuthPage extends React.Component { ); } - if (global.window.config.AllowSignUpWithEmail === 'true') { + if (global.window.config.EnableSignUpWithEmail === 'true') { buttons.push( <a className='btn btn-custom-login email btn-full' diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index b3fb28e99..b01e2bb24 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(); } diff --git a/web/templates/head.html b/web/templates/head.html index af5c86bba..39d5a262c 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -77,28 +77,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 86769dd54..9847d0b5e 100644 --- a/web/web.go +++ b/web/web.go @@ -204,7 +204,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 } @@ -253,7 +253,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 } @@ -414,7 +414,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 } @@ -476,7 +476,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 3da7eb2dc..c1d5dd6f9 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) } } |