From ed9a2da83b3b77e7dd0314eaa92082ac8a2a9a9c Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 15:11:56 -0700 Subject: [PATCH 01/15] Adding email to admin console --- api/admin.go | 27 + api/admin_test.go | 28 + api/team.go | 11 +- api/user.go | 4 +- config/config.json | 13 +- model/client.go | 9 + model/config.go | 21 +- utils/config.go | 29 +- utils/mail.go | 60 +- .../admin_console/email_settings.jsx | 546 +++++++++++------- .../components/admin_console/log_settings.jsx | 50 +- web/react/components/invite_member_modal.jsx | 2 +- web/react/components/login.jsx | 8 +- web/react/components/signup_team.jsx | 48 +- web/react/components/signup_user_complete.jsx | 9 +- .../components/team_signup_choose_auth.jsx | 20 +- .../team_signup_send_invites_page.jsx | 2 +- .../components/user_settings_general.jsx | 2 +- web/react/pages/login.jsx | 1 - web/react/pages/signup_team.jsx | 6 +- web/react/pages/signup_user_complete.jsx | 1 - web/react/utils/client.jsx | 15 + web/web.go | 4 - 23 files changed, 563 insertions(+), 353 deletions(-) diff --git a/api/admin.go b/api/admin.go index 6465977550..ca66b7cb40 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) { @@ -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", "


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 e1778b5ac2..c74fbf6e53 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -122,3 +122,31 @@ func TestSaveConfig(t *testing.T) { } } } + +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/team.go b/api/team.go index 92fcbff934..f0025fdbdc 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.ServiceSettings.DisableEmailSignUp { + if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -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))) } @@ -147,7 +144,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.AllowSignUpWithEmail { c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -257,7 +254,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.AllowSignUpWithEmail { c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return nil diff --git a/api/user.go b/api/user.go index 0a54b6a5d1..352e1f0e1f 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.ServiceSettings.DisableEmailSignUp { + if !utils.Cfg.EmailSettings.AllowSignUpWithEmail { c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -324,7 +324,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 diff --git a/config/config.json b/config/config.json index 38948641c2..4b0c0fe517 100644 --- a/config/config.json +++ b/config/config.json @@ -21,7 +21,6 @@ "UseLocalStorage": true, "StorageDirectory": "./data/", "AllowedLoginAttempts": 10, - "DisableEmailSignUp": false, "EnableOAuthServiceProvider": false }, "SqlSettings": { @@ -51,14 +50,16 @@ "InitialFont": "luximbi.ttf" }, "EmailSettings": { - "ByPassEmail": true, + "AllowSignUpWithEmail": true, + "SendEmailNotifications": false, + "RequireEmailVerification": false, + "FeedbackName": "", + "FeedbackEmail": "", "SMTPUsername": "", "SMTPPassword": "", "SMTPServer": "", - "UseTLS": false, - "UseStartTLS": false, - "FeedbackEmail": "", - "FeedbackName": "", + "SMTPPort": "", + "ConnectionSecurity": "", "ApplePushServer": "", "ApplePushCertPublic": "", "ApplePushCertPrivate": "" diff --git a/model/client.go b/model/client.go index f9127719f1..823e859cf1 100644 --- a/model/client.go +++ b/model/client.go @@ -403,6 +403,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 3b333dbe14..5240caf55d 100644 --- a/model/config.go +++ b/model/config.go @@ -22,7 +22,6 @@ type ServiceSettings struct { UseLocalStorage bool StorageDirectory string AllowedLoginAttempts int - DisableEmailSignUp bool EnableOAuthServiceProvider bool } @@ -73,14 +72,18 @@ type ImageSettings struct { } type EmailSettings struct { - ByPassEmail bool - SMTPUsername string - SMTPPassword string - SMTPServer string - UseTLS bool - UseStartTLS bool - FeedbackEmail string - FeedbackName string + AllowSignUpWithEmail bool + SendEmailNotifications bool + RequireEmailVerification bool + FeedbackName string + FeedbackEmail string + SMTPUsername string + SMTPPassword string + SMTPServer string + SMTPPort string + ConnectionSecurity string + + // For Future Use ApplePushServer string ApplePushCertPublic string ApplePushCertPrivate string diff --git a/utils/config.go b/utils/config.go index dd2c179777..f3e93ef11a 100644 --- a/utils/config.go +++ b/utils/config.go @@ -176,18 +176,24 @@ func getClientProperties(c *model.Config) map[string]string { props["BuildHash"] = model.BuildHash props["SiteName"] = c.ServiceSettings.SiteName - props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail) + props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl + props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) + + props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications) + props["AllowSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.AllowSignUpWithEmail) props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail + + props["AllowSignUpWithGitLab"] = strconv.FormatBool(false) + 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["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) return props } @@ -200,21 +206,6 @@ func IsS3Configured() bool { return true } -func GetAllowedAuthServices() []string { - authServices := []string{} - for name, service := range Cfg.SSOSettings { - if service.Allow { - authServices = append(authServices, name) - } - } - - if !Cfg.ServiceSettings.DisableEmailSignUp { - authServices = append(authServices, "email") - } - - return authServices -} - func IsServiceAllowed(s string) bool { if len(s) == 0 { return false diff --git a/utils/mail.go b/utils/mail.go index 7cb1786267..9d3db96401 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -15,43 +15,28 @@ 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) +const ( + CONN_SECURITY_NONE = "" + CONN_SECURITY_TLS = "TLS" + CONN_SECURITY_STARTTLS = "STARTTLS" +) +func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { var conn net.Conn var err error - if Cfg.EmailSettings.UseTLS { + if config.EmailSettings.ConnectionSecurity == 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 +45,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 == 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 == CONN_SECURITY_TLS { tlsconfig := &tls.Config{ InsecureSkipVerify: true, - ServerName: host, + ServerName: config.EmailSettings.SMTPServer, } c.StartTLS(tlsconfig) if err = c.Auth(auth); err != nil { @@ -88,12 +72,16 @@ func newSMTPClient(conn net.Conn) (*smtp.Client, *model.AppError) { } func SendMail(to, subject, body string) *model.AppError { + return SendMailUsingConfig(to, subject, body, Cfg) +} - if len(Cfg.EmailSettings.SMTPServer) == 0 || Cfg.EmailSettings.ByPassEmail { +func SendMailUsingConfig(to, subject, body string, config *model.Config) *model.AppError { + + 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 +98,13 @@ func SendMail(to, subject, body string) *model.AppError { } message += "\r\n" + body + "" - 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/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index e8fb258583..2e107f1ba7 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -1,15 +1,148 @@ // 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 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.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.AllowSignUpWithEmail = 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(); + return config; + } + + 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 + }); + }, + (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 =
; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + var emailSuccess = ''; + if (this.state.emailSuccess) { + emailSuccess = ( +
+ {'No errors were reported while sending an email. Please check your inbox to make sure.'} +
+ ); + } + + var emailFail = ''; + if (this.state.emailFail) { + emailSuccess = ( +
+ {'Connection unsuccessful: ' + this.state.emailFail} +
+ ); + } + return (

{'Email Settings'}

@@ -17,149 +150,130 @@ export default class EmailSettings extends React.Component { className='form-horizontal' role='form' > +
-

{'This is some sample help text for the Bypass Email field'}

+

{'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.'}

+
- -
-
{' This is some error text for the Bypass Email field'}
-
-

{'This is some sample help text for the SMTP username field'}

+ + +

{'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.'}

+
- + + +

{'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.'}

+
-
- - {'Test Connection'} - -
{' Connection successful'}
-
{' Connection unsuccessful'}
-
-
-
-
- -
- - -
-
-
- -
- - +

{'Name displayed on email account used when sending notification emails from Mattermost.'}

+
+
-
-
-
-
-
- -
+

{' Obtain this credential from administrator setting up your email server.'}

-
-
- + +
+ +
+ +

{'Location of SMTP email server.'}

+
+
+ +
+ +
+ +

{'Port of SMTP email server.'}

+
+
+ +
+ +
+ +
+ + + + +
{'None'}{'Mattermost will send email over an unsecure connection.'}
{'TLS'}{'Encrypts the communication between Mattermost and your email server.'}
{'STARTTLS'}{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}
-
-
-
- -
- -

{'This is some sample help text for the Apple push server field'}

-
-
-
- -
- -
-
-
- -
- -
-
-
+
+ + {emailSuccess} + {emailFail}
+ {serverError}
+
); } -} \ 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/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx index 4e3db8f68a..2707ce6b68 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.ConsoleEnable, + fileEnable: this.props.config.LogSettings.FileEnable, 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) { @@ -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.ConsoleEnable, + fileEnable: config.LogSettings.FileEnable, + serverError: null, + saveNeeded: false + }); $('#save-button').button('reset'); }, (err) => { - this.setState({serverError: err.message, saveNeeded: true}); + this.setState({ + consoleEnable: config.LogSettings.ConsoleEnable, + fileEnable: config.LogSettings.FileEnable, + serverError: err.message, + saveNeeded: true + }); $('#save-button').button('reset'); } ); @@ -81,7 +111,7 @@ export default class LogSettings extends React.Component { value='true' ref='consoleEnable' defaultChecked={this.props.config.LogSettings.ConsoleEnable} - onChange={this.handleChange} + onChange={this.handleChange.bind(this, 'console_true')} /> {'true'} @@ -91,7 +121,7 @@ export default class LogSettings extends React.Component { name='consoleEnable' value='false' defaultChecked={!this.props.config.LogSettings.ConsoleEnable} - onChange={this.handleChange} + onChange={this.handleChange.bind(this, 'console_false')} /> {'false'} @@ -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} > @@ -136,7 +167,7 @@ export default class LogSettings extends React.Component { ref='fileEnable' value='true' defaultChecked={this.props.config.LogSettings.FileEnable} - onChange={this.handleChange} + onChange={this.handleChange.bind(this, 'file_true')} /> {'true'} @@ -146,7 +177,7 @@ export default class LogSettings extends React.Component { name='fileEnable' value='false' defaultChecked={!this.props.config.LogSettings.FileEnable} - onChange={this.handleChange} + onChange={this.handleChange.bind(this, 'file_false')} /> {'false'} @@ -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} > @@ -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} />

{'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.'}

@@ -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} />

{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'} diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 650a725163..acf6db9dc7 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 }; } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index ffc07a4ddb..cb7bc88359 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -95,10 +95,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.AllowSignUpWithGitLab) { loginMessage.push(

@@ -206,10 +204,8 @@ export default class Login extends React.Component { Login.defaultProps = { teamName: '', teamDisplayName: '', - authServices: '' }; Login.propTypes = { teamName: React.PropTypes.string, teamDisplayName: React.PropTypes.string, - authServices: React.PropTypes.string }; diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index bf08e6508f..91d79b9194 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -4,7 +4,7 @@ const ChoosePage = require('./team_signup_choose_auth.jsx'); const EmailSignUpPage = require('./team_signup_with_email.jsx'); const SSOSignupPage = require('./team_signup_with_sso.jsx'); -const Constants = require('../utils/constants.jsx'); +var Constants = require('../utils/constants.jsx'); export default class TeamSignUp extends React.Component { constructor(props) { @@ -12,38 +12,32 @@ 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: ''}; + if (global.window.config.AllowSignUpWithEmail && global.window.config.AllowSignUpWithGitLab) { + this.state = {page: 'choose'}; + } else if (global.window.config.AllowSignUpWithEmail) { + this.state = {page: 'email'}; + } else if (global.window.config.AllowSignUpWithGitLab) { + 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 ( + + ); + } + if (this.state.page === 'email') { return ; - } else if (this.state.page === 'service' && this.state.service !== '') { - return ; + } else if (this.state.page === 'gitlab') { + return ; } - - return ( - - ); } } - -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 19c3b2d220..08d5e2f48f 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -161,11 +161,8 @@ export default class SignupUserComplete extends React.Component {
); - // 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.AllowSignUpWithGitLab) { signupMessage.push(
@@ -262,7 +259,6 @@ SignupUserComplete.defaultProps = { teamId: '', email: '', data: null, - authServices: '', teamDisplayName: '' }; SignupUserComplete.propTypes = { @@ -271,6 +267,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_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index d3107c5c71..cfd45edb37 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.AllowSignUpWithGitLab) { buttons.push( - Create new team with GitLab Account + {'Create new team with GitLab Account'} ); } - if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) { + if (global.window.config.AllowSignUpWithEmail) { buttons.push( - Create new team with email address + {'Create new team with email address'} ); } if (buttons.length === 0) { - buttons = No sign-up methods configured, please contact your system administrator.; + buttons = {'No sign-up methods configured, please contact your system administrator.'}; } 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 41ac98303d..ee51debb7d 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 }; if (!this.state.emailEnabled) { diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx index 66cde6ca27..895601bd2c 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/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; 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/pages/login.jsx b/web/react/pages/login.jsx index 830f622fa6..f78e0f37af 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -8,7 +8,6 @@ function setupLoginPage(props) { , document.getElementById('login') ); diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index 427daf5773..d0e08f446b 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( - , + , document.getElementById('signup-team') ); } diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index 112aaa3f2a..cc7607187e 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/utils/client.jsx b/web/react/utils/client.jsx index c9eb09c00f..b3fb28e995 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -334,6 +334,21 @@ 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 getMeSynchronous(success, error) { var currentUser = null; $.ajax({ diff --git a/web/web.go b/web/web.go index 305e4f1999..2264d5053a 100644 --- a/web/web.go +++ b/web/web.go @@ -143,7 +143,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") @@ -159,7 +158,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) } @@ -191,7 +189,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) } @@ -277,7 +274,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) } From 9baef5a6a033fea2ff5499ee9969c6cd65efef0c Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 15:17:02 -0700 Subject: [PATCH 02/15] Fixing button --- web/react/components/admin_console/email_settings.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index 2e107f1ba7..a87dfc4da6 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -68,6 +68,7 @@ export default class EmailSettings extends React.Component { emailSuccess: true, emailFail: null }); + $('#connection-button').button('reset'); }, (err) => { this.setState({ From e863096358dd64ecf2de6efeec3db132cdc8d6b9 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 15:34:09 -0700 Subject: [PATCH 03/15] Fixing broken signup pages --- web/react/components/invite_member_modal.jsx | 2 +- web/react/components/login.jsx | 4 ++-- web/react/components/signup_team.jsx | 16 +++++++++++++--- web/react/components/signup_user_complete.jsx | 4 ++-- web/react/components/team_signup_choose_auth.jsx | 4 ++-- .../components/team_signup_send_invites_page.jsx | 2 +- web/react/components/user_profile.jsx | 2 +- web/react/components/user_settings_general.jsx | 2 +- web/react/components/view_image.jsx | 2 +- 9 files changed, 24 insertions(+), 14 deletions(-) diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index acf6db9dc7..395b986301 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.SendEmailNotifications + emailEnabled: global.window.config.SendEmailNotifications === 'true' }; } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index cb7bc88359..72fae9149b 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -96,7 +96,7 @@ export default class Login extends React.Component { } let loginMessage = []; - if (global.window.config.AllowSignUpWithGitLab) { + if (global.window.config.AllowSignUpWithGitLab === 'true') { loginMessage.push(
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 91d79b9194..d08608c9bf 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -12,11 +12,21 @@ export default class TeamSignUp extends React.Component { this.updatePage = this.updatePage.bind(this); - if (global.window.config.AllowSignUpWithEmail && global.window.config.AllowSignUpWithGitLab) { + var count = 0; + + if (global.window.config.AllowSignUpWithEmail === 'true') { + count = count + 1; + } + + if (global.window.config.AllowSignUpWithGitLab === 'true') { + count = count + 1; + } + + if (count > 1) { this.state = {page: 'choose'}; - } else if (global.window.config.AllowSignUpWithEmail) { + } else if (global.window.config.AllowSignUpWithEmail === 'true') { this.state = {page: 'email'}; - } else if (global.window.config.AllowSignUpWithGitLab) { + } else if (global.window.config.AllowSignUpWithGitLab === 'true') { this.state = {page: 'gitlab'}; } } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 08d5e2f48f..5b11829386 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -162,7 +162,7 @@ export default class SignupUserComplete extends React.Component { ); var signupMessage = []; - if (global.window.config.AllowSignUpWithGitLab) { + if (global.window.config.AllowSignUpWithGitLab === 'true') { signupMessage.push(
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index cfd45edb37..4aeae8f084 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) { + if (global.window.config.AllowSignUpWithGitLab === 'true') { buttons.push( '; - if (!global.window.config.ShowEmailAddress) { + if (!global.window.config.ShowEmailAddress === 'true') { dataContent += '
Email not shared
'; } else { dataContent += '
'; diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx index 895601bd2c..cb60691a15 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/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.SendEmailNotifications; + 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/view_image.jsx b/web/react/components/view_image.jsx index f7c9803960..a37eb67759 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.AllowPublicLink === 'true') { publicLink = (
Date: Mon, 21 Sep 2015 17:34:13 -0700 Subject: [PATCH 04/15] Adding image properties --- api/export.go | 6 +- api/file.go | 62 +-- api/file_test.go | 24 +- api/user.go | 6 +- api/user_test.go | 16 +- config/config.json | 16 +- model/config.go | 39 +- utils/config.go | 12 +- utils/mail.go | 12 +- .../admin_console/admin_controller.jsx | 3 + .../admin_console/admin_sidebar.jsx | 9 + .../admin_console/image_settings.jsx | 417 ++++++++++++++++++ 12 files changed, 524 insertions(+), 98 deletions(-) create mode 100644 web/react/components/admin_console/image_settings.jsx diff --git a/api/export.go b/api/export.go index 9345f892fb..6d76982824 100644 --- a/api/export.go +++ b/api/export.go @@ -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.ImageSettings.Directory + "teams/" + teamId - if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { + if utils.Cfg.ImageSettings.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.ImageSettings.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 467bf53380..69303f5f8b 100644 --- a/api/file.go +++ b/api/file.go @@ -68,8 +68,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.ImageSettings.DriverName) == 0 { + c.Err = model.NewAppError("uploadFile", "Unable to upload file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -293,8 +293,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.ImageSettings.DriverName) == 0 { + c.Err = model.NewAppError("uploadFile", "Unable to get file info. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -356,8 +356,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.ImageSettings.DriverName) == 0 { + c.Err = model.NewAppError("uploadFile", "Unable to get file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -441,17 +441,17 @@ func asyncGetFile(path string, fileData chan []byte) { } func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { + if len(utils.Cfg.ImageSettings.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.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. ", "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - props := model.MapFromJson(r.Body) filename := props["filename"] @@ -510,13 +510,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.ImageSettings.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.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.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.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) ext := filepath.Ext(path) @@ -533,12 +533,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.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.ImageSettings.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.ImageSettings.Directory+path, f, 0644); err != nil { return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error()) } } else { @@ -550,13 +550,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.ImageSettings.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.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.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.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) // try to get the file from S3 with some basic retry logic tries := 0 @@ -572,8 +572,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.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if f, err := ioutil.ReadFile(utils.Cfg.ImageSettings.Directory + path); err != nil { return nil, model.NewAppError("readFile", "Encountered an error reading from local server storage", err.Error()) } else { return f, nil @@ -584,14 +584,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.ImageSettings.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.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.ImageSettings.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.ImageSettings.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_test.go b/api/file_test.go index eb1fcf2ceb..a62bdc83ea 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -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.AWSSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey - 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.AWSSettingsAmazonS3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) // wait a bit for files to ready time.Sleep(5 * time.Second) @@ -264,11 +264,11 @@ func TestGetFile(t *testing.T) { if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey - 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.AWSSettingsAmazonS3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] @@ -413,11 +413,11 @@ func TestGetPublicLink(t *testing.T) { if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { // 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.AWSSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey - 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.AWSSettingsAmazonS3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] diff --git a/api/user.go b/api/user.go index 352e1f0e1f..32dfa7dfba 100644 --- a/api/user.go +++ b/api/user.go @@ -704,7 +704,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.ImageSettings.DriverName) == 0 { var err *model.AppError if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { c.Err = err @@ -741,8 +741,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.ImageSettings.DriverName) == 0 { + c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } diff --git a/api/user_test.go b/api/user_test.go index 986365bd0a..a9529e9370 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -354,11 +354,11 @@ func TestUserCreateImage(t *testing.T) { if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey - 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.AWSSettingsAmazonS3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) @@ -452,11 +452,11 @@ func TestUserUploadProfileImage(t *testing.T) { if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey - 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.AWSSettingsAmazonS3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) diff --git a/config/config.json b/config/config.json index 4b0c0fe517..f9c49678fb 100644 --- a/config/config.json +++ b/config/config.json @@ -18,8 +18,6 @@ "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4", "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t", "AnalyticsUrl": "", - "UseLocalStorage": true, - "StorageDirectory": "./data/", "AllowedLoginAttempts": 10, "EnableOAuthServiceProvider": false }, @@ -34,20 +32,20 @@ "Trace": false, "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" }, - "AWSSettings": { - "S3AccessKeyId": "", - "S3SecretAccessKey": "", - "S3Bucket": "", - "S3Region": "" - }, "ImageSettings": { + "DriverName": "local", + "Directory": "./data/", "ThumbnailWidth": 120, "ThumbnailHeight": 100, "PreviewWidth": 1024, "PreviewHeight": 0, "ProfileWidth": 128, "ProfileHeight": 128, - "InitialFont": "luximbi.ttf" + "InitialFont": "luximbi.ttf", + "AmazonS3AccessKeyId": "", + "AmazonS3SecretAccessKey": "", + "AmazonS3Bucket": "", + "AmazonS3Region": "" }, "EmailSettings": { "AllowSignUpWithEmail": true, diff --git a/model/config.go b/model/config.go index 5240caf55d..2847d17da5 100644 --- a/model/config.go +++ b/model/config.go @@ -8,6 +8,15 @@ import ( "io" ) +const ( + CONN_SECURITY_NONE = "" + CONN_SECURITY_TLS = "TLS" + CONN_SECURITY_STARTTLS = "STARTTLS" + + IMAGE_DRIVER_LOCAL = "local" + IMAGE_DRIVER_S3 = "amazons3" +) + type ServiceSettings struct { SiteName string Mode string @@ -19,8 +28,6 @@ type ServiceSettings struct { PublicLinkSalt string ResetSalt string AnalyticsUrl string - UseLocalStorage bool - StorageDirectory string AllowedLoginAttempts int EnableOAuthServiceProvider bool } @@ -54,21 +61,20 @@ type LogSettings struct { 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 + DriverName string + Directory 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 { @@ -123,7 +129,6 @@ type Config struct { LogSettings LogSettings ServiceSettings ServiceSettings SqlSettings SqlSettings - AWSSettings AWSSettings ImageSettings ImageSettings EmailSettings EmailSettings RateLimitSettings RateLimitSettings diff --git a/utils/config.go b/utils/config.go index f3e93ef11a..c0e0391374 100644 --- a/utils/config.go +++ b/utils/config.go @@ -198,13 +198,13 @@ func getClientProperties(c *model.Config) map[string]string { return props } -func IsS3Configured() bool { - if Cfg.AWSSettings.S3AccessKeyId == "" || Cfg.AWSSettings.S3SecretAccessKey == "" || Cfg.AWSSettings.S3Region == "" || Cfg.AWSSettings.S3Bucket == "" { - return false - } +// func IsS3Configured() bool { +// if Cfg.AWSSettings.AmazonS3AccessKeyId == "" || Cfg.AWSSettingsAmazonS3SecretAccessKey == "" || Cfg.AWSSettingsAmazonS3Region == "" || Cfg.AWSSettingsAmazonS3Bucket == "" { +// return false +// } - return true -} +// return true +// } func IsServiceAllowed(s string) bool { if len(s) == 0 { diff --git a/utils/mail.go b/utils/mail.go index 9d3db96401..dd975155d2 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -15,17 +15,11 @@ import ( "time" ) -const ( - CONN_SECURITY_NONE = "" - CONN_SECURITY_TLS = "TLS" - CONN_SECURITY_STARTTLS = "STARTTLS" -) - func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { var conn net.Conn var err error - if config.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS { + if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: config.EmailSettings.SMTPServer, @@ -54,11 +48,11 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap // GO does not support plain auth over a non encrypted connection. // so if not tls then no auth auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) - if config.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS { + 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 config.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS { + } else if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS { tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: config.EmailSettings.SMTPServer, diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index e82fe1b767..0dc0ec45be 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -9,6 +9,7 @@ var LoadingScreen = require('../loading_screen.jsx'); var EmailSettingsTab = require('./email_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx'); var LogsTab = require('./logs.jsx'); +var ImageSettingsTab = require('./image_settings.jsx'); export default class AdminController extends React.Component { constructor(props) { @@ -53,6 +54,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'logs') { tab = ; + } else if (this.state.selected === 'image_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index a6e689490e..5544f9c6d0 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -65,6 +65,15 @@ export default class AdminSidebar extends React.Component { {'Logs'} +
  • + + {'Image Settings'} + +
  • 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 0000000000..9a7de266d0 --- /dev/null +++ b/web/react/components/admin_console/image_settings.jsx @@ -0,0 +1,417 @@ +// 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 ImageSettings 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, + DriverName: this.props.config.ImageSettings.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); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.ImageSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; + config.ImageSettings.Directory = React.findDOMNode(this.refs.Directory).value; + config.ImageSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; + config.ImageSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; + config.ImageSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; + config.ImageSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; + + var thumbnailWidth = 120; + if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) { + thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10); + } + config.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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 =
    ; + } + + 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 ( +
    +

    {'Image Settings'}

    +
    + +
    + +
    + +
    +
    + +
    + +
    + +

    {'Directory to which image files are written. If blank, will be set to ./data/.'}

    +
    +
    + +
    + +
    + +

    {'Obtain this credential from your Amazon EC2 administrator.'}

    +
    +
    + +
    + +
    + +

    {'Obtain this credential from your Amazon EC2 administrator.'}

    +
    +
    + +
    + +
    + +

    {'Name you selected for your S3 bucket in AWS.'}

    +
    +
    + +
    + +
    + +

    {'AWS region you selected for creating your S3 bucket.'}

    +
    +
    + +
    + +
    + +

    {'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.'}

    +
    +
    + +
    + +
    + +

    {'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.'}

    +
    +
    + +
    + +
    + +

    {'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}

    +
    +
    + +
    + +
    + +

    {'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.'}

    +
    +
    + +
    + +
    + +

    {'Width of profile picture.'}

    +
    +
    + +
    + +
    + +

    {'Height of profile picture.'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +ImageSettings.propTypes = { + config: React.PropTypes.object +}; From f8e2a43d09b4c2ec95e9565f9e62281226a1677d Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 17:52:30 -0700 Subject: [PATCH 05/15] Fixing eslint errors --- web/react/components/login.jsx | 5 ++--- web/react/components/signup_user_complete.jsx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 72fae9149b..b12c5d9888 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) { @@ -203,9 +202,9 @@ export default class Login extends React.Component { Login.defaultProps = { teamName: '', - teamDisplayName: '', + teamDisplayName: '' }; Login.propTypes = { teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string }; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 5b11829386..237169f172 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) { From cae5dbd245f05e9441156a3918ef796d92ecf331 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 18:23:10 -0700 Subject: [PATCH 06/15] Adding privacy to admin console --- model/config.go | 2 - utils/config.go | 2 - .../admin_console/admin_controller.jsx | 3 + .../admin_console/admin_sidebar.jsx | 9 + .../admin_console/privacy_settings.jsx | 163 ++++++++++++++++++ 5 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 web/react/components/admin_console/privacy_settings.jsx diff --git a/model/config.go b/model/config.go index 2847d17da5..3c0ada423f 100644 --- a/model/config.go +++ b/model/config.go @@ -105,8 +105,6 @@ type RateLimitSettings struct { type PrivacySettings struct { ShowEmailAddress bool - ShowPhoneNumber bool - ShowSkypeId bool ShowFullName bool } diff --git a/utils/config.go b/utils/config.go index c0e0391374..cef99d7d97 100644 --- a/utils/config.go +++ b/utils/config.go @@ -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 } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 0dc0ec45be..9db1c945e1 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -10,6 +10,7 @@ var EmailSettingsTab = require('./email_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx'); var LogsTab = require('./logs.jsx'); var ImageSettingsTab = require('./image_settings.jsx'); +var PrivacySettingsTab = require('./privacy_settings.jsx'); export default class AdminController extends React.Component { constructor(props) { @@ -56,6 +57,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'image_settings') { tab = ; + } else if (this.state.selected === 'privacy_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 5544f9c6d0..5f471ec2cb 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -74,6 +74,15 @@ export default class AdminSidebar extends React.Component { {'Image Settings'} +
  • + + {'Privacy Settings'} + +
  • 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 0000000000..8ce6939258 --- /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 =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    +

    {'Privacy Settings'}

    +
    + +
    + +
    + + +

    {'Hides email address of users from other users including team administrator.'}

    +
    +
    + +
    + +
    + + +

    {'Hides full name of users from other users including team administrator.'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +PrivacySettings.propTypes = { + config: React.PropTypes.object +}; From e78c79b83213efc40bffc5ef42071fedb85d6061 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 19:01:52 -0700 Subject: [PATCH 07/15] Adding rate limiter settings to the admin console --- api/server.go | 2 +- config/config.json | 46 ++- model/config.go | 22 +- utils/config.go | 5 +- .../admin_console/admin_controller.jsx | 3 + .../admin_console/admin_sidebar.jsx | 9 + .../admin_console/rate_settings.jsx | 272 ++++++++++++++++++ 7 files changed, 317 insertions(+), 42 deletions(-) create mode 100644 web/react/components/admin_console/rate_settings.jsx diff --git a/api/server.go b/api/server.go index 3273e766c3..a2f5fe552b 100644 --- a/api/server.go +++ b/api/server.go @@ -42,7 +42,7 @@ func StartServer() { var handler http.Handler = Srv.Router - if utils.Cfg.RateLimitSettings.UseRateLimiter { + if utils.Cfg.RateLimitSettings.EnableRateLimiter { l4g.Info("RateLimiter is enabled") vary := throttled.VaryBy{} diff --git a/config/config.json b/config/config.json index f9c49678fb..e1907152b4 100644 --- a/config/config.json +++ b/config/config.json @@ -1,12 +1,4 @@ { - "LogSettings": { - "ConsoleEnable": true, - "ConsoleLevel": "DEBUG", - "FileEnable": true, - "FileLevel": "INFO", - "FileFormat": "", - "FileLocation": "" - }, "ServiceSettings": { "SiteName": "Mattermost", "Mode": "dev", @@ -19,7 +11,18 @@ "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t", "AnalyticsUrl": "", "AllowedLoginAttempts": 10, - "EnableOAuthServiceProvider": false + "EnableOAuthServiceProvider": false, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "" + }, + "TeamSettings": { + "MaxUsersPerTeam": 150, + "AllowPublicLink": true, + "AllowValetDefault": false, + "TourLink": "", + "DefaultThemeColor": "#2389D7", + "DisableTeamCreation": false, + "RestrictCreationToDomains": "" }, "SqlSettings": { "DriverName": "mysql", @@ -32,6 +35,14 @@ "Trace": false, "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" }, + "LogSettings": { + "ConsoleEnable": true, + "ConsoleLevel": "DEBUG", + "FileEnable": true, + "FileLevel": "INFO", + "FileFormat": "", + "FileLocation": "" + }, "ImageSettings": { "DriverName": "local", "Directory": "./data/", @@ -63,7 +74,7 @@ "ApplePushCertPrivate": "" }, "RateLimitSettings": { - "UseRateLimiter": true, + "EnableRateLimiter": true, "PerSec": 10, "MemoryStoreSize": 10000, "VaryByRemoteAddr": true, @@ -71,23 +82,8 @@ }, "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, diff --git a/model/config.go b/model/config.go index 3c0ada423f..d4eb1e714c 100644 --- a/model/config.go +++ b/model/config.go @@ -30,6 +30,8 @@ type ServiceSettings struct { AnalyticsUrl string AllowedLoginAttempts int EnableOAuthServiceProvider bool + SegmentDeveloperKey string + GoogleDeveloperKey string } type SSOSetting struct { @@ -96,11 +98,11 @@ type EmailSettings struct { } 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 { @@ -108,11 +110,6 @@ type PrivacySettings struct { ShowFullName bool } -type ClientSettings struct { - SegmentDeveloperKey string - GoogleDeveloperKey string -} - type TeamSettings struct { MaxUsersPerTeam int AllowPublicLink bool @@ -124,15 +121,14 @@ type TeamSettings struct { } type Config struct { - LogSettings LogSettings ServiceSettings ServiceSettings + TeamSettings TeamSettings SqlSettings SqlSettings + LogSettings LogSettings ImageSettings ImageSettings EmailSettings EmailSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings - ClientSettings ClientSettings - TeamSettings TeamSettings SSOSettings map[string]SSOSetting } diff --git a/utils/config.go b/utils/config.go index cef99d7d97..5b7cc7c64e 100644 --- a/utils/config.go +++ b/utils/config.go @@ -176,6 +176,8 @@ func getClientProperties(c *model.Config) map[string]string { props["SiteName"] = c.ServiceSettings.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) @@ -186,9 +188,6 @@ func getClientProperties(c *model.Config) map[string]string { 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["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight) props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 9db1c945e1..0cd3500e55 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -11,6 +11,7 @@ var LogSettingsTab = require('./log_settings.jsx'); var LogsTab = require('./logs.jsx'); var ImageSettingsTab = require('./image_settings.jsx'); var PrivacySettingsTab = require('./privacy_settings.jsx'); +var RateSettingsTab = require('./rate_settings.jsx'); export default class AdminController extends React.Component { constructor(props) { @@ -59,6 +60,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'privacy_settings') { tab = ; + } else if (this.state.selected === 'rate_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 5f471ec2cb..f21511cb99 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -83,6 +83,15 @@ export default class AdminSidebar extends React.Component { {'Privacy Settings'} +
  • + + {'Rate Limit Settings'} + +
  • 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 0000000000..6c0f070da7 --- /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 =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +
    +
    +

    {'Note:'}

    +

    {'Changing properties in this section will require a server restart before taking effect.'}

    +
    +
    + +

    {'Rate Limit Settings'}

    +
    + +
    + +
    + + +

    {'When enabled throttles rate at which APIs respond.'}

    +
    +
    + +
    + +
    + +

    {'Height of profile picture.'}

    +
    +
    + +
    + +
    + +

    {'Maximum number of users sessions connected to the system as determined by VaryByRemoteAddr and VaryByHeader variables.'}

    +
    +
    + +
    + +
    + + +

    {'Rate limit API access by IP address.'}

    +
    +
    + +
    + +
    + +

    {'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").'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +RateSettings.propTypes = { + config: React.PropTypes.object +}; From afcff9b301ed45687ab7021709af8b2cf338355b Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 21 Sep 2015 20:38:31 -0700 Subject: [PATCH 08/15] Fixing unit tests --- api/file_test.go | 54 +++++++++++++++++++++++----------------------- api/team.go | 3 ++- api/user.go | 22 ++++++++++--------- api/user_test.go | 26 +++++++++++----------- config/config.json | 18 +++++++--------- model/config.go | 14 ++++++++++-- utils/config.go | 24 +-------------------- 7 files changed, 75 insertions(+), 86 deletions(-) diff --git a/api/file_test.go b/api/file_test.go index a62bdc83ea..a0a2f32551 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.ImageSettings.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.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettingsAmazonS3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -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.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettingsAmazonS3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { // perform clean-up on s3 var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettingsAmazonS3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.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.ImageSettings.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.ImageSettings.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.ImageSettings.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/team.go b/api/team.go index f0025fdbdc..4531c83b93 100644 --- a/api/team.go +++ b/api/team.go @@ -85,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.Allow { c.SetInvalidParam("createTeamFromSSO", "service") return } diff --git a/api/user.go b/api/user.go index 32dfa7dfba..7f4eb6c2da 100644 --- a/api/user.go +++ b/api/user.go @@ -1362,15 +1362,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.Allow { 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))) @@ -1389,7 +1390,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.Allow { return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) } @@ -1402,7 +1404,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", "") } @@ -1414,14 +1416,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") @@ -1443,7 +1445,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 a9529e9370..8342f37f6c 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -352,19 +352,19 @@ func TestUserCreateImage(t *testing.T) { Client.DoApiGet("/users/"+user.Id+"/image", "", "") - if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { + if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettingsAmazonS3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.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.ImageSettings.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 +382,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.ImageSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -450,19 +450,19 @@ func TestUserUploadProfileImage(t *testing.T) { Client.DoApiGet("/users/"+user.Id+"/image", "", "") - if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { + if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettingsAmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettingsAmazonS3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettingsAmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.ImageSettings.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.ImageSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } diff --git a/config/config.json b/config/config.json index e1907152b4..108271bca6 100644 --- a/config/config.json +++ b/config/config.json @@ -84,15 +84,13 @@ "ShowEmailAddress": true, "ShowFullName": true }, - "SSOSettings": { - "gitlab": { - "Allow": false, - "Secret": "", - "Id": "", - "Scope": "", - "AuthEndpoint": "", - "TokenEndpoint": "", - "UserApiEndpoint": "" - } + "GitLabSSOSettings": { + "Allow": false, + "Secret": "", + "Id": "", + "Scope": "", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" } } \ No newline at end of file diff --git a/model/config.go b/model/config.go index d4eb1e714c..31af619a5d 100644 --- a/model/config.go +++ b/model/config.go @@ -15,6 +15,8 @@ const ( IMAGE_DRIVER_LOCAL = "local" IMAGE_DRIVER_S3 = "amazons3" + + SERVICE_GITLAB = "gitlab" ) type ServiceSettings struct { @@ -34,7 +36,7 @@ type ServiceSettings struct { GoogleDeveloperKey string } -type SSOSetting struct { +type SSOSettings struct { Allow bool Secret string Id string @@ -129,7 +131,7 @@ type Config struct { EmailSettings EmailSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings - SSOSettings map[string]SSOSetting + GitLabSSOSettings SSOSettings } func (o *Config) ToJson() string { @@ -141,6 +143,14 @@ func (o *Config) ToJson() string { } } +func (o *Config) GetSSOService(service string) *SSOSettings { + if service == SERVICE_GITLAB { + return &o.GitLabSSOSettings + } + + return nil +} + func ConfigFromJson(data io.Reader) *Config { decoder := json.NewDecoder(data) var o Config diff --git a/utils/config.go b/utils/config.go index 5b7cc7c64e..66a20c39ba 100644 --- a/utils/config.go +++ b/utils/config.go @@ -183,7 +183,7 @@ func getClientProperties(c *model.Config) map[string]string { props["AllowSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.AllowSignUpWithEmail) props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail - props["AllowSignUpWithGitLab"] = strconv.FormatBool(false) + props["AllowSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSSOSettings.Allow) props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink) @@ -194,25 +194,3 @@ func getClientProperties(c *model.Config) map[string]string { return props } - -// func IsS3Configured() bool { -// if Cfg.AWSSettings.AmazonS3AccessKeyId == "" || Cfg.AWSSettingsAmazonS3SecretAccessKey == "" || Cfg.AWSSettingsAmazonS3Region == "" || Cfg.AWSSettingsAmazonS3Bucket == "" { -// return false -// } - -// return true -// } - -func IsServiceAllowed(s string) bool { - if len(s) == 0 { - return false - } - - if service, ok := Cfg.SSOSettings[s]; ok { - if service.Allow { - return true - } - } - - return false -} From f05a2c03d5dbf5b0b7d09148a37d2325012b309f Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 00:00:19 -0700 Subject: [PATCH 09/15] Adding SQL settings to admin console --- config/config.json | 8 +- model/config.go | 4 +- store/sql_store.go | 13 +- utils/config.go | 2 +- .../admin_console/admin_controller.jsx | 8 +- .../admin_console/admin_sidebar.jsx | 30 +- .../admin_console/gitlab_settings.jsx | 277 +++++++++++++++++ .../admin_console/rate_settings.jsx | 2 +- .../components/admin_console/sql_settings.jsx | 283 ++++++++++++++++++ 9 files changed, 608 insertions(+), 19 deletions(-) create mode 100644 web/react/components/admin_console/gitlab_settings.jsx create mode 100644 web/react/components/admin_console/sql_settings.jsx diff --git a/config/config.json b/config/config.json index 108271bca6..8baa09859a 100644 --- a/config/config.json +++ b/config/config.json @@ -27,13 +27,11 @@ "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": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV" }, "LogSettings": { "ConsoleEnable": true, @@ -84,7 +82,7 @@ "ShowEmailAddress": true, "ShowFullName": true }, - "GitLabSSOSettings": { + "GitLabSettings": { "Allow": false, "Secret": "", "Id": "", diff --git a/model/config.go b/model/config.go index 31af619a5d..3da068d8d3 100644 --- a/model/config.go +++ b/model/config.go @@ -131,7 +131,7 @@ type Config struct { EmailSettings EmailSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings - GitLabSSOSettings SSOSettings + GitLabSettings SSOSettings } func (o *Config) ToJson() string { @@ -145,7 +145,7 @@ func (o *Config) ToJson() string { func (o *Config) GetSSOService(service string) *SSOSettings { if service == SERVICE_GITLAB { - return &o.GitLabSSOSettings + return &o.GitLabSettings } return nil diff --git a/store/sql_store.go b/store/sql_store.go index adac47b4dc..7f3b555f1b 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -50,11 +50,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() diff --git a/utils/config.go b/utils/config.go index 66a20c39ba..4a5746830f 100644 --- a/utils/config.go +++ b/utils/config.go @@ -183,7 +183,7 @@ func getClientProperties(c *model.Config) map[string]string { props["AllowSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.AllowSignUpWithEmail) props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail - props["AllowSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSSOSettings.Allow) + props["AllowSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Allow) props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink) diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 0cd3500e55..491dbd7544 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -12,6 +12,8 @@ var LogsTab = require('./logs.jsx'); var ImageSettingsTab = 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'); export default class AdminController extends React.Component { constructor(props) { @@ -22,7 +24,7 @@ export default class AdminController extends React.Component { this.state = { config: null, - selected: 'email_settings' + selected: 'sql_settings' }; } @@ -62,6 +64,10 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'rate_settings') { tab = ; + } else if (this.state.selected === 'gitlab_settings') { + tab = ; + } else if (this.state.selected === 'sql_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index f21511cb99..deb064015e 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -38,6 +38,15 @@ export default class AdminSidebar extends React.Component {
    • 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 0000000000..f76655b899 --- /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 =
      ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
      + +

      {'GitLab Settings'}

      +
      + +
      + +
      + + +

      {'When true Mattermost will allow team creation and account signup utilizing GitLab OAuth.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      + +
      + +

      {'Need help text.'}

      +
      +
      + +
      +
      + {serverError} + +
      +
      + +
      +
      + ); + } +} + +GitLabSettings.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 index 6c0f070da7..c05bf4a820 100644 --- a/web/react/components/admin_console/rate_settings.jsx +++ b/web/react/components/admin_console/rate_settings.jsx @@ -162,7 +162,7 @@ export default class RateSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.EnableRateLimiter} /> -

      {'Height of profile picture.'}

      +

      {'Throttles API at this number of requests per second.'}

    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 0000000000..35810f7eef --- /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, 31); + 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, 31); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + 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 ( +
    + +
    +
    +

    {'Note:'}

    +

    {'Changing properties in this section will require a server restart before taking effect.'}

    +
    +
    + +

    {'SQL Settings'}

    +
    + +
    + +
    +

    {this.props.config.SqlSettings.DriverName}

    +
    +
    + +
    + +
    +

    {dataSource}

    +
    +
    + +
    + +
    +

    {dataSourceReplicas}

    +
    +
    + +
    + +
    + +

    {'Maximum number of idle connections held open to the database.'}

    +
    +
    + +
    + +
    + +

    {'Maximum number of open connections held open to the database.'}

    +
    +
    + +
    + +
    + +

    {'32-character salt available to encrypt and decrypt sensitive fields in database.'}

    +
    + +
    +
    +
    + +
    + +
    + + +

    {'Output executing SQL statements to the log. Typically used for development.'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +SqlSettings.propTypes = { + config: React.PropTypes.object +}; From 08a3acbb44b043b9bb56f9b96e91432352d06d1a Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 01:15:41 -0700 Subject: [PATCH 10/15] Adding team settings to admin console --- api/admin_test.go | 4 +- api/api_test.go | 1 + api/context.go | 2 +- api/file.go | 2 +- api/post.go | 70 ----- api/post_test.go | 92 ------- api/team.go | 25 +- api/team_test.go | 76 ------ api/user.go | 16 +- api/user_test.go | 1 + config/config.json | 11 +- model/config.go | 9 +- model/team.go | 1 - store/sql_store.go | 29 +- store/sql_team_store.go | 1 + store/sql_user_store_test.go | 2 +- utils/config.go | 5 +- .../admin_console/admin_controller.jsx | 6 +- .../admin_console/admin_sidebar.jsx | 9 + .../admin_console/image_settings.jsx | 34 +++ .../admin_console/team_settings.jsx | 257 ++++++++++++++++++ web/react/components/view_image.jsx | 2 +- web/web.go | 2 +- 23 files changed, 344 insertions(+), 313 deletions(-) create mode 100644 web/react/components/admin_console/team_settings.jsx diff --git a/api/admin_test.go b/api/admin_test.go index c74fbf6e53..ad7ac08f8f 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,7 +117,7 @@ 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() } } diff --git a/api/api_test.go b/api/api_test.go index 642db581e7..490f8ab5b5 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -14,6 +14,7 @@ var Client *model.Client func Setup() { if Srv == nil { utils.LoadConfig("config.json") + utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 NewServer() StartServer() InitApi() diff --git a/api/context.go b/api/context.go index 02716bb331..c4684221d7 100644 --- a/api/context.go +++ b/api/context.go @@ -471,7 +471,7 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) 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/file.go b/api/file.go index 69303f5f8b..61d0df413f 100644 --- a/api/file.go +++ b/api/file.go @@ -447,7 +447,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !utils.Cfg.TeamSettings.AllowPublicLink { + if !utils.Cfg.ImageSettings.EnablePublicLink { c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "") c.Err.StatusCode = http.StatusForbidden } diff --git a/api/post.go b/api/post.go index 21bc35b976..4294ae03ca 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 4cccfd62ae..358611240b 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/team.go b/api/team.go index 4531c83b93..8802208f73 100644 --- a/api/team.go +++ b/api/team.go @@ -60,7 +60,6 @@ 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 @@ -124,8 +123,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 @@ -207,8 +204,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 @@ -228,13 +223,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 @@ -286,13 +274,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 } } @@ -301,7 +282,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 } @@ -567,8 +548,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") @@ -597,8 +576,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 4f1b9e5f03..48c73c638f 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -330,79 +330,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 7f4eb6c2da..ba5323d776 100644 --- a/api/user.go +++ b/api/user.go @@ -155,19 +155,13 @@ 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 diff --git a/api/user_test.go b/api/user_test.go index 8342f37f6c..7451cb615a 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -952,6 +952,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/config/config.json b/config/config.json index 8baa09859a..12926bee57 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,5 @@ { "ServiceSettings": { - "SiteName": "Mattermost", "Mode": "dev", "AllowTesting": false, "UseSSL": false, @@ -16,12 +15,11 @@ "GoogleDeveloperKey": "" }, "TeamSettings": { - "MaxUsersPerTeam": 150, - "AllowPublicLink": true, - "AllowValetDefault": false, - "TourLink": "", + "SiteName": "Mattermost", + "MaxUsersPerTeam": 50, "DefaultThemeColor": "#2389D7", - "DisableTeamCreation": false, + "EnableTeamCreation": true, + "EnableUserCreation": true, "RestrictCreationToDomains": "" }, "SqlSettings": { @@ -44,6 +42,7 @@ "ImageSettings": { "DriverName": "local", "Directory": "./data/", + "EnablePublicLink": true, "ThumbnailWidth": 120, "ThumbnailHeight": 100, "PreviewWidth": 1024, diff --git a/model/config.go b/model/config.go index 3da068d8d3..876c36e985 100644 --- a/model/config.go +++ b/model/config.go @@ -20,7 +20,6 @@ const ( ) type ServiceSettings struct { - SiteName string Mode string AllowTesting bool UseSSL bool @@ -68,6 +67,7 @@ type LogSettings struct { type ImageSettings struct { DriverName string Directory string + EnablePublicLink bool ThumbnailWidth uint ThumbnailHeight uint PreviewWidth uint @@ -113,12 +113,11 @@ type PrivacySettings struct { } type TeamSettings struct { + SiteName string MaxUsersPerTeam int - AllowPublicLink bool - AllowValetDefault bool - TourLink string DefaultThemeColor string - DisableTeamCreation bool + EnableTeamCreation bool + EnableUserCreation bool RestrictCreationToDomains string } diff --git a/model/team.go b/model/team.go index 8b4f82830c..0d740dde27 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 { diff --git a/store/sql_store.go b/store/sql_store.go index 7f3b555f1b..7f3c59164e 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -311,26 +311,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 d2148c2e3c..3d644e5776 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() { diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index ddd7e5bb87..466da28452 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() diff --git a/utils/config.go b/utils/config.go index 4a5746830f..45f62dc197 100644 --- a/utils/config.go +++ b/utils/config.go @@ -173,7 +173,7 @@ func getClientProperties(c *model.Config) map[string]string { props["BuildDate"] = model.BuildDate props["BuildHash"] = model.BuildHash - props["SiteName"] = c.ServiceSettings.SiteName + props["SiteName"] = c.TeamSettings.SiteName props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey @@ -186,11 +186,10 @@ func getClientProperties(c *model.Config) map[string]string { props["AllowSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Allow) props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) - props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink) + props["EnablePublicLink"] = strconv.FormatBool(c.ImageSettings.EnablePublicLink) props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight) props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) - props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) return props } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 491dbd7544..72b5d5c9da 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -14,6 +14,8 @@ 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'); + export default class AdminController extends React.Component { constructor(props) { @@ -24,7 +26,7 @@ export default class AdminController extends React.Component { this.state = { config: null, - selected: 'sql_settings' + selected: 'team_settings' }; } @@ -68,6 +70,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'sql_settings') { tab = ; + } else if (this.state.selected === 'team_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index deb064015e..2b7159e1da 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -38,6 +38,15 @@ export default class AdminSidebar extends React.Component {
    +
    + +
    + + +

    {'Allow users to share public links to files and images.'}

    +
    +
    +
    {serverError} 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 0000000000..fefc0e936d --- /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 =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    {'Team Settings'}

    +
    + +
    + +
    + +

    {'Name of service shown in login screens and UI.'}

    +
    +
    + +
    + +
    + +

    {'Maximum number of users per team.'}

    +
    +
    + +
    + +
    + +

    {'Default theme color for team sites.'}

    +
    +
    + +
    + +
    + + +

    {'When false the ability to create teams is disabled. The create team button displays error when pressed.'}

    +
    +
    + +
    + +
    + + +

    {'When false the ability to create accounts is disabled. The create account button displays error when pressed.'}

    +
    +
    + +
    + +
    + +

    {'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").'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +TeamSettings.propTypes = { + config: React.PropTypes.object +}; diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index a37eb67759..dafcdd9f99 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 === 'true') { + if (global.window.config.EnablePublicLink === 'true') { publicLink = (
    0 { - title = utils.Cfg.ServiceSettings.SiteName + " - " + title + title = utils.Cfg.TeamSettings.SiteName + " - " + title } props := make(map[string]string) From 88e5a71e8c93b495cedaa07931a4f8052d9f12ed Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 12:12:50 -0700 Subject: [PATCH 11/15] Adding service settings to admin console --- api/admin.go | 4 +- api/api_test.go | 2 +- api/channel_test.go | 2 +- api/command.go | 4 +- api/context.go | 38 +-- api/file.go | 4 +- api/file_benchmark_test.go | 2 +- api/file_test.go | 2 +- api/server.go | 4 +- api/team.go | 21 +- api/team_test.go | 2 +- api/user.go | 14 +- api/user_test.go | 4 +- api/web_socket_test.go | 2 +- config/config.json | 28 +- docker/dev/config_docker.json | 125 ++++----- docker/local/config_docker.json | 125 ++++----- manualtesting/manual_testing.go | 2 +- mattermost.go | 2 +- model/client.go | 1 + model/config.go | 26 +- utils/config.go | 13 +- .../admin_console/admin_controller.jsx | 6 +- .../admin_console/admin_sidebar.jsx | 9 + .../admin_console/email_settings.jsx | 98 ++++++- .../admin_console/image_settings.jsx | 45 +++ .../components/admin_console/log_settings.jsx | 24 +- .../admin_console/service_settings.jsx | 262 ++++++++++++++++++ .../components/admin_console/sql_settings.jsx | 2 +- web/react/components/login.jsx | 4 +- web/react/components/signup_team.jsx | 8 +- web/react/components/signup_user_complete.jsx | 4 +- .../components/team_signup_choose_auth.jsx | 4 +- web/react/utils/client.jsx | 2 - web/templates/head.html | 23 -- web/web.go | 8 +- web/web_test.go | 2 +- 37 files changed, 631 insertions(+), 297 deletions(-) create mode 100644 web/react/components/admin_console/service_settings.jsx diff --git a/api/admin.go b/api/admin.go index ca66b7cb40..568d8f6e87 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 490f8ab5b5..761f3e33f6 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 14bfe1cf79..7845ac499a 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 bc55f206bb..0d2f7597b5 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 c4684221d7..9a276a1a18 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 61d0df413f..c85d241f3f 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 251ff7793c..f7d5de1d90 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 a0a2f32551..072a3fab15 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 a2f5fe552b..3f23d8df63 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 8802208f73..c9d2412d37 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 48c73c638f..cd39dacfe5 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 ba5323d776..d61afb0277 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 7451cb615a..34eefce59f 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 161274ff79..d086308bf5 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 12926bee57..4b5a16300a 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 aceeb95b44..8bd5f1b0a5 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -1,81 +1,73 @@ { + "ServiceSettings": { + "ListenAddress": ":80", + "MaximumLoginAttempts": 10, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": 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": [], + "MaxIdleConns": 10, + "MaxOpenConns": 10, + "Trace": false, + "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV" + }, "LogSettings": { - "ConsoleEnable": true, + "EnableConsole": false, "ConsoleLevel": "INFO", - "FileEnable": true, + "EnableFile": 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 - }, - "SSOSettings": { - "gitlab": { - "Allow": false, - "Secret" : "", - "Id": "", - "Scope": "", - "AuthEndpoint": "", - "TokenEndpoint": "", - "UserApiEndpoint": "" - } - }, - "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"], - "MaxIdleConns": 10, - "MaxOpenConns": 10, - "Trace": false, - "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" - }, - "AWSSettings": { - "S3AccessKeyId": "", - "S3SecretAccessKey": "", - "S3Bucket": "", - "S3Region": "" - }, "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 bc42951b8a..8bd5f1b0a5 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -1,81 +1,73 @@ { + "ServiceSettings": { + "ListenAddress": ":80", + "MaximumLoginAttempts": 10, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": 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": [], + "MaxIdleConns": 10, + "MaxOpenConns": 10, + "Trace": false, + "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV" + }, "LogSettings": { - "ConsoleEnable": true, + "EnableConsole": false, "ConsoleLevel": "INFO", - "FileEnable": true, + "EnableFile": 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 - }, - "SSOSettings": { - "gitlab": { - "Allow": false, - "Secret" : "", - "Id": "", - "Scope": "", - "AuthEndpoint": "", - "TokenEndpoint": "", - "UserApiEndpoint": "" - } - }, - "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"], - "MaxIdleConns": 10, - "MaxOpenConns": 10, - "Trace": false, - "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" - }, - "AWSSettings": { - "S3AccessKeyId": "", - "S3SecretAccessKey": "", - "S3Bucket": "", - "S3Region": "" - }, "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 86b173c6a2..a517b0c0e1 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 f54bcf15fb..4608bfff3e 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 823e859cf1..d13ffa6cb4 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 876c36e985..7791c40212 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 45f62dc197..c2466800e8 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 72b5d5c9da..ce7d61ca93 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 = ; } else if (this.state.selected === 'team_settings') { tab = ; + } else if (this.state.selected === 'service_settings') { + tab = ; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 2b7159e1da..0983c12765 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -38,6 +38,15 @@ export default class AdminSidebar extends React.Component {
    +
    + +
    + +

    {'32-character salt added to signing of email invites.'}

    +
    + +
    +
    +
    + +
    + +
    + +

    {'32-character salt added to signing of password reset emails.'}

    +
    + +
    +
    +
    +
    {serverError} diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index c0cbb5aa6f..80da0a47fa 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); @@ -424,6 +440,35 @@ export default class ImageSettings extends React.Component {
    +
    + +
    + +

    {'32-character salt added to signing of public image links.'}

    +
    + +
    +
    +
    +
    {serverError} diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx index 2707ce6b68..d668014311 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 0000000000..fcb8f800d9 --- /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 =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    {'Service Settings'}

    +
    + +
    + +
    + +

    {'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.'}

    +
    +
    + +
    + +
    + +

    {'Login attempts allowed before user is locked out and required to reset password via email.'}

    +
    +
    + +
    + +
    + +

    {'For users running a SaaS services, sign up for a key at Segment.com to track metrics.'}

    +
    +
    + +
    + +
    + +
    + + +

    {'When enabled Mattermost will act as an Oauth2 Provider. Changing this will require a server restart before taking effect.'}

    +
    +
    + +
    + +
    + + +

    {'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.'}

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +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 35810f7eef..cac0177705 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 b12c5d9888..8cc4f1483d 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(
    diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index d08608c9bf..7f320e0b2c 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 237169f172..4dad1ef4fd 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(
    diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index 4aeae8f084..b8264b887c 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( - - - {{end}} diff --git a/web/web.go b/web/web.go index 86769dd54e..9847d0b5e2 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 3da7eb2dc7..c1d5dd6f9e 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) } } From e22e7b8b7b66f342c2df693bbfc06a85980d253e Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 12:31:01 -0700 Subject: [PATCH 12/15] Fixing bug in generating key length --- config/config.json | 8 ++++---- web/react/components/admin_console/email_settings.jsx | 8 ++++---- web/react/components/admin_console/image_settings.jsx | 4 ++-- web/react/components/admin_console/sql_settings.jsx | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config/config.json b/config/config.json index 4b5a16300a..32ec2d28b0 100644 --- a/config/config.json +++ b/config/config.json @@ -22,7 +22,7 @@ "MaxIdleConns": 10, "MaxOpenConns": 10, "Trace": false, - "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV" + "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg" }, "LogSettings": { "EnableConsole": true, @@ -36,7 +36,7 @@ "DriverName": "local", "Directory": "./data/", "EnablePublicLink": true, - "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX", + "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AXe", "ThumbnailWidth": 120, "ThumbnailHeight": 100, "PreviewWidth": 1024, @@ -60,8 +60,8 @@ "SMTPServer": "", "SMTPPort": "", "ConnectionSecurity": "", - "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo", - "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e", + "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS", + "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL", "ApplePushServer": "", "ApplePushCertPublic": "", "ApplePushCertPrivate": "" diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index d94859cdda..8549889474 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -55,13 +55,13 @@ export default class EmailSettings extends React.Component { 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); + 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, 31); + config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt; } @@ -70,14 +70,14 @@ export default class EmailSettings extends React.Component { handleGenerateInvite(e) { e.preventDefault(); - React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 31); + 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, 31); + 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); } diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index 80da0a47fa..f84f1c7358 100644 --- a/web/react/components/admin_console/image_settings.jsx +++ b/web/react/components/admin_console/image_settings.jsx @@ -32,7 +32,7 @@ export default class ImageSettings extends React.Component { handleGenerate(e) { e.preventDefault(); - React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 31); + 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); } @@ -53,7 +53,7 @@ export default class ImageSettings extends React.Component { 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); + config.ImageSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); React.findDOMNode(this.refs.PublicLinkSalt).value = config.ImageSettings.PublicLinkSalt; } diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx index cac0177705..ad01b59631 100644 --- a/web/react/components/admin_console/sql_settings.jsx +++ b/web/react/components/admin_console/sql_settings.jsx @@ -33,7 +33,7 @@ export default class SqlSettings extends React.Component { config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim(); if (config.SqlSettings.AtRestEncryptKey === '') { - config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 31); + config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32); React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey; } @@ -73,7 +73,7 @@ export default class SqlSettings extends React.Component { handleGenerate(e) { e.preventDefault(); - React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 31); + 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); } From 554f0173270a3d66dd28f0f4cccb09d870d3e5f0 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 13:20:24 -0700 Subject: [PATCH 13/15] Fixing merge conflict --- config/config.json | 4 ++-- docker/dev/config_docker.json | 4 ++-- docker/local/config_docker.json | 4 ++-- model/config.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/config.json b/config/config.json index e055fd845a..c0f33a346f 100644 --- a/config/config.json +++ b/config/config.json @@ -5,8 +5,8 @@ "SegmentDeveloperKey": "", "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, - "EnableTesting": false, - "EnableIncomingWebhooks": false + "EnableIncomingWebhooks": false, + "EnableTesting": false }, "TeamSettings": { "SiteName": "Mattermost", diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 4a1be4ed3b..e33396214d 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -5,8 +5,8 @@ "SegmentDeveloperKey": "", "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, - "EnableTesting": false, - "EnableIncomingWebhooks": false + "EnableIncomingWebhooks": false, + "EnableTesting": false }, "TeamSettings": { "SiteName": "Mattermost", diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 4a1be4ed3b..e33396214d 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -5,8 +5,8 @@ "SegmentDeveloperKey": "", "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, - "EnableTesting": false, - "EnableIncomingWebhooks": false + "EnableIncomingWebhooks": false, + "EnableTesting": false }, "TeamSettings": { "SiteName": "Mattermost", diff --git a/model/config.go b/model/config.go index 76d449904c..1e95277a2a 100644 --- a/model/config.go +++ b/model/config.go @@ -25,8 +25,8 @@ type ServiceSettings struct { SegmentDeveloperKey string GoogleDeveloperKey string EnableOAuthServiceProvider bool - EnableTesting bool EnableIncomingWebhooks bool + EnableTesting bool } type SSOSettings struct { From 9e04909c0a3672d27c148c931d82b225cc86dfe5 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 22 Sep 2015 13:28:03 -0700 Subject: [PATCH 14/15] Adding new EnableIncomingWebhooks property to service settings in the admin console. --- .../admin_console/service_settings.jsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index fcb8f800d9..1bb1f053be 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -36,6 +36,7 @@ export default class ServiceSettings extends React.Component { 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; @@ -202,6 +203,39 @@ export default class ServiceSettings extends React.Component {
    +
    + +
    + + +

    {'When true incomming web hooks will be allowed.'}

    +
    +
    +
  • diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index f84f1c7358..25d5ad857d 100644 --- a/web/react/components/admin_console/image_settings.jsx +++ b/web/react/components/admin_console/image_settings.jsx @@ -5,7 +5,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 { +export default class FileSettings extends React.Component { constructor(props) { super(props); @@ -16,7 +16,7 @@ export default class ImageSettings extends React.Component { this.state = { saveNeeded: false, serverError: null, - DriverName: this.props.config.ImageSettings.DriverName + DriverName: this.props.config.FileSettings.DriverName }; } @@ -42,61 +42,61 @@ export default class ImageSettings extends React.Component { $('#save-button').button('loading'); var config = this.props.config; - config.ImageSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; - config.ImageSettings.Directory = React.findDOMNode(this.refs.Directory).value; - config.ImageSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; - config.ImageSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; - config.ImageSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; - config.ImageSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; - config.ImageSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; + 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.ImageSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); + config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); - if (config.ImageSettings.PublicLinkSalt === '') { - config.ImageSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); - React.findDOMNode(this.refs.PublicLinkSalt).value = config.ImageSettings.PublicLinkSalt; + 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.ImageSettings.ThumbnailWidth = thumbnailWidth; + 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.ImageSettings.ThumbnailHeight = thumbnailHeight; + 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.ImageSettings.PreviewWidth = previewWidth; + 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.ImageSettings.PreviewHeight = previewHeight; + 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.ImageSettings.ProfileWidth = profileWidth; + 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.ImageSettings.ProfileHeight = profileHeight; + config.FileSettings.ProfileHeight = profileHeight; React.findDOMNode(this.refs.ProfileHeight).value = profileHeight; Client.saveConfig( @@ -143,7 +143,7 @@ export default class ImageSettings extends React.Component { return (
    -

    {'Image Settings'}

    +

    {'File Settings'}

    @@ -185,7 +185,7 @@ export default class ImageSettings extends React.Component { id='Directory' ref='Directory' placeholder='Ex "./data/"' - defaultValue={this.props.config.ImageSettings.Directory} + defaultValue={this.props.config.FileSettings.Directory} onChange={this.handleChange} disabled={!enableFile} /> @@ -207,7 +207,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3AccessKeyId' ref='AmazonS3AccessKeyId' placeholder='Ex "AKIADTOVBGERKLCBV"' - defaultValue={this.props.config.ImageSettings.AmazonS3AccessKeyId} + defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId} onChange={this.handleChange} disabled={!enableS3} /> @@ -229,7 +229,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3SecretAccessKey' ref='AmazonS3SecretAccessKey' placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' - defaultValue={this.props.config.ImageSettings.AmazonS3SecretAccessKey} + defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey} onChange={this.handleChange} disabled={!enableS3} /> @@ -251,7 +251,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Bucket' ref='AmazonS3Bucket' placeholder='Ex "mattermost-media"' - defaultValue={this.props.config.ImageSettings.AmazonS3Bucket} + defaultValue={this.props.config.FileSettings.AmazonS3Bucket} onChange={this.handleChange} disabled={!enableS3} /> @@ -273,7 +273,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Region' ref='AmazonS3Region' placeholder='Ex "us-east-1"' - defaultValue={this.props.config.ImageSettings.AmazonS3Region} + defaultValue={this.props.config.FileSettings.AmazonS3Region} onChange={this.handleChange} disabled={!enableS3} /> @@ -295,7 +295,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailWidth' ref='ThumbnailWidth' placeholder='Ex "120"' - defaultValue={this.props.config.ImageSettings.ThumbnailWidth} + defaultValue={this.props.config.FileSettings.ThumbnailWidth} onChange={this.handleChange} />

    {'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.'}

    @@ -316,7 +316,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailHeight' ref='ThumbnailHeight' placeholder='Ex "100"' - defaultValue={this.props.config.ImageSettings.ThumbnailHeight} + defaultValue={this.props.config.FileSettings.ThumbnailHeight} onChange={this.handleChange} />

    {'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.'}

    @@ -337,7 +337,7 @@ export default class ImageSettings extends React.Component { id='PreviewWidth' ref='PreviewWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.PreviewWidth} + defaultValue={this.props.config.FileSettings.PreviewWidth} onChange={this.handleChange} />

    {'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}

    @@ -358,7 +358,7 @@ export default class ImageSettings extends React.Component { id='PreviewHeight' ref='PreviewHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.PreviewHeight} + defaultValue={this.props.config.FileSettings.PreviewHeight} onChange={this.handleChange} />

    {'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.'}

    @@ -379,7 +379,7 @@ export default class ImageSettings extends React.Component { id='ProfileWidth' ref='ProfileWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.ProfileWidth} + defaultValue={this.props.config.FileSettings.ProfileWidth} onChange={this.handleChange} />

    {'Width of profile picture.'}

    @@ -400,7 +400,7 @@ export default class ImageSettings extends React.Component { id='ProfileHeight' ref='ProfileHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.ProfileHeight} + defaultValue={this.props.config.FileSettings.ProfileHeight} onChange={this.handleChange} />

    {'Height of profile picture.'}

    @@ -421,7 +421,7 @@ export default class ImageSettings extends React.Component { name='EnablePublicLink' value='true' ref='EnablePublicLink' - defaultChecked={this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'true'} @@ -431,7 +431,7 @@ export default class ImageSettings extends React.Component { type='radio' name='EnablePublicLink' value='false' - defaultChecked={!this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={!this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'false'} @@ -454,7 +454,7 @@ export default class ImageSettings extends React.Component { id='PublicLinkSalt' ref='PublicLinkSalt' placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' - defaultValue={this.props.config.ImageSettings.PublicLinkSalt} + defaultValue={this.props.config.FileSettings.PublicLinkSalt} onChange={this.handleChange} />

    {'32-character salt added to signing of public image links.'}

    @@ -491,6 +491,6 @@ export default class ImageSettings extends React.Component { } } -ImageSettings.propTypes = { +FileSettings.propTypes = { config: React.PropTypes.object }; diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 7f320e0b2c..4112138fa3 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -4,7 +4,7 @@ const ChoosePage = require('./team_signup_choose_auth.jsx'); const EmailSignUpPage = require('./team_signup_with_email.jsx'); const SSOSignupPage = require('./team_signup_with_sso.jsx'); -var Constants = require('../utils/constants.jsx'); +const Constants = require('../utils/constants.jsx'); export default class TeamSignUp extends React.Component { constructor(props) {