Merge pull request #752 from mattermost/PLT-11-email

PLT-11 Cleaned up config file and added ability to makes changes from the admin console.
Этот коммит содержится в:
Corey Hulen
2015-09-23 14:04:08 -07:00
родитель 0170cfe604 ccf2e6e4e7
Коммит d792745f54
61 изменённых файлов: 3218 добавлений и 1149 удалений

Просмотреть файл

@@ -24,6 +24,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET") sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST") sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET") 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) { func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -34,7 +35,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
var lines []string var lines []string
if utils.Cfg.LogSettings.FileEnable { if utils.Cfg.LogSettings.EnableFile {
file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation)) file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
if err != nil { if err != nil {
@@ -81,7 +82,7 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
if len(cfg.ServiceSettings.Port) == 0 { if len(cfg.ServiceSettings.ListenAddress) == 0 {
c.SetInvalidParam("saveConfig", "config") c.SetInvalidParam("saveConfig", "config")
return return
} }
@@ -98,3 +99,29 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
json := utils.Cfg.ToJson() json := utils.Cfg.ToJson()
w.Write([]byte(json)) w.Write([]byte(json))
} }
func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("testEmail") {
return
}
cfg := model.ConfigFromJson(r.Body)
if cfg == nil {
c.SetInvalidParam("testEmail", "config")
return
}
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
if err := utils.SendMailUsingConfig(result.Data.(*model.User).Email, "Mattermost - Testing Email Settings", "<br/><br/><br/>It appears your Mattermost email is setup correctly!", cfg); err != nil {
c.Err = err
return
}
}
m := make(map[string]string)
m["SUCCESS"] = "true"
w.Write([]byte(model.MapToJson(m)))
}

Просмотреть файл

@@ -83,7 +83,7 @@ func TestGetConfig(t *testing.T) {
} else { } else {
cfg := result.Data.(*model.Config) cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 { if len(cfg.TeamSettings.SiteName) == 0 {
t.Fatal() t.Fatal()
} }
} }
@@ -117,8 +117,36 @@ func TestSaveConfig(t *testing.T) {
} else { } else {
cfg := result.Data.(*model.Config) cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 { if len(cfg.TeamSettings.SiteName) == 0 {
t.Fatal() t.Fatal()
} }
} }
} }
func TestEmailTest(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.TestEmail(utils.Cfg); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.TestEmail(utils.Cfg); err != nil {
t.Fatal(err)
}
}

Просмотреть файл

@@ -14,10 +14,11 @@ var Client *model.Client
func Setup() { func Setup() {
if Srv == nil { if Srv == nil {
utils.LoadConfig("config.json") utils.LoadConfig("config.json")
utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
NewServer() NewServer()
StartServer() StartServer()
InitApi() InitApi()
Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) Client = model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
} }
} }

Просмотреть файл

@@ -627,7 +627,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag = cache_result.Etag 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 := &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) user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User)

Просмотреть файл

@@ -215,8 +215,8 @@ func joinCommand(c *Context, command *model.Command) bool {
func loadTestCommand(c *Context, command *model.Command) bool { func loadTestCommand(c *Context, command *model.Command) bool {
cmd := "/loadtest" cmd := "/loadtest"
// This command is only available when AllowTesting is true // This command is only available when EnableTesting is true
if !utils.Cfg.ServiceSettings.AllowTesting { if !utils.Cfg.ServiceSettings.EnableTesting {
return false return false
} }

Просмотреть файл

@@ -107,21 +107,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
isTokenFromQueryString = true isTokenFromQueryString = true
} }
protocol := "http" protocol := GetProtocol(r)
// 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"
}
}
c.setSiteURL(protocol + "://" + r.Host) c.setSiteURL(protocol + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) 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) { 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} 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 { 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 { func GetIpAddress(r *http.Request) string {
address := r.Header.Get(model.HEADER_FORWARDED) address := r.Header.Get(model.HEADER_FORWARDED)
if len(address) == 0 {
address = r.Header.Get(model.HEADER_REAL_IP)
}
if len(address) == 0 { if len(address) == 0 {
address, _, _ = net.SplitHostPort(r.RemoteAddr) address, _, _ = net.SplitHostPort(r.RemoteAddr)
} }
@@ -458,20 +457,13 @@ func IsPrivateIpAddress(ipAddress string) bool {
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
protocol := "http" protocol := GetProtocol(r)
if utils.Cfg.ServiceSettings.UseSSL {
forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO)
if forwardProto != "http" {
protocol = "https"
}
}
SiteURL := protocol + "://" + r.Host SiteURL := protocol + "://" + r.Host
m := make(map[string]string) m := make(map[string]string)
m["Message"] = err.Message m["Message"] = err.Message
m["Details"] = err.DetailedError m["Details"] = err.DetailedError
m["SiteName"] = utils.Cfg.ServiceSettings.SiteName m["SiteName"] = utils.Cfg.TeamSettings.SiteName
m["SiteURL"] = SiteURL m["SiteURL"] = SiteURL
w.WriteHeader(err.StatusCode) w.WriteHeader(err.StatusCode)

Просмотреть файл

@@ -278,11 +278,11 @@ func copyDirToExportWriter(writer ExportWriter, inPath string, outPath string) *
} }
func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError { func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
teamDir := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + teamId teamDir := utils.Cfg.FileSettings.Directory + "teams/" + teamId
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "") return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "")
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil { if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil {
return err return err
} }

Просмотреть файл

@@ -69,8 +69,8 @@ func InitFile(r *mux.Router) {
} }
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("uploadFile", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "") c.Err = model.NewAppError("uploadFile", "Unable to upload file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -217,8 +217,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create thumbnail // Create thumbnail
go func() { go func() {
thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth) thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
thumbHeight := float64(utils.Cfg.ImageSettings.ThumbnailHeight) thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
imgWidth := float64(width) imgWidth := float64(width)
imgHeight := float64(height) imgHeight := float64(height)
@@ -226,9 +226,9 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
if imgHeight < thumbHeight && imgWidth < thumbWidth { if imgHeight < thumbHeight && imgWidth < thumbWidth {
thumbnail = img thumbnail = img
} else if imgHeight/imgWidth < thumbHeight/thumbWidth { } else if imgHeight/imgWidth < thumbHeight/thumbWidth {
thumbnail = resize.Resize(0, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3) thumbnail = resize.Resize(0, utils.Cfg.FileSettings.ThumbnailHeight, img, resize.Lanczos3)
} else { } else {
thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, 0, img, resize.Lanczos3) thumbnail = resize.Resize(utils.Cfg.FileSettings.ThumbnailWidth, 0, img, resize.Lanczos3)
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
@@ -247,8 +247,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create preview // Create preview
go func() { go func() {
var preview image.Image var preview image.Image
if width > int(utils.Cfg.ImageSettings.PreviewWidth) { if width > int(utils.Cfg.FileSettings.PreviewWidth) {
preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.Lanczos3) preview = resize.Resize(utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, img, resize.Lanczos3)
} else { } else {
preview = img preview = img
} }
@@ -294,8 +294,8 @@ type ImageGetResult struct {
} }
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("getFileInfo", "Unable to get file info. Amazon S3 not configured and local server storage turned off. ", "") c.Err = model.NewAppError("uploadFile", "Unable to get file info. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -357,8 +357,8 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func getFile(c *Context, w http.ResponseWriter, r *http.Request) { func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("getFile", "Unable to get file. Amazon S3 not configured and local server storage turned off. ", "") c.Err = model.NewAppError("uploadFile", "Unable to get file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -400,7 +400,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
asyncGetFile(path, fileData) asyncGetFile(path, fileData)
if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 { if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 {
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) {
c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "") c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "")
return return
} }
@@ -442,17 +442,17 @@ func asyncGetFile(path string, fileData chan []byte) {
} }
func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.TeamSettings.AllowPublicLink { if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "") c.Err = model.NewAppError("uploadFile", "Unable to get link. Image storage is not configured.", "")
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 c.Err.StatusCode = http.StatusNotImplemented
return return
} }
if !utils.Cfg.FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "")
c.Err.StatusCode = http.StatusForbidden
}
props := model.MapFromJson(r.Body) props := model.MapFromJson(r.Body)
filename := props["filename"] filename := props["filename"]
@@ -478,7 +478,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps) data := model.MapToJson(newProps)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
url := fmt.Sprintf("%s/api/v1/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL(), channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId) url := fmt.Sprintf("%s/api/v1/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL(), channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId)
@@ -511,13 +511,13 @@ func getExport(c *Context, w http.ResponseWriter, r *http.Request) {
func writeFile(f []byte, path string) *model.AppError { func writeFile(f []byte, path string) *model.AppError {
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
ext := filepath.Ext(path) ext := filepath.Ext(path)
@@ -534,12 +534,12 @@ func writeFile(f []byte, path string) *model.AppError {
if err != nil { if err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error()) return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error())
} }
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil { if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error()) return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error())
} }
if err := ioutil.WriteFile(utils.Cfg.ServiceSettings.StorageDirectory+path, f, 0644); err != nil { if err := ioutil.WriteFile(utils.Cfg.FileSettings.Directory+path, f, 0644); err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error()) return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
} }
} else { } else {
@@ -551,13 +551,13 @@ func writeFile(f []byte, path string) *model.AppError {
func readFile(path string) ([]byte, *model.AppError) { func readFile(path string) ([]byte, *model.AppError) {
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
// try to get the file from S3 with some basic retry logic // try to get the file from S3 with some basic retry logic
tries := 0 tries := 0
@@ -573,8 +573,8 @@ func readFile(path string) ([]byte, *model.AppError) {
} }
time.Sleep(3000 * time.Millisecond) time.Sleep(3000 * time.Millisecond)
} }
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if f, err := ioutil.ReadFile(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil { if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewAppError("readFile", "Encountered an error reading from local server storage", err.Error()) return nil, model.NewAppError("readFile", "Encountered an error reading from local server storage", err.Error())
} else { } else {
return f, nil return f, nil
@@ -585,14 +585,14 @@ func readFile(path string) ([]byte, *model.AppError) {
} }
func openFileWriteStream(path string) (io.Writer, *model.AppError) { func openFileWriteStream(path string) (io.Writer, *model.AppError) {
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "") return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "")
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil { if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error()) return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error())
} }
if fileHandle, err := os.Create(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil { if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error()) return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error())
} else { } else {
fileHandle.Chmod(0644) fileHandle.Chmod(0644)

Просмотреть файл

@@ -38,7 +38,7 @@ func BenchmarkGetFile(b *testing.B) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps) data := model.MapToJson(newProps)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
// wait a bit for files to ready // wait a bit for files to ready
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)

Просмотреть файл

@@ -68,7 +68,7 @@ func TestUploadFile(t *testing.T) {
} }
resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType())
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
if appErr != nil { if appErr != nil {
t.Fatal(appErr) t.Fatal(appErr)
} }
@@ -81,11 +81,11 @@ func TestUploadFile(t *testing.T) {
fileId := strings.Split(filename, ".")[0] fileId := strings.Split(filename, ".")[0]
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
// wait a bit for files to ready // wait a bit for files to ready
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
@@ -104,7 +104,7 @@ func TestUploadFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
if strings.Contains(filename, "../") { if strings.Contains(filename, "../") {
@@ -115,17 +115,17 @@ func TestUploadFile(t *testing.T) {
// wait a bit for files to ready // wait a bit for files to ready
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) 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 := &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) channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@@ -222,7 +222,7 @@ func TestGetFile(t *testing.T) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps) data := model.MapToJson(newProps)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt))
Client.LoginByEmail(team2.Name, user2.Email, "pwd") Client.LoginByEmail(team2.Name, user2.Email, "pwd")
@@ -262,13 +262,13 @@ func TestGetFile(t *testing.T) {
t.Fatal("Should have errored - user not logged in and link not public") t.Fatal("Should have errored - user not logged in and link not public")
} }
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] 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] filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
fileId := strings.Split(filename, ".")[0] fileId := strings.Split(filename, ".")[0]
path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) 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 := &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) channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@@ -410,14 +410,14 @@ func TestGetPublicLink(t *testing.T) {
t.Fatal("should have errored, user not member of channel") t.Fatal("should have errored, user not member of channel")
} }
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
// perform clean-up on s3 // perform clean-up on s3
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/")
filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] 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] filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1]
fileId := strings.Split(filename, ".")[0] fileId := strings.Split(filename, ".")[0]
path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
path = utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }

Просмотреть файл

@@ -25,7 +25,6 @@ func InitPost(r *mux.Router) {
sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter() sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter()
sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST") sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updatePost)).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/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET")
sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, 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) { func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) {
var pchan store.StoreChannel var pchan store.StoreChannel
if len(post.RootId) > 0 { if len(post.RootId) > 0 {

Просмотреть файл

@@ -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) { func TestUpdatePost(t *testing.T) {
Setup() Setup()

Просмотреть файл

@@ -38,11 +38,11 @@ func NewServer() {
func StartServer() { func StartServer() {
l4g.Info("Starting Server...") 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 var handler http.Handler = Srv.Router
if utils.Cfg.RateLimitSettings.UseRateLimiter { if utils.Cfg.RateLimitSettings.EnableRateLimiter {
l4g.Info("RateLimiter is enabled") l4g.Info("RateLimiter is enabled")
vary := throttled.VaryBy{} vary := throttled.VaryBy{}
@@ -71,7 +71,7 @@ func StartServer() {
} }
go func() { go func() {
err := Srv.Server.ListenAndServe(":"+utils.Cfg.ServiceSettings.Port, handler) err := Srv.Server.ListenAndServe(utils.Cfg.ServiceSettings.ListenAddress, handler)
if err != nil { if err != nil {
l4g.Critical("Error starting server, err:%v", err) l4g.Critical("Error starting server, err:%v", err)
time.Sleep(time.Second) time.Sleep(time.Second)

Просмотреть файл

@@ -38,7 +38,7 @@ func InitTeam(r *mux.Router) {
} }
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.ServiceSettings.DisableEmailSignUp { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "") c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
@@ -60,14 +60,13 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
subjectPage.Props["SiteURL"] = c.GetSiteURL() subjectPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage := NewServerTemplatePage("signup_team_body") bodyPage := NewServerTemplatePage("signup_team_body")
bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink
props := make(map[string]string) props := make(map[string]string)
props["email"] = email props["email"] = email
props["time"] = fmt.Sprintf("%v", model.GetMillis()) props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props) 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)) bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_team_complete/?d=%s&h=%s", c.GetSiteURL(), url.QueryEscape(data), url.QueryEscape(hash))
@@ -76,10 +75,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return 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.Header().Set("Access-Control-Allow-Origin", " *")
w.Write([]byte(model.MapToJson(m))) w.Write([]byte(model.MapToJson(m)))
} }
@@ -88,7 +84,8 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r) params := mux.Vars(r)
service := params["service"] service := params["service"]
if !utils.IsServiceAllowed(service) { sso := utils.Cfg.GetSSOService(service)
if sso != nil && !sso.Enable {
c.SetInvalidParam("createTeamFromSSO", "service") c.SetInvalidParam("createTeamFromSSO", "service")
return return
} }
@@ -126,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 { if result := <-Srv.Store.Team().Save(team); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return
@@ -147,7 +142,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.ServiceSettings.DisableEmailSignUp { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "") c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
@@ -188,7 +183,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
teamSignup.User.TeamId = "" teamSignup.User.TeamId = ""
teamSignup.User.Password = password 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", "") c.Err = model.NewAppError("createTeamFromSignup", "The signup link does not appear to be valid", "")
return return
} }
@@ -209,8 +204,6 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
teamSignup.Team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault
if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil { if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return
@@ -230,13 +223,6 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
if teamSignup.Team.AllowValet {
CreateValet(c, rteam)
if c.Err != nil {
return
}
}
InviteMembers(c, rteam, ruser, teamSignup.Invites) InviteMembers(c, rteam, ruser, teamSignup.Invites)
teamSignup.Team = *rteam teamSignup.Team = *rteam
@@ -257,7 +243,7 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func CreateTeam(c *Context, team *model.Team) *model.Team { func CreateTeam(c *Context, team *model.Team) *model.Team {
if utils.Cfg.ServiceSettings.DisableEmailSignUp { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return nil return nil
@@ -272,11 +258,6 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
return nil 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 { if result := <-Srv.Store.Team().Save(team); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return nil return nil
@@ -288,13 +269,6 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
return nil return nil
} }
if rteam.AllowValet {
CreateValet(c, rteam)
if c.Err != nil {
return nil
}
}
return rteam return rteam
} }
} }
@@ -303,7 +277,7 @@ func isTreamCreationAllowed(c *Context, email string) bool {
email = strings.ToLower(email) 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.", "") c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
return false return false
} }
@@ -509,10 +483,10 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
props["name"] = team.Name props["name"] = team.Name
props["time"] = fmt.Sprintf("%v", model.GetMillis()) props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props) 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)) 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"]) l4g.Info("sending invitation to %v %v", invite, bodyPage.Props["Link"])
} }
@@ -569,8 +543,6 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
allowValet := allowValetStr == "true"
teamId := props["team_id"] teamId := props["team_id"]
if len(teamId) > 0 && len(teamId) != 26 { if len(teamId) > 0 && len(teamId) != 26 {
c.SetInvalidParam("updateValetFeature", "team_id") c.SetInvalidParam("updateValetFeature", "team_id")
@@ -599,8 +571,6 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
team = tResult.Data.(*model.Team) team = tResult.Data.(*model.Team)
} }
team.AllowValet = allowValet
if result := <-Srv.Store.Team().Update(team); result.Err != nil { if result := <-Srv.Store.Team().Update(team); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return

Просмотреть файл

@@ -30,7 +30,7 @@ func TestCreateFromSignupTeam(t *testing.T) {
props["time"] = fmt.Sprintf("%v", model.GetMillis()) props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props) 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} 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"} user := model.User{Email: props["email"], Nickname: "Corey Hulen", Password: "hello"}
@@ -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")
}
}

Просмотреть файл

@@ -58,7 +58,7 @@ func InitUser(r *mux.Router) {
} }
func createUser(c *Context, w http.ResponseWriter, r *http.Request) { func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.ServiceSettings.DisableEmailSignUp { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail {
c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "") c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
@@ -90,7 +90,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
data := r.URL.Query().Get("d") data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data)) 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", "") c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "")
return return
} }
@@ -155,19 +155,13 @@ func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool
return shouldVerifyHash 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 { 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 := "" channelRole := ""
if team.Email == user.Email { if team.Email == user.Email {
user.Roles = model.ROLE_TEAM_ADMIN user.Roles = model.ROLE_TEAM_ADMIN
@@ -290,7 +284,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
func checkUserPassword(c *Context, user *model.User, password string) bool { 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.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 = 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 c.Err.StatusCode = http.StatusForbidden
@@ -321,7 +315,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) { func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
c.LogAuditWithUserId(user.Id, "attempt") 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 = model.NewAppError("Login", "Login failed because email address has not been verified", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
return return
@@ -652,7 +646,7 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError
initial := string(strings.ToUpper(username)[0]) initial := string(strings.ToUpper(username)[0])
fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.ImageSettings.InitialFont) fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.FileSettings.InitialFont)
if err != nil { if err != nil {
return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error())
} }
@@ -661,8 +655,8 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError
return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error())
} }
width := int(utils.Cfg.ImageSettings.ProfileWidth) width := int(utils.Cfg.FileSettings.ProfileWidth)
height := int(utils.Cfg.ImageSettings.ProfileHeight) height := int(utils.Cfg.FileSettings.ProfileHeight)
color := colors[int64(seed)%int64(len(colors))] color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, width, height)) dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
srcImg := image.White srcImg := image.White
@@ -701,7 +695,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
} else { } else {
var img []byte var img []byte
if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if len(utils.Cfg.FileSettings.DriverName) == 0 {
var err *model.AppError var err *model.AppError
if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil {
c.Err = err c.Err = err
@@ -738,8 +732,8 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func uploadProfileImage(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 { if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "") c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Image storage is not configured.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -781,7 +775,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
} }
// Scale profile image // Scale profile image
img = resize.Resize(utils.Cfg.ImageSettings.ProfileWidth, utils.Cfg.ImageSettings.ProfileHeight, img, resize.Lanczos3) img = resize.Resize(utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, img, resize.Lanczos3)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = png.Encode(buf, img) err = png.Encode(buf, img)
@@ -1132,7 +1126,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(newProps) 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)) link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
@@ -1211,7 +1205,7 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return 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", "") c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
return return
} }
@@ -1359,15 +1353,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) { func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri, loginHint string) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow { sso := utils.Cfg.GetSSOService(service)
if sso != nil && !sso.Enable {
c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
c.Err.StatusCode = http.StatusBadRequest c.Err.StatusCode = http.StatusBadRequest
return return
} }
clientId := utils.Cfg.SSOSettings[service].Id clientId := sso.Id
endpoint := utils.Cfg.SSOSettings[service].AuthEndpoint endpoint := sso.AuthEndpoint
scope := utils.Cfg.SSOSettings[service].Scope scope := sso.Scope
stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)} stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)}
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
@@ -1386,7 +1381,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) { func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow { sso := utils.Cfg.GetSSOService(service)
if sso != nil && !sso.Enable {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
} }
@@ -1399,7 +1395,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
stateProps := model.MapFromJson(strings.NewReader(stateStr)) 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", "") return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
} }
@@ -1411,14 +1407,14 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
tchan := Srv.Store.Team().GetByName(teamName) tchan := Srv.Store.Team().GetByName(teamName)
p := url.Values{} p := url.Values{}
p.Set("client_id", utils.Cfg.SSOSettings[service].Id) p.Set("client_id", sso.Id)
p.Set("client_secret", utils.Cfg.SSOSettings[service].Secret) p.Set("client_secret", sso.Secret)
p.Set("code", code) p.Set("code", code)
p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
p.Set("redirect_uri", redirectUri) p.Set("redirect_uri", redirectUri)
client := &http.Client{} 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("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
@@ -1440,7 +1436,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
p = url.Values{} p = url.Values{}
p.Set("access_token", ar.AccessToken) 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("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")

Просмотреть файл

@@ -151,7 +151,7 @@ func TestLogin(t *testing.T) {
props["display_name"] = rteam2.Data.(*model.Team).DisplayName props["display_name"] = rteam2.Data.(*model.Team).DisplayName
props["time"] = fmt.Sprintf("%v", model.GetMillis()) props["time"] = fmt.Sprintf("%v", model.GetMillis())
data := model.MapToJson(props) 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) ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash)
@@ -352,19 +352,19 @@ func TestUserCreateImage(t *testing.T) {
Client.DoApiGet("/users/"+user.Id+"/image", "", "") Client.DoApiGet("/users/"+user.Id+"/image", "", "")
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} else { } else {
path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) 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) user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id)) store.Must(Srv.Store.User().VerifyEmail(user.Id))
if utils.IsS3Configured() || utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName != "" {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@@ -450,19 +450,19 @@ func TestUserUploadProfileImage(t *testing.T) {
Client.DoApiGet("/users/"+user.Id+"/image", "", "") Client.DoApiGet("/users/"+user.Id+"/image", "", "")
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
var auth aws.Auth var auth aws.Auth
auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region])
bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} else { } else {
path := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
t.Fatal("Couldn't remove file at " + path) t.Fatal("Couldn't remove file at " + path)
} }
@@ -814,7 +814,7 @@ func TestResetPassword(t *testing.T) {
props["user_id"] = user.Id props["user_id"] = user.Id
props["time"] = fmt.Sprintf("%v", model.GetMillis()) props["time"] = fmt.Sprintf("%v", model.GetMillis())
data["data"] = model.MapToJson(props) 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 data["name"] = team.Name
if _, err := Client.ResetPassword(data); err != nil { if _, err := Client.ResetPassword(data); err != nil {
@@ -952,6 +952,7 @@ func TestUserUpdateNotify(t *testing.T) {
} }
func TestFuzzyUserCreate(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} team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team) rteam, _ := Client.CreateTeam(&team)

Просмотреть файл

@@ -16,7 +16,7 @@ import (
func TestSocket(t *testing.T) { func TestSocket(t *testing.T) {
Setup() 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 := &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) team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)

Просмотреть файл

@@ -21,7 +21,7 @@ func InitWebhook(r *mux.Router) {
} }
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "") c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
@@ -66,7 +66,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "") c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return
@@ -103,7 +103,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "") c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented c.Err.StatusCode = http.StatusNotImplemented
return return

Просмотреть файл

@@ -31,7 +31,7 @@ func TestCreateIncomingHook(t *testing.T) {
hook := &model.IncomingWebhook{ChannelId: channel1.Id} hook := &model.IncomingWebhook{ChannelId: channel1.Id}
if utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
var rhook *model.IncomingWebhook var rhook *model.IncomingWebhook
if result, err := Client.CreateIncomingWebhook(hook); err != nil { if result, err := Client.CreateIncomingWebhook(hook); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -89,7 +89,7 @@ func TestListIncomingHooks(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 := &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) channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
if utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook1 := &model.IncomingWebhook{ChannelId: channel1.Id} hook1 := &model.IncomingWebhook{ChannelId: channel1.Id}
hook1 = Client.Must(Client.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook) hook1 = Client.Must(Client.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook)
@@ -127,7 +127,7 @@ func TestDeleteIncomingHook(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 := &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) channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
if utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook := &model.IncomingWebhook{ChannelId: channel1.Id} hook := &model.IncomingWebhook{ChannelId: channel1.Id}
hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook)

Просмотреть файл

@@ -1,71 +1,74 @@
{ {
"LogSettings": {
"ConsoleEnable": true,
"ConsoleLevel": "DEBUG",
"FileEnable": true,
"FileLevel": "INFO",
"FileFormat": "",
"FileLocation": ""
},
"ServiceSettings": { "ServiceSettings": {
"SiteName": "Mattermost", "ListenAddress": ":8065",
"Mode": "dev", "MaximumLoginAttempts": 10,
"AllowTesting": false, "SegmentDeveloperKey": "",
"UseSSL": false, "GoogleDeveloperKey": "",
"Port": "8065",
"Version": "developer",
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
"AnalyticsUrl": "",
"UseLocalStorage": true,
"StorageDirectory": "./data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false, "EnableOAuthServiceProvider": false,
"AllowIncomingWebhooks": false "EnableIncomingWebhooks": false,
"EnableTesting": false
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
"DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
}, },
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8", "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
"DataSourceReplicas": [ "DataSourceReplicas": [],
"mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"
],
"MaxIdleConns": 10, "MaxIdleConns": 10,
"MaxOpenConns": 10, "MaxOpenConns": 10,
"Trace": false, "Trace": false,
"AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg"
}, },
"AWSSettings": { "LogSettings": {
"S3AccessKeyId": "", "EnableConsole": true,
"S3SecretAccessKey": "", "ConsoleLevel": "DEBUG",
"S3Bucket": "", "EnableFile": true,
"S3Region": "" "FileLevel": "INFO",
"FileFormat": "",
"FileLocation": ""
}, },
"ImageSettings": { "FileSettings": {
"DriverName": "local",
"Directory": "./data/",
"EnablePublicLink": true,
"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
"ThumbnailWidth": 120, "ThumbnailWidth": 120,
"ThumbnailHeight": 100, "ThumbnailHeight": 100,
"PreviewWidth": 1024, "PreviewWidth": 1024,
"PreviewHeight": 0, "PreviewHeight": 0,
"ProfileWidth": 128, "ProfileWidth": 128,
"ProfileHeight": 128, "ProfileHeight": 128,
"InitialFont": "luximbi.ttf" "InitialFont": "luximbi.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",
"AmazonS3Region": ""
}, },
"EmailSettings": { "EmailSettings": {
"ByPassEmail": true, "EnableSignUpWithEmail": true,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "",
"SMTPUsername": "", "SMTPUsername": "",
"SMTPPassword": "", "SMTPPassword": "",
"SMTPServer": "", "SMTPServer": "",
"UseTLS": false, "SMTPPort": "",
"UseStartTLS": false, "ConnectionSecurity": "",
"FeedbackEmail": "", "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"FeedbackName": "", "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
"ApplePushServer": "", "ApplePushServer": "",
"ApplePushCertPublic": "", "ApplePushCertPublic": "",
"ApplePushCertPrivate": "" "ApplePushCertPrivate": ""
}, },
"RateLimitSettings": { "RateLimitSettings": {
"UseRateLimiter": true, "EnableRateLimiter": true,
"PerSec": 10, "PerSec": 10,
"MemoryStoreSize": 10000, "MemoryStoreSize": 10000,
"VaryByRemoteAddr": true, "VaryByRemoteAddr": true,
@@ -73,32 +76,15 @@
}, },
"PrivacySettings": { "PrivacySettings": {
"ShowEmailAddress": true, "ShowEmailAddress": true,
"ShowPhoneNumber": true,
"ShowSkypeId": true,
"ShowFullName": true "ShowFullName": true
}, },
"ClientSettings": { "GitLabSettings": {
"SegmentDeveloperKey": "", "Enable": false,
"GoogleDeveloperKey": "" "Secret": "",
}, "Id": "",
"TeamSettings": { "Scope": "",
"MaxUsersPerTeam": 150, "AuthEndpoint": "",
"AllowPublicLink": true, "TokenEndpoint": "",
"AllowValetDefault": false, "UserApiEndpoint": ""
"TourLink": "",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
},
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
} }
} }

Просмотреть файл

@@ -1,82 +1,74 @@
{ {
"ServiceSettings": {
"ListenAddress": ":80",
"MaximumLoginAttempts": 10,
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableTesting": false
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
"DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
},
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
"DataSourceReplicas": [],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
"AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"
},
"LogSettings": { "LogSettings": {
"ConsoleEnable": true, "EnableConsole": false,
"ConsoleLevel": "INFO", "ConsoleLevel": "INFO",
"FileEnable": true, "EnableFile": true,
"FileLevel": "INFO", "FileLevel": "INFO",
"FileFormat": "", "FileFormat": "",
"FileLocation": "" "FileLocation": ""
}, },
"ServiceSettings": { "FileSettings": {
"SiteName": "Mattermost", "DriverName": "local",
"Mode" : "dev", "Directory": "/mattermost/data/",
"AllowTesting" : true, "EnablePublicLink": true,
"UseSSL": false, "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX",
"Port": "80",
"Version": "developer",
"Shards": {
},
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
"AnalyticsUrl": "",
"UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false,
"AllowIncomingWebhooks": 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": {
"ThumbnailWidth": 120, "ThumbnailWidth": 120,
"ThumbnailHeight": 100, "ThumbnailHeight": 100,
"PreviewWidth": 1024, "PreviewWidth": 1024,
"PreviewHeight": 0, "PreviewHeight": 0,
"ProfileWidth": 128, "ProfileWidth": 128,
"ProfileHeight": 128, "ProfileHeight": 128,
"InitialFont": "luximbi.ttf" "InitialFont": "luximbi.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",
"AmazonS3Region": ""
}, },
"EmailSettings": { "EmailSettings": {
"ByPassEmail" : true, "EnableSignUpWithEmail": true,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "",
"SMTPUsername": "", "SMTPUsername": "",
"SMTPPassword": "", "SMTPPassword": "",
"SMTPServer": "", "SMTPServer": "",
"UseTLS": false, "SMTPPort": "",
"UseStartTLS": false, "ConnectionSecurity": "",
"FeedbackEmail": "", "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo",
"FeedbackName": "", "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e",
"ApplePushServer": "", "ApplePushServer": "",
"ApplePushCertPublic": "", "ApplePushCertPublic": "",
"ApplePushCertPrivate": "" "ApplePushCertPrivate": ""
}, },
"RateLimitSettings": { "RateLimitSettings": {
"UseRateLimiter": true, "EnableRateLimiter": true,
"PerSec": 10, "PerSec": 10,
"MemoryStoreSize": 10000, "MemoryStoreSize": 10000,
"VaryByRemoteAddr": true, "VaryByRemoteAddr": true,
@@ -84,22 +76,15 @@
}, },
"PrivacySettings": { "PrivacySettings": {
"ShowEmailAddress": true, "ShowEmailAddress": true,
"ShowPhoneNumber": true,
"ShowSkypeId": true,
"ShowFullName": true "ShowFullName": true
}, },
"TeamSettings": { "GitLabSettings": {
"MaxUsersPerTeam": 150, "Enable": false,
"AllowPublicLink": true, "Secret": "",
"AllowValetDefault": false, "Id": "",
"TermsLink": "/static/help/configure_links.html", "Scope": "",
"PrivacyLink": "/static/help/configure_links.html", "AuthEndpoint": "",
"AboutLink": "/static/help/configure_links.html", "TokenEndpoint": "",
"HelpLink": "/static/help/configure_links.html", "UserApiEndpoint": ""
"ReportProblemLink": "/static/help/configure_links.html",
"TourLink": "/static/help/configure_links.html",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
} }
} }

Просмотреть файл

@@ -1,82 +1,74 @@
{ {
"ServiceSettings": {
"ListenAddress": ":80",
"MaximumLoginAttempts": 10,
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableTesting": false
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
"DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
},
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
"DataSourceReplicas": [],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
"AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"
},
"LogSettings": { "LogSettings": {
"ConsoleEnable": true, "EnableConsole": false,
"ConsoleLevel": "INFO", "ConsoleLevel": "INFO",
"FileEnable": true, "EnableFile": true,
"FileLevel": "INFO", "FileLevel": "INFO",
"FileFormat": "", "FileFormat": "",
"FileLocation": "" "FileLocation": ""
}, },
"ServiceSettings": { "FileSettings": {
"SiteName": "Mattermost", "DriverName": "local",
"Mode" : "dev", "Directory": "/mattermost/data/",
"AllowTesting" : true, "EnablePublicLink": true,
"UseSSL": false, "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AX",
"Port": "80",
"Version": "developer",
"Shards": {
},
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
"AnalyticsUrl": "",
"UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false,
"AllowIncomingWebhooks": 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": {
"ThumbnailWidth": 120, "ThumbnailWidth": 120,
"ThumbnailHeight": 100, "ThumbnailHeight": 100,
"PreviewWidth": 1024, "PreviewWidth": 1024,
"PreviewHeight": 0, "PreviewHeight": 0,
"ProfileWidth": 128, "ProfileWidth": 128,
"ProfileHeight": 128, "ProfileHeight": 128,
"InitialFont": "luximbi.ttf" "InitialFont": "luximbi.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",
"AmazonS3Region": ""
}, },
"EmailSettings": { "EmailSettings": {
"ByPassEmail" : true, "EnableSignUpWithEmail": true,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "",
"SMTPUsername": "", "SMTPUsername": "",
"SMTPPassword": "", "SMTPPassword": "",
"SMTPServer": "", "SMTPServer": "",
"UseTLS": false, "SMTPPort": "",
"UseStartTLS": false, "ConnectionSecurity": "",
"FeedbackEmail": "", "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo",
"FeedbackName": "", "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5e",
"ApplePushServer": "", "ApplePushServer": "",
"ApplePushCertPublic": "", "ApplePushCertPublic": "",
"ApplePushCertPrivate": "" "ApplePushCertPrivate": ""
}, },
"RateLimitSettings": { "RateLimitSettings": {
"UseRateLimiter": true, "EnableRateLimiter": true,
"PerSec": 10, "PerSec": 10,
"MemoryStoreSize": 10000, "MemoryStoreSize": 10000,
"VaryByRemoteAddr": true, "VaryByRemoteAddr": true,
@@ -84,22 +76,15 @@
}, },
"PrivacySettings": { "PrivacySettings": {
"ShowEmailAddress": true, "ShowEmailAddress": true,
"ShowPhoneNumber": true,
"ShowSkypeId": true,
"ShowFullName": true "ShowFullName": true
}, },
"TeamSettings": { "GitLabSettings": {
"MaxUsersPerTeam": 150, "Enable": false,
"AllowPublicLink": true, "Secret": "",
"AllowValetDefault": false, "Id": "",
"TermsLink": "/static/help/configure_links.html", "Scope": "",
"PrivacyLink": "/static/help/configure_links.html", "AuthEndpoint": "",
"AboutLink": "/static/help/configure_links.html", "TokenEndpoint": "",
"HelpLink": "/static/help/configure_links.html", "UserApiEndpoint": ""
"ReportProblemLink": "/static/help/configure_links.html",
"TourLink": "/static/help/configure_links.html",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
} }
} }

Просмотреть файл

@@ -53,7 +53,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
} }
// Create a client for tests to use // 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 // Check for username parameter and create a user if present
username, ok1 := params["username"] username, ok1 := params["username"]

Просмотреть файл

@@ -57,7 +57,7 @@ func main() {
api.StartServer() api.StartServer()
// If we allow testing then listen for manual testing URL hits // If we allow testing then listen for manual testing URL hits
if utils.Cfg.ServiceSettings.AllowTesting { if utils.Cfg.ServiceSettings.EnableTesting {
manualtesting.InitManualTesting() manualtesting.InitManualTesting()
} }

Просмотреть файл

@@ -21,6 +21,7 @@ const (
HEADER_ETAG_SERVER = "ETag" HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match" HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For" HEADER_FORWARDED = "X-Forwarded-For"
HEADER_REAL_IP = "X-Real-IP"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token" HEADER_TOKEN = "token"
HEADER_BEARER = "BEARER" HEADER_BEARER = "BEARER"
@@ -403,6 +404,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) { func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil { if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err return nil, err

Просмотреть файл

@@ -8,27 +8,29 @@ import (
"io" "io"
) )
const (
CONN_SECURITY_NONE = ""
CONN_SECURITY_TLS = "TLS"
CONN_SECURITY_STARTTLS = "STARTTLS"
IMAGE_DRIVER_LOCAL = "local"
IMAGE_DRIVER_S3 = "amazons3"
SERVICE_GITLAB = "gitlab"
)
type ServiceSettings struct { type ServiceSettings struct {
SiteName string ListenAddress string
Mode string MaximumLoginAttempts int
AllowTesting bool SegmentDeveloperKey string
UseSSL bool GoogleDeveloperKey string
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool EnableOAuthServiceProvider bool
AllowIncomingWebhooks bool EnableIncomingWebhooks bool
EnableTesting bool
} }
type SSOSetting struct { type SSOSettings struct {
Allow bool Enable bool
Secret string Secret string
Id string Id string
Scope string Scope string
@@ -48,87 +50,84 @@ type SqlSettings struct {
} }
type LogSettings struct { type LogSettings struct {
ConsoleEnable bool EnableConsole bool
ConsoleLevel string ConsoleLevel string
FileEnable bool EnableFile bool
FileLevel string FileLevel string
FileFormat string FileFormat string
FileLocation string FileLocation string
} }
type AWSSettings struct { type FileSettings struct {
S3AccessKeyId string DriverName string
S3SecretAccessKey string Directory string
S3Bucket string EnablePublicLink bool
S3Region string PublicLinkSalt string
} ThumbnailWidth uint
ThumbnailHeight uint
type ImageSettings struct { PreviewWidth uint
ThumbnailWidth uint PreviewHeight uint
ThumbnailHeight uint ProfileWidth uint
PreviewWidth uint ProfileHeight uint
PreviewHeight uint InitialFont string
ProfileWidth uint AmazonS3AccessKeyId string
ProfileHeight uint AmazonS3SecretAccessKey string
InitialFont string AmazonS3Bucket string
AmazonS3Region string
} }
type EmailSettings struct { type EmailSettings struct {
ByPassEmail bool EnableSignUpWithEmail bool
SMTPUsername string SendEmailNotifications bool
SMTPPassword string RequireEmailVerification bool
SMTPServer string FeedbackName string
UseTLS bool FeedbackEmail string
UseStartTLS bool SMTPUsername string
FeedbackEmail string SMTPPassword string
FeedbackName string SMTPServer string
SMTPPort string
ConnectionSecurity string
InviteSalt string
PasswordResetSalt string
// For Future Use
ApplePushServer string ApplePushServer string
ApplePushCertPublic string ApplePushCertPublic string
ApplePushCertPrivate string ApplePushCertPrivate string
} }
type RateLimitSettings struct { type RateLimitSettings struct {
UseRateLimiter bool EnableRateLimiter bool
PerSec int PerSec int
MemoryStoreSize int MemoryStoreSize int
VaryByRemoteAddr bool VaryByRemoteAddr bool
VaryByHeader string VaryByHeader string
} }
type PrivacySettings struct { type PrivacySettings struct {
ShowEmailAddress bool ShowEmailAddress bool
ShowPhoneNumber bool
ShowSkypeId bool
ShowFullName bool ShowFullName bool
} }
type ClientSettings struct {
SegmentDeveloperKey string
GoogleDeveloperKey string
}
type TeamSettings struct { type TeamSettings struct {
SiteName string
MaxUsersPerTeam int MaxUsersPerTeam int
AllowPublicLink bool
AllowValetDefault bool
TourLink string
DefaultThemeColor string DefaultThemeColor string
DisableTeamCreation bool EnableTeamCreation bool
EnableUserCreation bool
RestrictCreationToDomains string RestrictCreationToDomains string
} }
type Config struct { type Config struct {
LogSettings LogSettings
ServiceSettings ServiceSettings ServiceSettings ServiceSettings
TeamSettings TeamSettings
SqlSettings SqlSettings SqlSettings SqlSettings
AWSSettings AWSSettings LogSettings LogSettings
ImageSettings ImageSettings FileSettings FileSettings
EmailSettings EmailSettings EmailSettings EmailSettings
RateLimitSettings RateLimitSettings RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings PrivacySettings PrivacySettings
ClientSettings ClientSettings GitLabSettings SSOSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
} }
func (o *Config) ToJson() string { func (o *Config) ToJson() string {
@@ -140,6 +139,14 @@ func (o *Config) ToJson() string {
} }
} }
func (o *Config) GetSSOService(service string) *SSOSettings {
if service == SERVICE_GITLAB {
return &o.GitLabSettings
}
return nil
}
func ConfigFromJson(data io.Reader) *Config { func ConfigFromJson(data io.Reader) *Config {
decoder := json.NewDecoder(data) decoder := json.NewDecoder(data)
var o Config var o Config

Просмотреть файл

@@ -27,7 +27,6 @@ type Team struct {
Type string `json:"type"` Type string `json:"type"`
CompanyName string `json:"company_name"` CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"` AllowedDomains string `json:"allowed_domains"`
AllowValet bool `json:"allow_valet"`
} }
type Invites struct { type Invites struct {

Просмотреть файл

@@ -51,11 +51,18 @@ func NewSqlStore() Store {
utils.Cfg.SqlSettings.DataSource, utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.DataSource, utils.Cfg.SqlSettings.MaxIdleConns,
utils.Cfg.SqlSettings.MaxOpenConns, utils.Cfg.SqlSettings.Trace) utils.Cfg.SqlSettings.MaxOpenConns, utils.Cfg.SqlSettings.Trace)
sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas)) if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas { sqlStore.replicas = make([]*gorp.DbMap, 1)
sqlStore.replicas[i] = setupConnection(fmt.Sprintf("replica-%v", i), utils.Cfg.SqlSettings.DriverName, replica, 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.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
utils.Cfg.SqlSettings.Trace) 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() schemaVersion := sqlStore.GetCurrentSchemaVersion()
@@ -308,26 +315,21 @@ func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
} }
} }
// func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool { func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool {
// // XXX TODO FIXME this should be removed after 0.6.0 if !ss.DoesColumnExist(tableName, columnName) {
// if utils.Cfg.SqlSettings.DriverName == "postgres" { return false
// return false }
// }
// if !ss.DoesColumnExist(tableName, columnName) { _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName)
// return false 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) return true
// if err != nil { }
// l4g.Critical("Failed to drop column %v", err)
// time.Sleep(time.Second)
// panic("Failed to drop column " + err.Error())
// }
// return true
// }
// func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool { // func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool {

Просмотреть файл

@@ -28,6 +28,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
} }
func (s SqlTeamStore) UpgradeSchemaIfNeeded() { func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
s.RemoveColumnIfExists("Teams", "AllowValet")
} }
func (s SqlTeamStore) CreateIndexesIfNotExists() { func (s SqlTeamStore) CreateIndexesIfNotExists() {

Просмотреть файл

@@ -42,7 +42,7 @@ func TestUserStoreSave(t *testing.T) {
t.Fatal("should be unique username") t.Fatal("should be unique username")
} }
for i := 0; i < 150; i++ { for i := 0; i < 50; i++ {
u1.Id = "" u1.Id = ""
u1.Email = model.NewId() u1.Email = model.NewId()
u1.Username = model.NewId() u1.Username = model.NewId()

Просмотреть файл

@@ -58,9 +58,9 @@ func FindDir(dir string) string {
func ConfigureCmdLineLog() { func ConfigureCmdLineLog() {
ls := model.LogSettings{} ls := model.LogSettings{}
ls.ConsoleEnable = true ls.EnableConsole = true
ls.ConsoleLevel = "ERROR" ls.ConsoleLevel = "ERROR"
ls.FileEnable = false ls.EnableFile = false
configureLog(&ls) configureLog(&ls)
} }
@@ -68,7 +68,7 @@ func configureLog(s *model.LogSettings) {
l4g.Close() l4g.Close()
if s.ConsoleEnable { if s.EnableConsole {
level := l4g.DEBUG level := l4g.DEBUG
if s.ConsoleLevel == "INFO" { if s.ConsoleLevel == "INFO" {
level = l4g.INFO level = l4g.INFO
@@ -79,7 +79,7 @@ func configureLog(s *model.LogSettings) {
l4g.AddFilter("stdout", level, l4g.NewConsoleLogWriter()) l4g.AddFilter("stdout", level, l4g.NewConsoleLogWriter())
} }
if s.FileEnable { if s.EnableFile {
var fileFormat = s.FileFormat var fileFormat = s.FileFormat
@@ -161,8 +161,6 @@ func getSanitizeOptions(c *model.Config) map[string]bool {
options := map[string]bool{} options := map[string]bool{}
options["fullname"] = c.PrivacySettings.ShowFullName options["fullname"] = c.PrivacySettings.ShowFullName
options["email"] = c.PrivacySettings.ShowEmailAddress options["email"] = c.PrivacySettings.ShowEmailAddress
options["skypeid"] = c.PrivacySettings.ShowSkypeId
options["phonenumber"] = c.PrivacySettings.ShowPhoneNumber
return options return options
} }
@@ -175,57 +173,24 @@ func getClientProperties(c *model.Config) map[string]string {
props["BuildDate"] = model.BuildDate props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash props["BuildHash"] = model.BuildHash
props["SiteName"] = c.ServiceSettings.SiteName props["SiteName"] = c.TeamSettings.SiteName
props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink)
props["SegmentDeveloperKey"] = c.ClientSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ClientSettings.GoogleDeveloperKey
props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl
props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail)
props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight)
props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
props["AllowIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.AllowIncomingWebhooks)
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable)
props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink)
props["ProfileHeight"] = fmt.Sprintf("%v", c.FileSettings.ProfileHeight)
props["ProfileWidth"] = fmt.Sprintf("%v", c.FileSettings.ProfileWidth)
return props return props
} }
func IsS3Configured() bool {
if Cfg.AWSSettings.S3AccessKeyId == "" || Cfg.AWSSettings.S3SecretAccessKey == "" || Cfg.AWSSettings.S3Region == "" || Cfg.AWSSettings.S3Bucket == "" {
return false
}
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
}
if service, ok := Cfg.SSOSettings[s]; ok {
if service.Allow {
return true
}
}
return false
}

Просмотреть файл

@@ -15,43 +15,22 @@ import (
"time" "time"
) )
func CheckMailSettings() *model.AppError { func connectToSMTPServer(config *model.Config) (net.Conn, *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)
var conn net.Conn var conn net.Conn
var err error var err error
if Cfg.EmailSettings.UseTLS { if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
tlsconfig := &tls.Config{ tlsconfig := &tls.Config{
InsecureSkipVerify: true, 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 { if err != nil {
return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error()) return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
} }
} else { } else {
conn, err = net.Dial("tcp", Cfg.EmailSettings.SMTPServer) conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if err != nil { if err != nil {
return nil, model.NewAppError("SendMail", "Failed to open connection", err.Error()) return nil, model.NewAppError("SendMail", "Failed to open connection", err.Error())
} }
@@ -60,24 +39,23 @@ func connectToSMTPServer() (net.Conn, *model.AppError) {
return conn, nil return conn, nil
} }
func newSMTPClient(conn net.Conn) (*smtp.Client, *model.AppError) { func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
host, _, _ := net.SplitHostPort(Cfg.EmailSettings.SMTPServer) c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
c, err := smtp.NewClient(conn, host)
if err != nil { if err != nil {
l4g.Error("Failed to open a connection to SMTP server %v", err) l4g.Error("Failed to open a connection to SMTP server %v", err)
return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error()) return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
} }
// GO does not support plain auth over a non encrypted connection. // GO does not support plain auth over a non encrypted connection.
// so if not tls then no auth // so if not tls then no auth
auth := smtp.PlainAuth("", Cfg.EmailSettings.SMTPUsername, Cfg.EmailSettings.SMTPPassword, host) auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if Cfg.EmailSettings.UseTLS { if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
if err = c.Auth(auth); err != nil { if err = c.Auth(auth); err != nil {
return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error()) return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error())
} }
} else if Cfg.EmailSettings.UseStartTLS { } else if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
tlsconfig := &tls.Config{ tlsconfig := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
ServerName: host, ServerName: config.EmailSettings.SMTPServer,
} }
c.StartTLS(tlsconfig) c.StartTLS(tlsconfig)
if err = c.Auth(auth); err != nil { if err = c.Auth(auth); err != nil {
@@ -88,12 +66,16 @@ func newSMTPClient(conn net.Conn) (*smtp.Client, *model.AppError) {
} }
func SendMail(to, subject, body string) *model.AppError { 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 return nil
} }
fromMail := mail.Address{Cfg.EmailSettings.FeedbackName, Cfg.EmailSettings.FeedbackEmail} fromMail := mail.Address{config.EmailSettings.FeedbackName, config.EmailSettings.FeedbackEmail}
toMail := mail.Address{"", to} toMail := mail.Address{"", to}
headers := make(map[string]string) headers := make(map[string]string)
@@ -110,13 +92,13 @@ func SendMail(to, subject, body string) *model.AppError {
} }
message += "\r\n<html><body>" + body + "</body></html>" message += "\r\n<html><body>" + body + "</body></html>"
conn, err1 := connectToSMTPServer() conn, err1 := connectToSMTPServer(config)
if err1 != nil { if err1 != nil {
return err1 return err1
} }
defer conn.Close() defer conn.Close()
c, err2 := newSMTPClient(conn) c, err2 := newSMTPClient(conn, config)
if err2 != nil { if err2 != nil {
return err2 return err2
} }

Просмотреть файл

@@ -9,6 +9,13 @@ var LoadingScreen = require('../loading_screen.jsx');
var EmailSettingsTab = require('./email_settings.jsx'); var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx');
var LogsTab = require('./logs.jsx'); var LogsTab = require('./logs.jsx');
var FileSettingsTab = require('./image_settings.jsx');
var PrivacySettingsTab = require('./privacy_settings.jsx');
var RateSettingsTab = require('./rate_settings.jsx');
var GitLabSettingsTab = require('./gitlab_settings.jsx');
var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
export default class AdminController extends React.Component { export default class AdminController extends React.Component {
constructor(props) { constructor(props) {
@@ -19,7 +26,7 @@ export default class AdminController extends React.Component {
this.state = { this.state = {
config: null, config: null,
selected: 'email_settings' selected: 'service_settings'
}; };
} }
@@ -53,6 +60,20 @@ export default class AdminController extends React.Component {
tab = <LogSettingsTab config={this.state.config} />; tab = <LogSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') { } else if (this.state.selected === 'logs') {
tab = <LogsTab />; tab = <LogsTab />;
} else if (this.state.selected === 'image_settings') {
tab = <FileSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'privacy_settings') {
tab = <PrivacySettingsTab config={this.state.config} />;
} else if (this.state.selected === 'rate_settings') {
tab = <RateSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'gitlab_settings') {
tab = <GitLabSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'sql_settings') {
tab = <SqlSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_settings') {
tab = <TeamSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
} }
} }

Просмотреть файл

@@ -38,6 +38,33 @@ export default class AdminSidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li> <li>
<ul className='nav nav__sub-menu'> <ul className='nav nav__sub-menu'>
<li>
<a
href='#'
className={this.isSelected('service_settings')}
onClick={this.handleClick.bind(this, 'service_settings')}
>
{'Service Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('team_settings')}
onClick={this.handleClick.bind(this, 'team_settings')}
>
{'Team Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('sql_settings')}
onClick={this.handleClick.bind(this, 'sql_settings')}
>
{'SQL Settings'}
</a>
</li>
<li> <li>
<a <a
href='#' href='#'
@@ -47,6 +74,15 @@ export default class AdminSidebar extends React.Component {
{'Email Settings'} {'Email Settings'}
</a> </a>
</li> </li>
<li>
<a
href='#'
className={this.isSelected('image_settings')}
onClick={this.handleClick.bind(this, 'image_settings')}
>
{'File Settings'}
</a>
</li>
<li> <li>
<a <a
href='#' href='#'
@@ -65,6 +101,33 @@ export default class AdminSidebar extends React.Component {
{'Logs'} {'Logs'}
</a> </a>
</li> </li>
<li>
<a
href='#'
className={this.isSelected('rate_settings')}
onClick={this.handleClick.bind(this, 'rate_settings')}
>
{'Rate Limit Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('privacy_settings')}
onClick={this.handleClick.bind(this, 'privacy_settings')}
>
{'Privacy Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('gitlab_settings')}
onClick={this.handleClick.bind(this, 'gitlab_settings')}
>
{'GitLab Settings'}
</a>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>

Просмотреть файл

@@ -1,15 +1,179 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var crypto = require('crypto');
export default class EmailSettings extends React.Component { export default class EmailSettings extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleChange = this.handleChange.bind(this);
this.handleTestConnection = this.handleTestConnection.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.buildConfig = this.buildConfig.bind(this);
this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
this.handleGenerateReset = this.handleGenerateReset.bind(this);
this.state = { this.state = {
sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
saveNeeded: false,
serverError: null,
emailSuccess: null,
emailFail: null
}; };
} }
handleChange(action) {
var s = {saveNeeded: true, serverError: this.state.serverError};
if (action === 'sendEmailNotifications_true') {
s.sendEmailNotifications = true;
}
if (action === 'sendEmailNotifications_false') {
s.sendEmailNotifications = false;
}
this.setState(s);
}
buildConfig() {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked;
config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked;
config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim();
config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim();
config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim();
config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim();
config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim();
config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim();
config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim();
config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim();
if (config.EmailSettings.InviteSalt === '') {
config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
}
config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim();
if (config.EmailSettings.PasswordResetSalt === '') {
config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
}
return config;
}
handleGenerateInvite(e) {
e.preventDefault();
React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleGenerateReset(e) {
e.preventDefault();
React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleTestConnection(e) {
e.preventDefault();
$('#connection-button').button('loading');
var config = this.buildConfig();
Client.testEmail(
config,
() => {
this.setState({
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
serverError: null,
saveNeeded: true,
emailSuccess: true,
emailFail: null
});
$('#connection-button').button('reset');
},
(err) => {
this.setState({
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
serverError: null,
saveNeeded: true,
emailSuccess: null,
emailFail: err.message + ' - ' + err.detailed_error
});
$('#connection-button').button('reset');
}
);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.buildConfig();
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
serverError: null,
saveNeeded: false,
emailSuccess: null,
emailFail: null
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
serverError: err.message,
saveNeeded: true,
emailSuccess: null,
emailFail: null
});
$('#save-button').button('reset');
}
);
}
render() { render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
var emailSuccess = '';
if (this.state.emailSuccess) {
emailSuccess = (
<div className='alert alert-success'>
<i className='fa fa-check'></i>{'No errors were reported while sending an email. Please check your inbox to make sure.'}
</div>
);
}
var emailFail = '';
if (this.state.emailFail) {
emailSuccess = (
<div className='alert alert-warning'>
<i className='fa fa-warning'></i>{'Connection unsuccessful: ' + this.state.emailFail}
</div>
);
}
return ( return (
<div className='wrapper--fixed'> <div className='wrapper--fixed'>
<h3>{'Email Settings'}</h3> <h3>{'Email Settings'}</h3>
@@ -17,149 +181,130 @@ export default class EmailSettings extends React.Component {
className='form-horizontal' className='form-horizontal'
role='form' role='form'
> >
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='email' htmlFor='allowSignUpWithEmail'
> >
{'Bypass Email: '} {'Allow Sign Up With Email: '}
<a
href='#'
data-trigger='hover click'
data-toggle='popover'
data-position='bottom'
data-content={'Here\'s some more help text inside a popover for the Bypass Email field just to show how popovers look.'}
>
{'(?)'}
</a>
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<label className='radio-inline'> <label className='radio-inline'>
<input <input
type='radio' type='radio'
name='byPassEmail' name='allowSignUpWithEmail'
value='option1' value='true'
ref='allowSignUpWithEmail'
defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
/> />
{'True'} {'true'}
</label> </label>
<label className='radio-inline'> <label className='radio-inline'>
<input <input
type='radio' type='radio'
name='byPassEmail' name='allowSignUpWithEmail'
value='option2' value='false'
defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
/> />
{'False'} {'false'}
</label> </label>
<p className='help-text'>{'This is some sample help text for the Bypass Email field'}</p> <p className='help-text'>{'Typically set to true in production. When true Mattermost will allow team creation and account signup utilizing email and password. You would set this to false if you only wanted to allow signup from a service like OAuth or LDAP.'}</p>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='smtpUsername' htmlFor='sendEmailNotifications'
> >
{'SMTP Username:'} {'Send Email Notifications: '}
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<input <label className='radio-inline'>
type='email' <input
className='form-control' type='radio'
id='smtpUsername' name='sendEmailNotifications'
placeholder='Enter your SMTP username' value='true'
value='' ref='sendEmailNotifications'
/> defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
<div className='help-text'> onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
<div className='alert alert-warning'><i className='fa fa-warning'></i>{' This is some error text for the Bypass Email field'}</div> />
</div> {'true'}
<p className='help-text'>{'This is some sample help text for the SMTP username field'}</p> </label>
<label className='radio-inline'>
<input
type='radio'
name='sendEmailNotifications'
value='false'
defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to true in production. When true Mattermost will attempt to send email notifications. Developers may set this field to false skipping sending emails for faster development.'}</p>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='smtpPassword' htmlFor='requireEmailVerification'
> >
{'SMTP Password:'} {'Require Email Verification: '}
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<input <label className='radio-inline'>
type='password' <input
className='form-control' type='radio'
id='smtpPassword' name='requireEmailVerification'
placeholder='Enter your SMTP password' value='true'
value='' ref='requireEmailVerification'
/> defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
disabled={!this.state.sendEmailNotifications}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='requireEmailVerification'
value='false'
defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
disabled={!this.state.sendEmailNotifications}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to true in production. When true Mattermost will not allow a user to login without first having recieved an email with a verification link. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='smtpServer' htmlFor='feedbackName'
> >
{'SMTP Server:'} {'Feedback Name:'}
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<input <input
type='text' type='text'
className='form-control' className='form-control'
id='smtpServer' id='feedbackName'
placeholder='Enter your SMTP server' ref='feedbackName'
value='' placeholder='Ex: "Mattermost", "System", "John Smith"'
defaultValue={this.props.config.EmailSettings.FeedbackName}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/> />
<div className='help-text'> <p className='help-text'>{'Name displayed on email account used when sending notification emails from Mattermost.'}</p>
<a
href='#'
className='help-link'
>
{'Test Connection'}
</a>
<div className='alert alert-success'><i className='fa fa-check'></i>{' Connection successful'}</div>
<div className='alert alert-warning hide'><i className='fa fa-warning'></i>{' Connection unsuccessful'}</div>
</div>
</div>
</div>
<div className='form-group'>
<label className='control-label col-sm-4'>{'Use TLS:'}</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='tls'
value='option1'
/>
{'True'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='tls'
value='option2'
/>
{'False'}
</label>
</div>
</div>
<div className='form-group'>
<label className='control-label col-sm-4'>{'Use Start TLS:'}</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='starttls'
value='option1'
/>
{'True'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='starttls'
value='option2'
/>
{'False'}
</label>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
@@ -169,143 +314,237 @@ export default class EmailSettings extends React.Component {
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<input <input
type='text' type='email'
className='form-control' className='form-control'
id='feedbackEmail' id='feedbackEmail'
placeholder='Enter your feedback email' ref='feedbackEmail'
value='' placeholder='Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
defaultValue={this.props.config.EmailSettings.FeedbackEmail}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/> />
<p className='help-text'>{'Email displayed on email account used when sending notification emails from Mattermost.'}</p>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='feedbackUsername' htmlFor='SMTPUsername'
> >
{'Feedback Username:'} {'SMTP Username:'}
</label> </label>
<div className='col-sm-8'> <div className='col-sm-8'>
<input <input
type='text' type='text'
className='form-control' className='form-control'
id='feedbackUsername' id='SMTPUsername'
placeholder='Enter your feedback username' ref='SMTPUsername'
value='' placeholder='Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
defaultValue={this.props.config.EmailSettings.SMTPUsername}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/> />
<p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<div className='col-sm-offset-4 col-sm-8'> <label
<div className='checkbox'> className='control-label col-sm-4'
<label><input type='checkbox' />{'Remember me'}</label> htmlFor='SMTPPassword'
>
{'SMTP Password:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='SMTPPassword'
ref='SMTPPassword'
placeholder='Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
defaultValue={this.props.config.EmailSettings.SMTPPassword}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
<p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='SMTPServer'
>
{'SMTP Server:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='SMTPServer'
ref='SMTPServer'
placeholder='Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
defaultValue={this.props.config.EmailSettings.SMTPServer}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
<p className='help-text'>{'Location of SMTP email server.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='SMTPPort'
>
{'SMTP Port:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='SMTPPort'
ref='SMTPPort'
placeholder='Ex: "25", "465"'
defaultValue={this.props.config.EmailSettings.SMTPPort}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
<p className='help-text'>{'Port of SMTP email server.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ConnectionSecurity'
>
{'Connection Security:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='ConnectionSecurity'
ref='ConnectionSecurity'
defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
>
<option value=''>{'None'}</option>
<option value='TLS'>{'TLS (Recommended)'}</option>
<option value='STARTTLS'>{'STARTTLS'}</option>
</select>
<div className='help-text'>
<table
className='table-bordered'
cellPadding='5'
>
<tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
<tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
<tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
</table>
</div>
<div className='help-text'>
<button
className='help-link'
onClick={this.handleTestConnection}
disabled={!this.state.sendEmailNotifications}
id='connection-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Testing...'}
>
{'Test Connection'}
</button>
{emailSuccess}
{emailFail}
</div> </div>
</div> </div>
</div> </div>
<div <div className='form-group'>
className='panel-group' <label
id='accordion' className='control-label col-sm-4'
role='tablist' htmlFor='InviteSalt'
aria-multiselectable='true' >
> {'Invite Salt:'}
<div className='panel panel-default'> </label>
<div <div className='col-sm-8'>
className='panel-heading' <input
role='tab' type='text'
id='headingOne' className='form-control'
> id='InviteSalt'
<h3 className='panel-title'> ref='InviteSalt'
<a placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
className='collapsed' defaultValue={this.props.config.EmailSettings.InviteSalt}
role='button' onChange={this.handleChange}
data-toggle='collapse' disabled={!this.state.sendEmailNotifications}
data-parent='#accordion' />
href='#collapseOne' <p className='help-text'>{'32-character salt added to signing of email invites.'}</p>
aria-expanded='true' <div className='help-text'>
aria-controls='collapseOne' <button
> className='help-link'
{'Advanced Settings '} onClick={this.handleGenerateInvite}
<i className='fa fa-plus'></i> disabled={!this.state.sendEmailNotifications}
<i className='fa fa-minus'></i> >
</a> {'Re-Generate'}
</h3> </button>
</div> </div>
<div </div>
id='collapseOne' </div>
className='panel-collapse collapse'
role='tabpanel' <div className='form-group'>
aria-labelledby='headingOne' <label
> className='control-label col-sm-4'
<div className='panel-body'> htmlFor='PasswordResetSalt'
<div className='form-group'> >
<label {'Password Reset Salt:'}
className='control-label col-sm-4' </label>
htmlFor='feedbackUsername' <div className='col-sm-8'>
> <input
{'Apple push server:'} type='text'
</label> className='form-control'
<div className='col-sm-8'> id='PasswordResetSalt'
<input ref='PasswordResetSalt'
type='text' placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
className='form-control' defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
id='feedbackUsername' onChange={this.handleChange}
placeholder='Enter your Apple push server' disabled={!this.state.sendEmailNotifications}
value='' />
/> <p className='help-text'>{'32-character salt added to signing of password reset emails.'}</p>
<p className='help-text'>{'This is some sample help text for the Apple push server field'}</p> <div className='help-text'>
</div> <button
</div> className='help-link'
<div className='form-group'> onClick={this.handleGenerateReset}
<label disabled={!this.state.sendEmailNotifications}
className='control-label col-sm-4' >
htmlFor='feedbackUsername' {'Re-Generate'}
> </button>
{'Apple push certificate public:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='feedbackUsername'
placeholder='Enter your public apple push certificate'
value=''
/>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='feedbackUsername'
>
{'Apple push certificate private:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='feedbackUsername'
placeholder='Enter your private apple push certificate'
value=''
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<div className='col-sm-12'> <div className='col-sm-12'>
{serverError}
<button <button
disabled={!this.state.saveNeeded}
type='submit' type='submit'
className='btn btn-primary' className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
> >
{'Save'} {'Save'}
</button> </button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
); );
} }
} }
EmailSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,277 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class GitLabSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
Allow: this.props.config.GitLabSettings.Allow,
saveNeeded: false,
serverError: null
};
}
handleChange(action) {
var s = {saveNeeded: true, serverError: this.state.serverError};
if (action === 'AllowTrue') {
s.Allow = true;
}
if (action === 'AllowFalse') {
s.Allow = false;
}
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.GitLabSettings.Allow = React.findDOMNode(this.refs.Allow).checked;
config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'GitLab Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Allow'
>
{'Enable Sign Up With GitLab: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='Allow'
value='true'
ref='Allow'
defaultChecked={this.props.config.GitLabSettings.Allow}
onChange={this.handleChange.bind(this, 'AllowTrue')}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='Allow'
value='false'
defaultChecked={!this.props.config.GitLabSettings.Allow}
onChange={this.handleChange.bind(this, 'AllowFalse')}
/>
{'false'}
</label>
<p className='help-text'>{'When true Mattermost will allow team creation and account signup utilizing GitLab OAuth.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Secret'
>
{'Secret:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='Secret'
ref='Secret'
placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
defaultValue={this.props.config.GitLabSettings.Secret}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Id'
>
{'Id:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='Id'
ref='Id'
placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
defaultValue={this.props.config.GitLabSettings.Id}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Scope'
>
{'Scope:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='Scope'
ref='Scope'
placeholder='Ex ""'
defaultValue={this.props.config.GitLabSettings.Scope}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AuthEndpoint'
>
{'Auth Endpoint:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AuthEndpoint'
ref='AuthEndpoint'
placeholder='Ex ""'
defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='TokenEndpoint'
>
{'Token Endpoint:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='TokenEndpoint'
ref='TokenEndpoint'
placeholder='Ex ""'
defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='UserApiEndpoint'
>
{'User API Endpoint:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='UserApiEndpoint'
ref='UserApiEndpoint'
placeholder='Ex ""'
defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
<p className='help-text'>{'Need help text.'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
GitLabSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,496 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var crypto = require('crypto');
export default class FileSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleGenerate = this.handleGenerate.bind(this);
this.state = {
saveNeeded: false,
serverError: null,
DriverName: this.props.config.FileSettings.DriverName
};
}
handleChange(action) {
var s = {saveNeeded: true, serverError: this.state.serverError};
if (action === 'DriverName') {
s.DriverName = React.findDOMNode(this.refs.DriverName).value;
}
this.setState(s);
}
handleGenerate(e) {
e.preventDefault();
React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value;
config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value;
config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value;
config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value;
config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked;
config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim();
if (config.FileSettings.PublicLinkSalt === '') {
config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
}
var thumbnailWidth = 120;
if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10);
}
config.FileSettings.ThumbnailWidth = thumbnailWidth;
React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
var thumbnailHeight = 100;
if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10);
}
config.FileSettings.ThumbnailHeight = thumbnailHeight;
React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
var previewWidth = 1024;
if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) {
previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10);
}
config.FileSettings.PreviewWidth = previewWidth;
React.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
var previewHeight = 0;
if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) {
previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10);
}
config.FileSettings.PreviewHeight = previewHeight;
React.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
var profileWidth = 128;
if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) {
profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10);
}
config.FileSettings.ProfileWidth = profileWidth;
React.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
var profileHeight = 128;
if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) {
profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10);
}
config.FileSettings.ProfileHeight = profileHeight;
React.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
var enableFile = false;
var enableS3 = false;
if (this.state.DriverName === 'local') {
enableFile = true;
}
if (this.state.DriverName === 'amazons3') {
enableS3 = true;
}
return (
<div className='wrapper--fixed'>
<h3>{'File Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='DriverName'
>
{'Store Files In:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='DriverName'
ref='DriverName'
defaultValue={this.props.config.FileSettings.DriverName}
onChange={this.handleChange.bind(this, 'DriverName')}
>
<option value=''>{'Disable File Storage'}</option>
<option value='local'>{'Local File System'}</option>
<option value='amazons3'>{'Amazon S3'}</option>
</select>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Directory'
>
{'Local Directory Location:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='Directory'
ref='Directory'
placeholder='Ex "./data/"'
defaultValue={this.props.config.FileSettings.Directory}
onChange={this.handleChange}
disabled={!enableFile}
/>
<p className='help-text'>{'Directory to which image files are written. If blank, will be set to ./data/.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AmazonS3AccessKeyId'
>
{'Amazon S3 Access Key Id:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AmazonS3AccessKeyId'
ref='AmazonS3AccessKeyId'
placeholder='Ex "AKIADTOVBGERKLCBV"'
defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
onChange={this.handleChange}
disabled={!enableS3}
/>
<p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AmazonS3SecretAccessKey'
>
{'Amazon S3 Secret Access Key:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AmazonS3SecretAccessKey'
ref='AmazonS3SecretAccessKey'
placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
onChange={this.handleChange}
disabled={!enableS3}
/>
<p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AmazonS3Bucket'
>
{'Amazon S3 Bucket:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AmazonS3Bucket'
ref='AmazonS3Bucket'
placeholder='Ex "mattermost-media"'
defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
onChange={this.handleChange}
disabled={!enableS3}
/>
<p className='help-text'>{'Name you selected for your S3 bucket in AWS.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AmazonS3Region'
>
{'Amazon S3 Region:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AmazonS3Region'
ref='AmazonS3Region'
placeholder='Ex "us-east-1"'
defaultValue={this.props.config.FileSettings.AmazonS3Region}
onChange={this.handleChange}
disabled={!enableS3}
/>
<p className='help-text'>{'AWS region you selected for creating your S3 bucket.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ThumbnailWidth'
>
{'Thumbnail Width:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='ThumbnailWidth'
ref='ThumbnailWidth'
placeholder='Ex "120"'
defaultValue={this.props.config.FileSettings.ThumbnailWidth}
onChange={this.handleChange}
/>
<p className='help-text'>{'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ThumbnailHeight'
>
{'Thumbnail Height:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='ThumbnailHeight'
ref='ThumbnailHeight'
placeholder='Ex "100"'
defaultValue={this.props.config.FileSettings.ThumbnailHeight}
onChange={this.handleChange}
/>
<p className='help-text'>{'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='PreviewWidth'
>
{'Preview Width:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='PreviewWidth'
ref='PreviewWidth'
placeholder='Ex "1024"'
defaultValue={this.props.config.FileSettings.PreviewWidth}
onChange={this.handleChange}
/>
<p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='PreviewHeight'
>
{'Preview Height:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='PreviewHeight'
ref='PreviewHeight'
placeholder='Ex "0"'
defaultValue={this.props.config.FileSettings.PreviewHeight}
onChange={this.handleChange}
/>
<p className='help-text'>{'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ProfileWidth'
>
{'Profile Width:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='ProfileWidth'
ref='ProfileWidth'
placeholder='Ex "1024"'
defaultValue={this.props.config.FileSettings.ProfileWidth}
onChange={this.handleChange}
/>
<p className='help-text'>{'Width of profile picture.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ProfileHeight'
>
{'Profile Height:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='ProfileHeight'
ref='ProfileHeight'
placeholder='Ex "0"'
defaultValue={this.props.config.FileSettings.ProfileHeight}
onChange={this.handleChange}
/>
<p className='help-text'>{'Height of profile picture.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnablePublicLink'
>
{'Share Public File Link: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnablePublicLink'
value='true'
ref='EnablePublicLink'
defaultChecked={this.props.config.FileSettings.EnablePublicLink}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnablePublicLink'
value='false'
defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Allow users to share public links to files and images.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='PublicLinkSalt'
>
{'Public Link Salt:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='PublicLinkSalt'
ref='PublicLinkSalt'
placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
defaultValue={this.props.config.FileSettings.PublicLinkSalt}
onChange={this.handleChange}
/>
<p className='help-text'>{'32-character salt added to signing of public image links.'}</p>
<div className='help-text'>
<button
className='help-link'
onClick={this.handleGenerate}
>
{'Re-Generate'}
</button>
</div>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
FileSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -12,13 +12,33 @@ export default class LogSettings extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.state = { this.state = {
consoleEnable: this.props.config.LogSettings.EnableConsole,
fileEnable: this.props.config.LogSettings.EnableFile,
saveNeeded: false, saveNeeded: false,
serverError: null serverError: null
}; };
} }
handleChange() { handleChange(action) {
this.setState({saveNeeded: true, serverError: this.state.serverError}); 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) { handleSubmit(e) {
@@ -26,9 +46,9 @@ export default class LogSettings extends React.Component {
$('#save-button').button('loading'); $('#save-button').button('loading');
var config = this.props.config; 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.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.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim(); config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim(); config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
@@ -37,11 +57,21 @@ export default class LogSettings extends React.Component {
config, config,
() => { () => {
AsyncClient.getConfig(); AsyncClient.getConfig();
this.setState({serverError: null, saveNeeded: false}); this.setState({
consoleEnable: config.LogSettings.EnableConsole,
fileEnable: config.LogSettings.EnableFile,
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset'); $('#save-button').button('reset');
}, },
(err) => { (err) => {
this.setState({serverError: err.message, saveNeeded: true}); this.setState({
consoleEnable: config.LogSettings.EnableConsole,
fileEnable: config.LogSettings.EnableFile,
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset'); $('#save-button').button('reset');
} }
); );
@@ -80,8 +110,8 @@ export default class LogSettings extends React.Component {
name='consoleEnable' name='consoleEnable'
value='true' value='true'
ref='consoleEnable' ref='consoleEnable'
defaultChecked={this.props.config.LogSettings.ConsoleEnable} defaultChecked={this.props.config.LogSettings.EnableConsole}
onChange={this.handleChange} onChange={this.handleChange.bind(this, 'console_true')}
/> />
{'true'} {'true'}
</label> </label>
@@ -90,8 +120,8 @@ export default class LogSettings extends React.Component {
type='radio' type='radio'
name='consoleEnable' name='consoleEnable'
value='false' value='false'
defaultChecked={!this.props.config.LogSettings.ConsoleEnable} defaultChecked={!this.props.config.LogSettings.EnableConsole}
onChange={this.handleChange} onChange={this.handleChange.bind(this, 'console_false')}
/> />
{'false'} {'false'}
</label> </label>
@@ -113,6 +143,7 @@ export default class LogSettings extends React.Component {
ref='consoleLevel' ref='consoleLevel'
defaultValue={this.props.config.LogSettings.consoleLevel} defaultValue={this.props.config.LogSettings.consoleLevel}
onChange={this.handleChange} onChange={this.handleChange}
disabled={!this.state.consoleEnable}
> >
<option value='DEBUG'>{'DEBUG'}</option> <option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option> <option value='INFO'>{'INFO'}</option>
@@ -135,8 +166,8 @@ export default class LogSettings extends React.Component {
name='fileEnable' name='fileEnable'
ref='fileEnable' ref='fileEnable'
value='true' value='true'
defaultChecked={this.props.config.LogSettings.FileEnable} defaultChecked={this.props.config.LogSettings.EnableFile}
onChange={this.handleChange} onChange={this.handleChange.bind(this, 'file_true')}
/> />
{'true'} {'true'}
</label> </label>
@@ -145,8 +176,8 @@ export default class LogSettings extends React.Component {
type='radio' type='radio'
name='fileEnable' name='fileEnable'
value='false' value='false'
defaultChecked={!this.props.config.LogSettings.FileEnable} defaultChecked={!this.props.config.LogSettings.EnableFile}
onChange={this.handleChange} onChange={this.handleChange.bind(this, 'file_false')}
/> />
{'false'} {'false'}
</label> </label>
@@ -168,6 +199,7 @@ export default class LogSettings extends React.Component {
ref='fileLevel' ref='fileLevel'
defaultValue={this.props.config.LogSettings.FileLevel} defaultValue={this.props.config.LogSettings.FileLevel}
onChange={this.handleChange} onChange={this.handleChange}
disabled={!this.state.fileEnable}
> >
<option value='DEBUG'>{'DEBUG'}</option> <option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option> <option value='INFO'>{'INFO'}</option>
@@ -193,6 +225,7 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file location' placeholder='Enter your file location'
defaultValue={this.props.config.LogSettings.FileLocation} defaultValue={this.props.config.LogSettings.FileLocation}
onChange={this.handleChange} onChange={this.handleChange}
disabled={!this.state.fileEnable}
/> />
<p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p> <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
</div> </div>
@@ -214,6 +247,7 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file format' placeholder='Enter your file format'
defaultValue={this.props.config.LogSettings.FileFormat} defaultValue={this.props.config.LogSettings.FileFormat}
onChange={this.handleChange} onChange={this.handleChange}
disabled={!this.state.fileEnable}
/> />
<p className='help-text'> <p className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'} {'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}

Просмотреть файл

@@ -0,0 +1,163 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class PrivacySettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'Privacy Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ShowEmailAddress'
>
{'Show Email Address: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='ShowEmailAddress'
value='true'
ref='ShowEmailAddress'
defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='ShowEmailAddress'
value='false'
defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Hides email address of users from other users including team administrator.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ShowFullName'
>
{'Show Full Name: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='ShowFullName'
value='true'
ref='ShowFullName'
defaultChecked={this.props.config.PrivacySettings.ShowFullName}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='ShowFullName'
value='false'
defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Hides full name of users from other users including team administrator.'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
PrivacySettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,272 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class RateSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
saveNeeded: false,
serverError: null
};
}
handleChange(action) {
var s = {saveNeeded: true, serverError: this.state.serverError};
if (action === 'EnableRateLimiterTrue') {
s.EnableRateLimiter = true;
}
if (action === 'EnableRateLimiterFalse') {
s.EnableRateLimiter = false;
}
if (action === 'VaryByRemoteAddrTrue') {
s.VaryByRemoteAddr = true;
}
if (action === 'VaryByRemoteAddrFalse') {
s.VaryByRemoteAddr = false;
}
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.RateLimitSettings.EnableRateLimiter = React.findDOMNode(this.refs.EnableRateLimiter).checked;
config.RateLimitSettings.VaryByRemoteAddr = React.findDOMNode(this.refs.VaryByRemoteAddr).checked;
config.RateLimitSettings.VaryByHeader = React.findDOMNode(this.refs.VaryByHeader).value.trim();
var PerSec = 10;
if (!isNaN(parseInt(React.findDOMNode(this.refs.PerSec).value, 10))) {
PerSec = parseInt(React.findDOMNode(this.refs.PerSec).value, 10);
}
config.RateLimitSettings.PerSec = PerSec;
React.findDOMNode(this.refs.PerSec).value = PerSec;
var MemoryStoreSize = 10000;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
MemoryStoreSize = parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10);
}
config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
React.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<div className='banner'>
<div className='banner__content'>
<h4 className='banner__heading'>{'Note:'}</h4>
<p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
</div>
</div>
<h3>{'Rate Limit Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableRateLimiter'
>
{'Enable Rate Limiter: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableRateLimiter'
value='true'
ref='EnableRateLimiter'
defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableRateLimiter'
value='false'
defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
/>
{'false'}
</label>
<p className='help-text'>{'When enabled throttles rate at which APIs respond.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='PerSec'
>
{'Number Of Queries Per Second:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='PerSec'
ref='PerSec'
placeholder='Ex "10"'
defaultValue={this.props.config.RateLimitSettings.PerSec}
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter}
/>
<p className='help-text'>{'Throttles API at this number of requests per second.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='MemoryStoreSize'
>
{'Memory Store Size:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='MemoryStoreSize'
ref='MemoryStoreSize'
placeholder='Ex "10000"'
defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter}
/>
<p className='help-text'>{'Maximum number of users sessions connected to the system as determined by VaryByRemoteAddr and VaryByHeader variables.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='VaryByRemoteAddr'
>
{'Limit By Remote Address: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='VaryByRemoteAddr'
value='true'
ref='VaryByRemoteAddr'
defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
disabled={!this.state.EnableRateLimiter}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='VaryByRemoteAddr'
value='false'
defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
disabled={!this.state.EnableRateLimiter}
/>
{'false'}
</label>
<p className='help-text'>{'Rate limit API access by IP address.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='VaryByHeader'
>
{'Limit By Http Header:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='VaryByHeader'
ref='VaryByHeader'
placeholder='Ex "X-Real-IP", "X-Forwarded-For"'
defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
/>
<p className='help-text'>{'When filled in, vary rate limiting by http header field specified (e.g. when configuring ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
RateSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,296 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class ServiceSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.ServiceSettings.ListenAddress = React.findDOMNode(this.refs.ListenAddress).value.trim();
if (config.ServiceSettings.ListenAddress === '') {
config.ServiceSettings.ListenAddress = ':8065';
React.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
}
config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
var MaximumLoginAttempts = 10;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
MaximumLoginAttempts = parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
}
config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
React.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'Service Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='ListenAddress'
>
{'Listen Address:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='ListenAddress'
ref='ListenAddress'
placeholder='Ex ":8065"'
defaultValue={this.props.config.ServiceSettings.ListenAddress}
onChange={this.handleChange}
/>
<p className='help-text'>{'The address to bind to and listen. ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='MaximumLoginAttempts'
>
{'Maximum Login Attempts:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='MaximumLoginAttempts'
ref='MaximumLoginAttempts'
placeholder='Ex "10"'
defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
onChange={this.handleChange}
/>
<p className='help-text'>{'Login attempts allowed before user is locked out and required to reset password via email.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='SegmentDeveloperKey'
>
{'Segment Developer Key:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='SegmentDeveloperKey'
ref='SegmentDeveloperKey'
placeholder='Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
onChange={this.handleChange}
/>
<p className='help-text'>{'For users running a SaaS services, sign up for a key at Segment.com to track metrics.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='GoogleDeveloperKey'
>
{'Google Developer Key:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='GoogleDeveloperKey'
ref='GoogleDeveloperKey'
placeholder='Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
onChange={this.handleChange}
/>
<p className='help-text'>{'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '}<a href='https://www.youtube.com/watch?v=Im69kzhpR3I'>{'https://www.youtube.com/watch?v=Im69kzhpR3I'}</a>{'. Leaving field blank disables the automatic generation of YouTube video previews from links.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableOAuthServiceProvider'
>
{'Enable OAuth Service Provider: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableOAuthServiceProvider'
value='true'
ref='EnableOAuthServiceProvider'
defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableOAuthServiceProvider'
value='false'
defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When enabled Mattermost will act as an Oauth2 Provider. Changing this will require a server restart before taking effect.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableIncomingWebhooks'
>
{'EnableIncomingWebhooks: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableIncomingWebhooks'
value='true'
ref='EnableIncomingWebhooks'
defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableIncomingWebhooks'
value='false'
defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When true incomming web hooks will be allowed.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableTesting'
>
{'Enable Testing: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableTesting'
value='true'
ref='EnableTesting'
defaultChecked={this.props.config.ServiceSettings.EnableTesting}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableTesting'
value='false'
defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When true slash commands like /loadtest are enabled in the add comment box. Changing this will require a server restart before taking effect. Typically used for development.'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
ServiceSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,283 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var crypto = require('crypto');
export default class SqlSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleGenerate = this.handleGenerate.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.SqlSettings.Trace = React.findDOMNode(this.refs.Trace).checked;
config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
if (config.SqlSettings.AtRestEncryptKey === '') {
config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
}
var MaxOpenConns = 10;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
MaxOpenConns = parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10);
}
config.SqlSettings.MaxOpenConns = MaxOpenConns;
React.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
var MaxIdleConns = 10;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
MaxIdleConns = parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10);
}
config.SqlSettings.MaxIdleConns = MaxIdleConns;
React.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
handleGenerate(e) {
e.preventDefault();
React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
var dataSourceReplicas = '';
this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
});
if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
dataSourceReplicas = 'none';
}
return (
<div className='wrapper--fixed'>
<div className='banner'>
<div className='banner__content'>
<h4 className='banner__heading'>{'Note:'}</h4>
<p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
</div>
</div>
<h3>{'SQL Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='DriverName'
>
{'Driver Name:'}
</label>
<div className='col-sm-8'>
<p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='DataSource'
>
{'Data Source:'}
</label>
<div className='col-sm-8'>
<p className='help-text'>{dataSource}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='DataSourceReplicas'
>
{'Data Source Replicas:'}
</label>
<div className='col-sm-8'>
<p className='help-text'>{dataSourceReplicas}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='MaxIdleConns'
>
{'Maximum Idle Connections:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='MaxIdleConns'
ref='MaxIdleConns'
placeholder='Ex "10"'
defaultValue={this.props.config.SqlSettings.MaxIdleConns}
onChange={this.handleChange}
/>
<p className='help-text'>{'Maximum number of idle connections held open to the database.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='MaxOpenConns'
>
{'Maximum Open Connections:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='MaxOpenConns'
ref='MaxOpenConns'
placeholder='Ex "10"'
defaultValue={this.props.config.SqlSettings.MaxOpenConns}
onChange={this.handleChange}
/>
<p className='help-text'>{'Maximum number of open connections held open to the database.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='AtRestEncryptKey'
>
{'At Rest Encrypt Key:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='AtRestEncryptKey'
ref='AtRestEncryptKey'
placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
onChange={this.handleChange}
/>
<p className='help-text'>{'32-character salt available to encrypt and decrypt sensitive fields in database.'}</p>
<div className='help-text'>
<button
className='help-link'
onClick={this.handleGenerate}
>
{'Re-Generate'}
</button>
</div>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='Trace'
>
{'Trace: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='Trace'
value='true'
ref='Trace'
defaultChecked={this.props.config.SqlSettings.Trace}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='Trace'
value='false'
defaultChecked={!this.props.config.SqlSettings.Trace}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Output executing SQL statements to the log. Typically used for development.'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
SqlSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -0,0 +1,257 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class TeamSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
config.TeamSettings.DefaultThemeColor = React.findDOMNode(this.refs.DefaultThemeColor).value.trim();
config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
MaxUsersPerTeam = parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
}
config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
React.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({
serverError: null,
saveNeeded: false
});
$('#save-button').button('reset');
},
(err) => {
this.setState({
serverError: err.message,
saveNeeded: true
});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'Team Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='SiteName'
>
{'Site Name:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='SiteName'
ref='SiteName'
placeholder='Ex "Mattermost"'
defaultValue={this.props.config.TeamSettings.SiteName}
onChange={this.handleChange}
/>
<p className='help-text'>{'Name of service shown in login screens and UI.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='MaxUsersPerTeam'
>
{'Max Users Per Team:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='MaxUsersPerTeam'
ref='MaxUsersPerTeam'
placeholder='Ex "25"'
defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
onChange={this.handleChange}
/>
<p className='help-text'>{'Maximum number of users per team.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='DefaultThemeColor'
>
{'Default Theme Color:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='DefaultThemeColor'
ref='DefaultThemeColor'
placeholder='Ex "#2389D7"'
defaultValue={this.props.config.TeamSettings.DefaultThemeColor}
onChange={this.handleChange}
/>
<p className='help-text'>{'Default theme color for team sites.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableTeamCreation'
>
{'Enable Team Creation: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableTeamCreation'
value='true'
ref='EnableTeamCreation'
defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableTeamCreation'
value='false'
defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When false the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableUserCreation'
>
{'Enable User Creation: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableUserCreation'
value='true'
ref='EnableUserCreation'
defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableUserCreation'
value='false'
defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When false the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='RestrictCreationToDomains'
>
{'Restrict Creation To Domains:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='RestrictCreationToDomains'
ref='RestrictCreationToDomains'
placeholder='Ex "corp.mattermost.com, mattermost.org"'
defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
onChange={this.handleChange}
/>
<p className='help-text'>{'Teams can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
TeamSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component {
emailErrors: {}, emailErrors: {},
firstNameErrors: {}, firstNameErrors: {},
lastNameErrors: {}, lastNameErrors: {},
emailEnabled: !global.window.config.ByPassEmail emailEnabled: global.window.config.SendEmailNotifications === 'true'
}; };
} }

Просмотреть файл

@@ -5,7 +5,6 @@ const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx'); const Client = require('../utils/client.jsx');
const UserStore = require('../stores/user_store.jsx'); const UserStore = require('../stores/user_store.jsx');
const BrowserStore = require('../stores/browser_store.jsx'); const BrowserStore = require('../stores/browser_store.jsx');
const Constants = require('../utils/constants.jsx');
export default class Login extends React.Component { export default class Login extends React.Component {
constructor(props) { constructor(props) {
@@ -95,10 +94,8 @@ export default class Login extends React.Component {
focusEmail = true; focusEmail = true;
} }
const authServices = JSON.parse(this.props.authServices);
let loginMessage = []; let loginMessage = [];
if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) { if (global.window.config.EnableSignUpWithGitLab === 'true') {
loginMessage.push( loginMessage.push(
<a <a
className='btn btn-custom-login gitlab' className='btn btn-custom-login gitlab'
@@ -116,7 +113,7 @@ export default class Login extends React.Component {
} }
let emailSignup; let emailSignup;
if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = ( emailSignup = (
<div> <div>
<div className={'form-group' + errorClass}> <div className={'form-group' + errorClass}>
@@ -205,11 +202,9 @@ export default class Login extends React.Component {
Login.defaultProps = { Login.defaultProps = {
teamName: '', teamName: '',
teamDisplayName: '', teamDisplayName: ''
authServices: ''
}; };
Login.propTypes = { Login.propTypes = {
teamName: React.PropTypes.string, teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string, teamDisplayName: React.PropTypes.string
authServices: React.PropTypes.string
}; };

Просмотреть файл

@@ -12,38 +12,42 @@ export default class TeamSignUp extends React.Component {
this.updatePage = this.updatePage.bind(this); this.updatePage = this.updatePage.bind(this);
if (props.services.length === 1) { var count = 0;
if (props.services[0] === Constants.EMAIL_SERVICE) {
this.state = {page: 'email', service: ''}; if (global.window.config.EnableSignUpWithEmail === 'true') {
} else { count = count + 1;
this.state = {page: 'service', service: props.services[0]}; }
}
} else { if (global.window.config.EnableSignUpWithGitLab === 'true') {
this.state = {page: 'choose', service: ''}; count = count + 1;
}
if (count > 1) {
this.state = {page: 'choose'};
} else if (global.window.config.EnableSignUpWithEmail === 'true') {
this.state = {page: 'email'};
} else if (global.window.config.EnableSignUpWithGitLab === 'true') {
this.state = {page: 'gitlab'};
} }
} }
updatePage(page, service) {
this.setState({page: page, service: service}); updatePage(page) {
this.setState({page});
} }
render() { render() {
if (this.state.page === 'choose') {
return (
<ChoosePage
updatePage={this.updatePage}
/>
);
}
if (this.state.page === 'email') { if (this.state.page === 'email') {
return <EmailSignUpPage />; return <EmailSignUpPage />;
} else if (this.state.page === 'service' && this.state.service !== '') { } else if (this.state.page === 'gitlab') {
return <SSOSignupPage service={this.state.service} />; return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
} }
return (
<ChoosePage
services={this.props.services}
updatePage={this.updatePage}
/>
);
} }
} }
TeamSignUp.defaultProps = {
services: []
};
TeamSignUp.propTypes = {
services: React.PropTypes.array
};

Просмотреть файл

@@ -5,7 +5,6 @@ var Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx'); var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx');
var Constants = require('../utils/constants.jsx');
export default class SignupUserComplete extends React.Component { export default class SignupUserComplete extends React.Component {
constructor(props) { constructor(props) {
@@ -168,11 +167,8 @@ export default class SignupUserComplete extends React.Component {
</div> </div>
); );
// add options to log in using another service
var authServices = JSON.parse(this.props.authServices);
var signupMessage = []; var signupMessage = [];
if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) { if (global.window.config.EnableSignUpWithGitLab === 'true') {
signupMessage.push( signupMessage.push(
<a <a
className='btn btn-custom-login gitlab' className='btn btn-custom-login gitlab'
@@ -185,7 +181,7 @@ export default class SignupUserComplete extends React.Component {
} }
var emailSignup; var emailSignup;
if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = ( emailSignup = (
<div> <div>
<div className='inner__content'> <div className='inner__content'>
@@ -269,7 +265,6 @@ SignupUserComplete.defaultProps = {
teamId: '', teamId: '',
email: '', email: '',
data: null, data: null,
authServices: '',
teamDisplayName: '' teamDisplayName: ''
}; };
SignupUserComplete.propTypes = { SignupUserComplete.propTypes = {
@@ -278,6 +273,5 @@ SignupUserComplete.propTypes = {
teamId: React.PropTypes.string, teamId: React.PropTypes.string,
email: React.PropTypes.string, email: React.PropTypes.string,
data: React.PropTypes.string, data: React.PropTypes.string,
authServices: React.PropTypes.string,
teamDisplayName: React.PropTypes.string teamDisplayName: React.PropTypes.string
}; };

Просмотреть файл

@@ -1,8 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var Constants = require('../utils/constants.jsx');
export default class ChooseAuthPage extends React.Component { export default class ChooseAuthPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -10,7 +8,7 @@ export default class ChooseAuthPage extends React.Component {
} }
render() { render() {
var buttons = []; var buttons = [];
if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) { if (global.window.config.EnableSignUpWithGitLab === 'true') {
buttons.push( buttons.push(
<a <a
className='btn btn-custom-login gitlab btn-full' className='btn btn-custom-login gitlab btn-full'
@@ -18,17 +16,17 @@ export default class ChooseAuthPage extends React.Component {
onClick={ onClick={
function clickGit(e) { function clickGit(e) {
e.preventDefault(); e.preventDefault();
this.props.updatePage('service', Constants.GITLAB_SERVICE); this.props.updatePage('gitlab');
}.bind(this) }.bind(this)
} }
> >
<span className='icon' /> <span className='icon' />
<span>Create new team with GitLab Account</span> <span>{'Create new team with GitLab Account'}</span>
</a> </a>
); );
} }
if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) { if (global.window.config.EnableSignUpWithEmail === 'true') {
buttons.push( buttons.push(
<a <a
className='btn btn-custom-login email btn-full' className='btn btn-custom-login email btn-full'
@@ -36,18 +34,18 @@ export default class ChooseAuthPage extends React.Component {
onClick={ onClick={
function clickEmail(e) { function clickEmail(e) {
e.preventDefault(); e.preventDefault();
this.props.updatePage('email', ''); this.props.updatePage('email');
}.bind(this) }.bind(this)
} }
> >
<span className='fa fa-envelope' /> <span className='fa fa-envelope' />
<span>Create new team with email address</span> <span>{'Create new team with email address'}</span>
</a> </a>
); );
} }
if (buttons.length === 0) { if (buttons.length === 0) {
buttons = <span>No sign-up methods configured, please contact your system administrator.</span>; buttons = <span>{'No sign-up methods configured, please contact your system administrator.'}</span>;
} }
return ( return (
@@ -61,10 +59,6 @@ export default class ChooseAuthPage extends React.Component {
} }
} }
ChooseAuthPage.defaultProps = {
services: []
};
ChooseAuthPage.propTypes = { ChooseAuthPage.propTypes = {
services: React.PropTypes.array,
updatePage: React.PropTypes.func updatePage: React.PropTypes.func
}; };

Просмотреть файл

@@ -13,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.submitSkip = this.submitSkip.bind(this); this.submitSkip = this.submitSkip.bind(this);
this.keySubmit = this.keySubmit.bind(this); this.keySubmit = this.keySubmit.bind(this);
this.state = { this.state = {
emailEnabled: !global.window.config.ByPassEmail emailEnabled: global.window.config.SendEmailNotifications === 'true'
}; };
if (!this.state.emailEnabled) { if (!this.state.emailEnabled) {

Просмотреть файл

@@ -57,7 +57,7 @@ export default class UserProfile extends React.Component {
} }
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />'; var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
if (!global.window.config.ShowEmailAddress) { if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>'; dataContent += '<div class="text-nowrap">Email not shared</div>';
} else { } else {
dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>'; dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';

Просмотреть файл

@@ -208,7 +208,7 @@ export default class UserSettingsGeneralTab extends React.Component {
} }
setupInitialState(props) { setupInitialState(props) {
var user = props.user; var user = props.user;
var emailEnabled = !global.window.config.ByPassEmail; var emailEnabled = global.window.config.SendEmailNotifications === 'true';
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled}; email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
} }

Просмотреть файл

@@ -38,7 +38,7 @@ export default class UserSettingsModal extends React.Component {
if (global.window.config.EnableOAuthServiceProvider === 'true') { if (global.window.config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
} }
if (global.window.config.AllowIncomingWebhooks === 'true') { if (global.window.config.EnableIncomingWebhooks === 'true') {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
} }

Просмотреть файл

@@ -300,7 +300,7 @@ export default class ViewImageModal extends React.Component {
} }
var publicLink = ''; var publicLink = '';
if (global.window.config.AllowPublicLink) { if (global.window.config.EnablePublicLink === 'true') {
publicLink = ( publicLink = (
<div> <div>
<a <a

Просмотреть файл

@@ -8,7 +8,6 @@ function setupLoginPage(props) {
<Login <Login
teamDisplayName={props.TeamDisplayName} teamDisplayName={props.TeamDisplayName}
teamName={props.TeamName} teamName={props.TeamName}
authServices={props.AuthServices}
/>, />,
document.getElementById('login') document.getElementById('login')
); );

Просмотреть файл

@@ -3,11 +3,9 @@
var SignupTeam = require('../components/signup_team.jsx'); var SignupTeam = require('../components/signup_team.jsx');
function setupSignupTeamPage(props) { function setupSignupTeamPage() {
var services = JSON.parse(props.AuthServices);
React.render( React.render(
<SignupTeam services={services} />, <SignupTeam />,
document.getElementById('signup-team') document.getElementById('signup-team')
); );
} }

Просмотреть файл

@@ -12,7 +12,6 @@ function setupSignupUserCompletePage(props) {
email={props.Email} email={props.Email}
hash={props.Hash} hash={props.Hash}
data={props.Data} data={props.Data}
authServices={props.AuthServices}
/>, />,
document.getElementById('signup-user-complete') document.getElementById('signup-user-complete')
); );

Просмотреть файл

@@ -4,12 +4,10 @@ var BrowserStore = require('../stores/browser_store.jsx');
var TeamStore = require('../stores/team_store.jsx'); var TeamStore = require('../stores/team_store.jsx');
export function track(category, action, label, prop, val) { export function track(category, action, label, prop, val) {
global.window.snowplow('trackStructEvent', category, action, label, prop, val);
global.window.analytics.track(action, {category: category, label: label, property: prop, value: val}); global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
} }
export function trackPage() { export function trackPage() {
global.window.snowplow('trackPageView');
global.window.analytics.page(); global.window.analytics.page();
} }
@@ -334,6 +332,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) { export function getMeSynchronous(success, error) {
var currentUser = null; var currentUser = null;
$.ajax({ $.ajax({

Просмотреть файл

@@ -74,28 +74,5 @@
analytics.track = function(){}; analytics.track = function(){};
} }
</script> </script>
<!-- Snowplow starts plowing -->
<script type="text/javascript">
if ('{{ .Props.AnalyticsUrl }}'.trim() !== '') {
;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[];
p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments)
};p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1;
n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","//d1fc8wv8zag5ca.cloudfront.net/2.4.2/sp.js","snowplow"));
window.snowplow('newTracker', 'cf', '{{ .Props.AnalyticsUrl }}', {
appId: window.config.SiteName
});
var user = window.UserStore.getCurrentUser(true);
if (user) {
window.snowplow('setUserId', user.id);
}
window.snowplow('trackPageView');
} else {
window.snowplow = function(){};
}
</script>
<!-- Snowplow stops plowing -->
</head> </head>
{{end}} {{end}}

Просмотреть файл

@@ -27,7 +27,7 @@ type HtmlTemplatePage api.Page
func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage {
if len(title) > 0 { if len(title) > 0 {
title = utils.Cfg.ServiceSettings.SiteName + " - " + title title = utils.Cfg.TeamSettings.SiteName + " - " + title
} }
props := make(map[string]string) props := make(map[string]string)
@@ -147,7 +147,6 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.UserId) == 0 { if len(c.Session.UserId) == 0 {
page := NewHtmlTemplatePage("signup_team", "Signup") page := NewHtmlTemplatePage("signup_team", "Signup")
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} else { } else {
page := NewHtmlTemplatePage("home", "Home") page := NewHtmlTemplatePage("home", "Home")
@@ -163,7 +162,6 @@ func signup(c *api.Context, w http.ResponseWriter, r *http.Request) {
} }
page := NewHtmlTemplatePage("signup_team", "Signup") page := NewHtmlTemplatePage("signup_team", "Signup")
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -195,7 +193,6 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("login", "Login") page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = teamName page.Props["TeamName"] = teamName
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -211,7 +208,7 @@ func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
data := r.FormValue("d") data := r.FormValue("d")
hash := r.FormValue("h") 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", "") c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "")
return return
} }
@@ -260,7 +257,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
} }
} else { } 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", "") c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "")
return return
} }
@@ -281,7 +278,6 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
page.Props["TeamId"] = props["id"] page.Props["TeamId"] = props["id"]
page.Props["Data"] = data page.Props["Data"] = data
page.Props["Hash"] = hash page.Props["Hash"] = hash
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -426,7 +422,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
if len(hash) == 0 || len(data) == 0 { if len(hash) == 0 || len(data) == 0 {
isResetLink = false isResetLink = false
} else { } 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", "") c.Err = model.NewAppError("resetPassword", "The reset link does not appear to be valid", "")
return return
} }
@@ -488,7 +484,7 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
data := r.URL.Query().Get("d") data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data)) 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", "") c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "")
return return
} }

Просмотреть файл

@@ -25,7 +25,7 @@ func Setup() {
api.StartServer() api.StartServer()
api.InitApi() api.InitApi()
InitWeb() InitWeb()
URL = "http://localhost:" + utils.Cfg.ServiceSettings.Port URL = "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress
ApiClient = model.NewClient(URL) ApiClient = model.NewClient(URL)
} }
} }
@@ -195,7 +195,7 @@ func TestIncomingWebhook(t *testing.T) {
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel) channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel)
if utils.Cfg.ServiceSettings.AllowIncomingWebhooks { if utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
hook1 := &model.IncomingWebhook{ChannelId: channel1.Id} hook1 := &model.IncomingWebhook{ChannelId: channel1.Id}
hook1 = ApiClient.Must(ApiClient.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook) hook1 = ApiClient.Must(ApiClient.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook)