diff --git a/api/admin.go b/api/admin.go
index 6d7a9028f2..6465977550 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -7,6 +7,7 @@ import (
"bufio"
"net/http"
"os"
+ "strings"
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
@@ -20,6 +21,8 @@ func InitAdmin(r *mux.Router) {
sr := r.PathPrefix("/admin").Subrouter()
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
+ sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
+ sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
}
@@ -33,7 +36,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.LogSettings.FileEnable {
- file, err := os.Open(utils.Cfg.LogSettings.FileLocation)
+ file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
if err != nil {
c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error())
}
@@ -54,3 +57,44 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientProperties)))
}
+
+func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getConfig") {
+ return
+ }
+
+ json := utils.Cfg.ToJson()
+ cfg := model.ConfigFromJson(strings.NewReader(json))
+ json = cfg.ToJson()
+
+ w.Write([]byte(json))
+}
+
+func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getConfig") {
+ return
+ }
+
+ cfg := model.ConfigFromJson(r.Body)
+ if cfg == nil {
+ c.SetInvalidParam("saveConfig", "config")
+ return
+ }
+
+ if len(cfg.ServiceSettings.Port) == 0 {
+ c.SetInvalidParam("saveConfig", "config")
+ return
+ }
+
+ if cfg.TeamSettings.MaxUsersPerTeam == 0 {
+ c.SetInvalidParam("saveConfig", "config")
+ return
+ }
+
+ // TODO run some cleanup validators
+
+ utils.SaveConfig(utils.CfgFileName, cfg)
+ utils.LoadConfig(utils.CfgFileName)
+ json := utils.Cfg.ToJson()
+ w.Write([]byte(json))
+}
diff --git a/api/admin_test.go b/api/admin_test.go
index e67077c555..e1778b5ac2 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -8,6 +8,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
)
func TestGetLogs(t *testing.T) {
@@ -20,6 +21,12 @@ func TestGetLogs(t *testing.T) {
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.GetLogs(); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
@@ -37,8 +44,81 @@ func TestGetLogs(t *testing.T) {
func TestGetClientProperties(t *testing.T) {
Setup()
- if _, err := Client.GetClientProperties(); err != nil {
-
+ if result, err := Client.GetClientProperties(); err != nil {
t.Fatal(err)
+ } else {
+ props := result.Data.(map[string]string)
+
+ if len(props["Version"]) == 0 {
+ t.Fatal()
+ }
+ }
+}
+
+func TestGetConfig(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.GetConfig(); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if result, err := Client.GetConfig(); err != nil {
+ t.Fatal(err)
+ } else {
+ cfg := result.Data.(*model.Config)
+
+ if len(cfg.ServiceSettings.SiteName) == 0 {
+ t.Fatal()
+ }
+ }
+}
+
+func TestSaveConfig(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.SaveConfig(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 result, err := Client.SaveConfig(utils.Cfg); err != nil {
+ t.Fatal(err)
+ } else {
+ cfg := result.Data.(*model.Config)
+
+ if len(cfg.ServiceSettings.SiteName) == 0 {
+ t.Fatal()
+ }
}
}
diff --git a/config/config.json b/config/config.json
index 4c4fbb2556..38948641c2 100644
--- a/config/config.json
+++ b/config/config.json
@@ -9,13 +9,11 @@
},
"ServiceSettings": {
"SiteName": "Mattermost",
- "Mode" : "dev",
- "AllowTesting" : false,
+ "Mode": "dev",
+ "AllowTesting": false,
"UseSSL": false,
"Port": "8065",
"Version": "developer",
- "Shards": {
- },
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
@@ -26,21 +24,12 @@
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
- "SSOSettings": {
- "gitlab": {
- "Allow": false,
- "Secret" : "",
- "Id": "",
- "Scope": "",
- "AuthEndpoint": "",
- "TokenEndpoint": "",
- "UserApiEndpoint": ""
- }
- },
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
- "DataSourceReplicas": ["mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
+ "DataSourceReplicas": [
+ "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"
+ ],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
@@ -62,7 +51,7 @@
"InitialFont": "luximbi.ttf"
},
"EmailSettings": {
- "ByPassEmail" : true,
+ "ByPassEmail": true,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
@@ -95,8 +84,20 @@
"MaxUsersPerTeam": 150,
"AllowPublicLink": true,
"AllowValetDefault": false,
+ "TourLink": "",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
+ },
+ "SSOSettings": {
+ "gitlab": {
+ "Allow": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
+ }
}
-}
+}
\ No newline at end of file
diff --git a/model/client.go b/model/client.go
index 9a89e8208f..d720f30f88 100644
--- a/model/client.go
+++ b/model/client.go
@@ -385,6 +385,24 @@ func (c *Client) GetClientProperties() (*Result, *AppError) {
}
}
+func (c *Client) GetConfig() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/config", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
diff --git a/model/config.go b/model/config.go
new file mode 100644
index 0000000000..3b333dbe14
--- /dev/null
+++ b/model/config.go
@@ -0,0 +1,151 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type ServiceSettings struct {
+ SiteName string
+ Mode string
+ AllowTesting bool
+ UseSSL bool
+ Port string
+ Version string
+ InviteSalt string
+ PublicLinkSalt string
+ ResetSalt string
+ AnalyticsUrl string
+ UseLocalStorage bool
+ StorageDirectory string
+ AllowedLoginAttempts int
+ DisableEmailSignUp bool
+ EnableOAuthServiceProvider bool
+}
+
+type SSOSetting struct {
+ Allow bool
+ Secret string
+ Id string
+ Scope string
+ AuthEndpoint string
+ TokenEndpoint string
+ UserApiEndpoint string
+}
+
+type SqlSettings struct {
+ DriverName string
+ DataSource string
+ DataSourceReplicas []string
+ MaxIdleConns int
+ MaxOpenConns int
+ Trace bool
+ AtRestEncryptKey string
+}
+
+type LogSettings struct {
+ ConsoleEnable bool
+ ConsoleLevel string
+ FileEnable bool
+ FileLevel string
+ FileFormat string
+ FileLocation string
+}
+
+type AWSSettings struct {
+ S3AccessKeyId string
+ S3SecretAccessKey string
+ S3Bucket string
+ S3Region string
+}
+
+type ImageSettings struct {
+ ThumbnailWidth uint
+ ThumbnailHeight uint
+ PreviewWidth uint
+ PreviewHeight uint
+ ProfileWidth uint
+ ProfileHeight uint
+ InitialFont string
+}
+
+type EmailSettings struct {
+ ByPassEmail bool
+ SMTPUsername string
+ SMTPPassword string
+ SMTPServer string
+ UseTLS bool
+ UseStartTLS bool
+ FeedbackEmail string
+ FeedbackName string
+ ApplePushServer string
+ ApplePushCertPublic string
+ ApplePushCertPrivate string
+}
+
+type RateLimitSettings struct {
+ UseRateLimiter bool
+ PerSec int
+ MemoryStoreSize int
+ VaryByRemoteAddr bool
+ VaryByHeader string
+}
+
+type PrivacySettings struct {
+ ShowEmailAddress bool
+ ShowPhoneNumber bool
+ ShowSkypeId bool
+ ShowFullName bool
+}
+
+type ClientSettings struct {
+ SegmentDeveloperKey string
+ GoogleDeveloperKey string
+}
+
+type TeamSettings struct {
+ MaxUsersPerTeam int
+ AllowPublicLink bool
+ AllowValetDefault bool
+ TourLink string
+ DefaultThemeColor string
+ DisableTeamCreation bool
+ RestrictCreationToDomains string
+}
+
+type Config struct {
+ LogSettings LogSettings
+ ServiceSettings ServiceSettings
+ SqlSettings SqlSettings
+ AWSSettings AWSSettings
+ ImageSettings ImageSettings
+ EmailSettings EmailSettings
+ RateLimitSettings RateLimitSettings
+ PrivacySettings PrivacySettings
+ ClientSettings ClientSettings
+ TeamSettings TeamSettings
+ SSOSettings map[string]SSOSetting
+}
+
+func (o *Config) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ConfigFromJson(data io.Reader) *Config {
+ decoder := json.NewDecoder(data)
+ var o Config
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/utils/config.go b/utils/config.go
index 0eb8329d13..1836ec1faf 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -6,11 +6,14 @@ package utils
import (
"encoding/json"
"fmt"
+ "io/ioutil"
"os"
"path/filepath"
"strconv"
l4g "code.google.com/p/log4go"
+
+ "github.com/mattermost/platform/model"
)
const (
@@ -20,139 +23,9 @@ const (
LOG_ROTATE_SIZE = 10000
)
-type ServiceSettings struct {
- SiteName string
- Mode string
- AllowTesting bool
- UseSSL bool
- Port string
- Version string
- InviteSalt string
- PublicLinkSalt string
- ResetSalt string
- AnalyticsUrl string
- UseLocalStorage bool
- StorageDirectory string
- AllowedLoginAttempts int
- DisableEmailSignUp bool
- EnableOAuthServiceProvider bool
-}
-
-type SSOSetting struct {
- Allow bool
- Secret string
- Id string
- Scope string
- AuthEndpoint string
- TokenEndpoint string
- UserApiEndpoint string
-}
-
-type SqlSettings struct {
- DriverName string
- DataSource string
- DataSourceReplicas []string
- MaxIdleConns int
- MaxOpenConns int
- Trace bool
- AtRestEncryptKey string
-}
-
-type LogSettings struct {
- ConsoleEnable bool
- ConsoleLevel string
- FileEnable bool
- FileLevel string
- FileFormat string
- FileLocation string
-}
-
-type AWSSettings struct {
- S3AccessKeyId string
- S3SecretAccessKey string
- S3Bucket string
- S3Region string
-}
-
-type ImageSettings struct {
- ThumbnailWidth uint
- ThumbnailHeight uint
- PreviewWidth uint
- PreviewHeight uint
- ProfileWidth uint
- ProfileHeight uint
- InitialFont string
-}
-
-type EmailSettings struct {
- ByPassEmail bool
- SMTPUsername string
- SMTPPassword string
- SMTPServer string
- UseTLS bool
- UseStartTLS bool
- FeedbackEmail string
- FeedbackName string
- ApplePushServer string
- ApplePushCertPublic string
- ApplePushCertPrivate string
-}
-
-type RateLimitSettings struct {
- UseRateLimiter bool
- PerSec int
- MemoryStoreSize int
- VaryByRemoteAddr bool
- VaryByHeader string
-}
-
-type PrivacySettings struct {
- ShowEmailAddress bool
- ShowPhoneNumber bool
- ShowSkypeId bool
- ShowFullName bool
-}
-
-type ClientSettings struct {
- SegmentDeveloperKey string
- GoogleDeveloperKey string
-}
-
-type TeamSettings struct {
- MaxUsersPerTeam int
- AllowPublicLink bool
- AllowValetDefault bool
- TourLink string
- DefaultThemeColor string
- DisableTeamCreation bool
- RestrictCreationToDomains string
-}
-
-type Config struct {
- LogSettings LogSettings
- ServiceSettings ServiceSettings
- SqlSettings SqlSettings
- AWSSettings AWSSettings
- ImageSettings ImageSettings
- EmailSettings EmailSettings
- RateLimitSettings RateLimitSettings
- PrivacySettings PrivacySettings
- ClientSettings ClientSettings
- TeamSettings TeamSettings
- SSOSettings map[string]SSOSetting
-}
-
-func (o *Config) ToJson() string {
- b, err := json.Marshal(o)
- if err != nil {
- return ""
- } else {
- return string(b)
- }
-}
-
-var Cfg *Config = &Config{}
+var Cfg *model.Config = &model.Config{}
var CfgLastModified int64 = 0
+var CfgFileName string = ""
var ClientProperties map[string]string = map[string]string{}
var SanitizeOptions map[string]bool = map[string]bool{}
@@ -184,14 +57,14 @@ func FindDir(dir string) string {
}
func ConfigureCmdLineLog() {
- ls := LogSettings{}
+ ls := model.LogSettings{}
ls.ConsoleEnable = true
ls.ConsoleLevel = "ERROR"
ls.FileEnable = false
configureLog(&ls)
}
-func configureLog(s *LogSettings) {
+func configureLog(s *model.LogSettings) {
l4g.Close()
@@ -207,12 +80,11 @@ func configureLog(s *LogSettings) {
}
if s.FileEnable {
- if s.FileFormat == "" {
- s.FileFormat = "[%D %T] [%L] %M"
- }
- if s.FileLocation == "" {
- s.FileLocation = FindDir("logs") + "mattermost.log"
+ var fileFormat = s.FileFormat
+
+ if fileFormat == "" {
+ fileFormat = "[%D %T] [%L] %M"
}
level := l4g.DEBUG
@@ -222,14 +94,36 @@ func configureLog(s *LogSettings) {
level = l4g.ERROR
}
- flw := l4g.NewFileLogWriter(s.FileLocation, false)
- flw.SetFormat(s.FileFormat)
+ flw := l4g.NewFileLogWriter(GetLogFileLocation(s.FileLocation), false)
+ flw.SetFormat(fileFormat)
flw.SetRotate(true)
flw.SetRotateLines(LOG_ROTATE_SIZE)
l4g.AddFilter("file", level, flw)
}
}
+func GetLogFileLocation(fileLocation string) string {
+ if fileLocation == "" {
+ return FindDir("logs") + "mattermost.log"
+ } else {
+ return fileLocation
+ }
+}
+
+func SaveConfig(fileName string, config *model.Config) *model.AppError {
+ b, err := json.MarshalIndent(config, "", " ")
+ if err != nil {
+ return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
+ }
+
+ err = ioutil.WriteFile(fileName, b, 0644)
+ if err != nil {
+ return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
+ }
+
+ return nil
+}
+
// LoadConfig will try to search around for the corresponding config file.
// It will search /tmp/fileName then attempt ./config/fileName,
// then ../config/fileName and last it will look at fileName
@@ -243,7 +137,7 @@ func LoadConfig(fileName string) {
}
decoder := json.NewDecoder(file)
- config := Config{}
+ config := model.Config{}
err = decoder.Decode(&config)
if err != nil {
panic("Error decoding config file=" + fileName + ", err=" + err.Error())
@@ -253,6 +147,7 @@ func LoadConfig(fileName string) {
panic("Error getting config info file=" + fileName + ", err=" + err.Error())
} else {
CfgLastModified = info.ModTime().Unix()
+ CfgFileName = fileName
}
configureLog(&config.LogSettings)
@@ -262,7 +157,7 @@ func LoadConfig(fileName string) {
ClientProperties = getClientProperties(Cfg)
}
-func getSanitizeOptions(c *Config) map[string]bool {
+func getSanitizeOptions(c *model.Config) map[string]bool {
options := map[string]bool{}
options["fullname"] = c.PrivacySettings.ShowFullName
options["email"] = c.PrivacySettings.ShowEmailAddress
@@ -272,7 +167,7 @@ func getSanitizeOptions(c *Config) map[string]bool {
return options
}
-func getClientProperties(c *Config) map[string]string {
+func getClientProperties(c *model.Config) map[string]string {
props := make(map[string]string)
props["Version"] = c.ServiceSettings.Version
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 68984c9e0f..7593e50a48 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -1,36 +1,63 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var AdminSidebar = require('./admin_sidebar.jsx');
-var EmailTab = require('./email_settings.jsx');
-var JobsTab = require('./jobs_settings.jsx');
-var LogsTab = require('./logs.jsx');
var Navbar = require('../../components/navbar.jsx');
+var AdminSidebar = require('./admin_sidebar.jsx');
+var AdminStore = require('../../stores/admin_store.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+var EmailSettingsTab = require('./email_settings.jsx');
+var JobsSettingsTab = require('./jobs_settings.jsx');
+var LogSettingsTab = require('./log_settings.jsx');
+var LogsTab = require('./logs.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
this.state = {
+ config: null,
selected: 'email_settings'
};
}
+ componentDidMount() {
+ AdminStore.addConfigChangeListener(this.onConfigListenerChange);
+ AsyncClient.getConfig();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ }
+
+ onConfigListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ selected: this.state.selected
+ });
+ }
+
selectTab(tab) {
this.setState({selected: tab});
}
render() {
- var tab = '';
+ var tab =