From ca63490aa75f5e6a72968a4cc87a916b5559eab5 Mon Sep 17 00:00:00 2001 From: it33 Date: Tue, 8 Sep 2015 00:13:11 -0700 Subject: Changed "30 person team" to "30-person team" "30-person" describes team, so it should be hyphenated. --- doc/install/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index fa54e81ef..f09bb644e 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -65,6 +65,6 @@ File usage per user varies significantly across industries. The below benchmarks - **High usage teams** - (25-100 MB/user/month) - Heaviest utlization comes from teams uploading a high number of large files into Mattermost on a regular basis. Examples include creative teams sharing and storing artwork and media with tags and commentary in a pipeline production process. -*Example:* A 30 person team with medium usage (5-25 MB/user/month) with a safety factor of 2x would require between 300 MB (30 users * 5 MB * 2x safety factor) and 1500 MB (30 users * 25 MB * 2x safety factor) of free space in the next year. +*Example:* A 30-person team with medium usage (5-25 MB/user/month) with a safety factor of 2x would require between 300 MB (30 users * 5 MB * 2x safety factor) and 1500 MB (30 users * 25 MB * 2x safety factor) of free space in the next year. It's recommended to review storage utilization at least quarterly to ensure adequate free space is available. -- cgit v1.2.3-1-g7c22 From 1626a6de6f16ba0878160b0a7eae9f49b8d34d4f Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Wed, 23 Sep 2015 12:49:28 -0700 Subject: PLT-349 adding team mgt to admin console --- api/export.go | 2 +- api/team.go | 48 +++++ api/team_test.go | 33 +++ api/user.go | 74 ++++--- api/user_test.go | 21 ++ model/client.go | 12 +- model/team.go | 20 ++ store/sql_session_store.go | 18 ++ store/sql_session_store_test.go | 23 +++ store/sql_team_store.go | 4 +- store/store.go | 3 +- .../components/admin_console/admin_controller.jsx | 84 +++++++- .../components/admin_console/admin_sidebar.jsx | 157 ++++++++++++-- .../admin_console/reset_password_modal.jsx | 136 ++++++++++++ .../components/admin_console/select_team_modal.jsx | 193 ++++++++--------- web/react/components/admin_console/team_users.jsx | 147 +++++++++++++ web/react/components/admin_console/user_item.jsx | 230 +++++++++++++++++++++ web/react/stores/admin_store.jsx | 40 ++++ web/react/utils/async_client.jsx | 26 +++ web/react/utils/client.jsx | 29 +++ web/react/utils/constants.jsx | 4 +- web/sass-files/sass/partials/_admin-console.scss | 8 +- 22 files changed, 1150 insertions(+), 162 deletions(-) create mode 100644 web/react/components/admin_console/reset_password_modal.jsx create mode 100644 web/react/components/admin_console/team_users.jsx create mode 100644 web/react/components/admin_console/user_item.jsx diff --git a/api/export.go b/api/export.go index 6d7698282..c6bc626f9 100644 --- a/api/export.go +++ b/api/export.go @@ -87,7 +87,7 @@ func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError { // Get the teams var teams []*model.Team if len(options.TeamsToExport) == 0 { - if result := <-Srv.Store.Team().GetForExport(); result.Err != nil { + if result := <-Srv.Store.Team().GetAll(); result.Err != nil { return result.Err } else { teams = result.Data.([]*model.Team) diff --git a/api/team.go b/api/team.go index c9d2412d3..d59b2b484 100644 --- a/api/team.go +++ b/api/team.go @@ -25,6 +25,7 @@ func InitTeam(r *mux.Router) { sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") + sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") @@ -302,6 +303,53 @@ func isTreamCreationAllowed(c *Context, email string) bool { return true } +func getAll(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.HasSystemAdminPermissions("getLogs") { + return + } + + if result := <-Srv.Store.Team().GetAll(); result.Err != nil { + c.Err = result.Err + return + } else { + teams := result.Data.([]*model.Team) + m := make(map[string]*model.Team) + for _, v := range teams { + m[v.Id] = v + } + + w.Write([]byte(model.TeamMapToJson(m))) + } +} + +func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + id := props["id"] + + if result := <-Srv.Store.Session().Get(id); result.Err != nil { + c.Err = result.Err + return + } else { + session := result.Data.(*model.Session) + + c.LogAudit("revoked_all=" + id) + + if session.IsOAuth { + RevokeAccessToken(session.Token) + } else { + sessionCache.Remove(session.Token) + + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(model.MapToJson(props))) + return + } + } + } +} + func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) { m := model.MapFromJson(r.Body) diff --git a/api/team_test.go b/api/team_test.go index cd39dacfe..e2a7cf430 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -132,6 +132,39 @@ func TestFindTeamByEmail(t *testing.T) { } } +func TestGetAllTeams(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + if _, err := Client.GetAllTeams(); err == nil { + t.Fatal("you shouldn't have permissions") + } + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + if r1, err := Client.GetAllTeams(); err != nil { + t.Fatal(err) + } else { + teams := r1.Data.(map[string]*model.Team) + if teams[team.Id].Name != team.Name { + t.Fatal() + } + } +} + /* XXXXXX investigate and fix failing test diff --git a/api/user.go b/api/user.go index d61afb027..348475e38 100644 --- a/api/user.go +++ b/api/user.go @@ -51,6 +51,7 @@ func InitUser(r *mux.Router) { sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") + sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET") @@ -553,13 +554,26 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { } func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, ok := params["id"] + if ok { + // You must be system admin to access another team + if id != c.Session.TeamId { + if !c.HasSystemAdminPermissions("getProfiles") { + return + } + } - etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string) + } else { + id = c.Session.TeamId + } + + etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string) if HandleEtag(etag, w, r) { return } - if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil { c.Err = result.Err return } else { @@ -1158,29 +1172,35 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { return } - hash := props["hash"] - if len(hash) == 0 { - c.SetInvalidParam("resetPassword", "hash") + name := props["name"] + if len(name) == 0 { + c.SetInvalidParam("resetPassword", "name") return } - data := model.MapFromJson(strings.NewReader(props["data"])) + userId := props["user_id"] + hash := props["hash"] + timeStr := "" - userId := data["user_id"] - if len(userId) != 26 { - c.SetInvalidParam("resetPassword", "data:user_id") - return - } + if !c.IsSystemAdmin() { + if len(hash) == 0 { + c.SetInvalidParam("resetPassword", "hash") + return + } - timeStr := data["time"] - if len(timeStr) == 0 { - c.SetInvalidParam("resetPassword", "data:time") - return + data := model.MapFromJson(strings.NewReader(props["data"])) + + userId = data["user_id"] + + timeStr = data["time"] + if len(timeStr) == 0 { + c.SetInvalidParam("resetPassword", "data:time") + return + } } - name := props["name"] - if len(name) == 0 { - c.SetInvalidParam("resetPassword", "name") + if len(userId) != 26 { + c.SetInvalidParam("resetPassword", "user_id") return } @@ -1208,15 +1228,17 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "") - return - } + if !c.IsSystemAdmin() { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) { + c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "") + return + } - t, err := strconv.ParseInt(timeStr, 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewAppError("resetPassword", "The reset link has expired", "") - return + t, err := strconv.ParseInt(timeStr, 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour + c.Err = model.NewAppError("resetPassword", "The reset link has expired", "") + return + } } if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { diff --git a/api/user_test.go b/api/user_test.go index 34eefce59..c2dca752c 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -228,6 +228,13 @@ func TestGetUser(t *testing.T) { ruser2, _ := Client.CreateUser(&user2, "") store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id)) + team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam2, _ := Client.CreateTeam(&team2) + + user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser3, _ := Client.CreateUser(&user3, "") + store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id)) + Client.LoginByEmail(team.Name, user.Email, user.Password) rId := ruser.Data.(*model.User).Id @@ -276,13 +283,27 @@ func TestGetUser(t *testing.T) { t.Log(cache_result.Data) t.Fatal("cache should be empty") } + } + if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil { + t.Fatal("shouldn't have access") } Client.AuthToken = "" if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil { t.Fatal("shouldn't have accss") } + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil { + t.Fatal(err) + } } func TestGetAudits(t *testing.T) { diff --git a/model/client.go b/model/client.go index cc75ce370..ca17da6d2 100644 --- a/model/client.go +++ b/model/client.go @@ -150,6 +150,16 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { } } +func (c *Client) GetAllTeams() (*Result, *AppError) { + if r, err := c.DoApiGet("/teams/all", "", ""); err != nil { + return nil, err + } else { + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil + } +} + func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) { m := make(map[string]string) m["name"] = name @@ -254,7 +264,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) { } func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil { + if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), diff --git a/model/team.go b/model/team.go index 0d740dde2..f80fa3b11 100644 --- a/model/team.go +++ b/model/team.go @@ -73,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team { } } +func TeamMapToJson(u map[string]*Team) string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +func TeamMapFromJson(data io.Reader) map[string]*Team { + decoder := json.NewDecoder(data) + var teams map[string]*Team + err := decoder.Decode(&teams) + if err == nil { + return teams + } else { + return nil + } +} + func (o *Team) Etag() string { return Etag(o.Id, o.UpdateAt) } diff --git a/store/sql_session_store.go b/store/sql_session_store.go index c1d2c852b..22411389d 100644 --- a/store/sql_session_store.go +++ b/store/sql_session_store.go @@ -140,6 +140,24 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel { return storeChannel } +func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}) + if err != nil { + result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForTeam", "We couldn't remove all the sessions for the team", "id="+teamId+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go index 4ae680556..3d8aafe25 100644 --- a/store/sql_session_store_test.go +++ b/store/sql_session_store_test.go @@ -80,6 +80,29 @@ func TestSessionRemove(t *testing.T) { } } +func TestSessionRemoveAll(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + Must(store.Session().Save(&s1)) + + if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { + t.Fatal(rs1.Err) + } else { + if rs1.Data.(*model.Session).Id != s1.Id { + t.Fatal("should match") + } + } + + Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId)) + + if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil { + t.Fatal("should have been removed") + } +} + func TestSessionRemoveToken(t *testing.T) { Setup() diff --git a/store/sql_team_store.go b/store/sql_team_store.go index 3d644e577..109fe5401 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -196,7 +196,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel { return storeChannel } -func (s SqlTeamStore) GetForExport() StoreChannel { +func (s SqlTeamStore) GetAll() StoreChannel { storeChannel := make(StoreChannel) go func() { @@ -204,7 +204,7 @@ func (s SqlTeamStore) GetForExport() StoreChannel { var data []*model.Team if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error()) + result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error()) } result.Data = data diff --git a/store/store.go b/store/store.go index c9d40cfa5..7ba3e1d99 100644 --- a/store/store.go +++ b/store/store.go @@ -47,7 +47,7 @@ type TeamStore interface { Get(id string) StoreChannel GetByName(name string) StoreChannel GetTeamsForEmail(domain string) StoreChannel - GetForExport() StoreChannel + GetAll() StoreChannel } type ChannelStore interface { @@ -110,6 +110,7 @@ type SessionStore interface { Get(sessionIdOrToken string) StoreChannel GetSessions(userId string) StoreChannel Remove(sessionIdOrToken string) StoreChannel + RemoveAllSessionsForTeam(teamId string) StoreChannel UpdateLastActivityAt(sessionId string, time int64) StoreChannel UpdateRoles(userId string, roles string) StoreChannel } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index ce7d61ca9..25476251f 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -3,6 +3,7 @@ var AdminSidebar = require('./admin_sidebar.jsx'); var AdminStore = require('../../stores/admin_store.jsx'); +var TeamStore = require('../../stores/team_store.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var LoadingScreen = require('../loading_screen.jsx'); @@ -16,38 +17,104 @@ var GitLabSettingsTab = require('./gitlab_settings.jsx'); var SqlSettingsTab = require('./sql_settings.jsx'); var TeamSettingsTab = require('./team_settings.jsx'); var ServiceSettingsTab = require('./service_settings.jsx'); +var TeamUsersTab = require('./team_users.jsx'); export default class AdminController extends React.Component { constructor(props) { super(props); this.selectTab = this.selectTab.bind(this); + this.removeSelectedTeam = this.removeSelectedTeam.bind(this); + this.addSelectedTeam = this.addSelectedTeam.bind(this); this.onConfigListenerChange = this.onConfigListenerChange.bind(this); + this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this); + + var selectedTeams = AdminStore.getSelectedTeams(); + if (selectedTeams == null) { + selectedTeams = {}; + selectedTeams[TeamStore.getCurrentId()] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + } this.state = { - config: null, - selected: 'service_settings' + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams, + selected: 'service_settings', + selectedTeam: null }; } componentDidMount() { AdminStore.addConfigChangeListener(this.onConfigListenerChange); AsyncClient.getConfig(); + + AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); + AsyncClient.getAllTeams(); } componentWillUnmount() { AdminStore.removeConfigChangeListener(this.onConfigListenerChange); + AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange); } onConfigListenerChange() { this.setState({ config: AdminStore.getConfig(), - selected: this.state.selected + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam }); } - selectTab(tab) { - this.setState({selected: tab}); + onAllTeamsListenerChange() { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + + }); + } + + selectTab(tab, teamId) { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: tab, + selectedTeam: teamId + }); + } + + removeSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + Reflect.deleteProperty(selectedTeams, teamId); + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); + } + + addSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + selectedTeams[teamId] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); } render() { @@ -74,6 +141,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'service_settings') { tab = ; + } else if (this.state.selected === 'team_users') { + tab = ; } } @@ -85,7 +154,12 @@ export default class AdminController extends React.Component { />
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 0983c1276..b8413d6c7 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var AdminSidebarHeader = require('./admin_sidebar_header.jsx'); +var SelectTeamModal = require('./select_team_modal.jsx'); export default class AdminSidebar extends React.Component { constructor(props) { @@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component { this.isSelected = this.isSelected.bind(this); this.handleClick = this.handleClick.bind(this); + this.removeTeam = this.removeTeam.bind(this); + + this.showTeamSelect = this.showTeamSelect.bind(this); + this.teamSelectedModal = this.teamSelectedModal.bind(this); + this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this); this.state = { + showSelectModal: false }; } - handleClick(name, e) { + handleClick(name, teamId, e) { e.preventDefault(); - this.props.selectTab(name); + this.props.selectTab(name, teamId); } - isSelected(name) { + isSelected(name, teamId) { if (this.props.selected === name) { - return 'active'; + if (name === 'team_users') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + return 'active'; + } + } else { + return 'active'; + } } return ''; } + removeTeam(teamId, e) { + e.preventDefault(); + Reflect.deleteProperty(this.props.selectedTeams, teamId); + this.props.removeSelectedTeam(teamId); + + if (this.props.selected === 'team_users') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + this.props.selectTab('service_settings', null); + } + } + } + componentDidMount() { } + showTeamSelect(e) { + e.preventDefault(); + this.setState({showSelectModal: true}); + } + + teamSelectedModal(teamId) { + this.props.selectedTeams[teamId] = 'true'; + this.setState({showSelectModal: false}); + this.props.addSelectedTeam(teamId); + this.forceUpdate(); + } + + teamSelectedModalDismissed() { + this.setState({showSelectModal: false}); + } + render() { + var count = '*'; + var teams = 'Loading'; + + if (this.props.teams != null) { + count = '' + Object.keys(this.props.teams).length; + + teams = []; + for (var key in this.props.selectedTeams) { + if (this.props.selectedTeams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + + if (team != null) { + teams.push( + + ); + } + } + } + } + return (
@@ -38,11 +132,17 @@ export default class AdminSidebar extends React.Component {
+ +
); } } AdminSidebar.propTypes = { + teams: React.PropTypes.object, + selectedTeams: React.PropTypes.object, + removeSelectedTeam: React.PropTypes.func, + addSelectedTeam: React.PropTypes.func, selected: React.PropTypes.string, + selectedTeam: React.PropTypes.string, selectTab: React.PropTypes.func }; \ No newline at end of file diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx new file mode 100644 index 000000000..4aa86dfcb --- /dev/null +++ b/web/react/components/admin_console/reset_password_modal.jsx @@ -0,0 +1,136 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Modal = ReactBootstrap.Modal; + +export default class ResetPasswordModal extends React.Component { + constructor(props) { + super(props); + + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + + this.state = { + serverError: null + }; + } + + doSubmit(e) { + e.preventDefault(); + var password = React.findDOMNode(this.refs.password).value; + + if (!password || password.length < 5) { + this.state.serverError = 'Please enter at least 5 characters.'; + this.setState(this.state); + return; + } + + this.state.serverError = null; + this.setState(this.state); + + var data = {}; + data.new_password = password; + data.name = this.props.team.name; + data.user_id = this.props.user.id; + + Client.resetPassword(data, + () => { + this.props.onModalSubmit(React.findDOMNode(this.refs.password).value); + }, + (err) => { + this.state.serverError = err.message; + this.setState(this.state); + } + ); + } + + doCancel() { + this.state.serverError = null; + this.setState(this.state); + this.props.onModalDismissed(); + } + + render() { + if (this.props.user == null) { + return
; + } + + let urlClass = 'input-group input-group--limit'; + let serverError = null; + + if (this.state.serverError) { + urlClass += ' has-error'; + serverError =

{this.state.serverError}

; + } + + return ( + + + {'Reset Password'} + +
+ +
+
+
+ + {'New Password'} + + +
+ {serverError} +
+
+
+ + + + +
+
+ ); + } +} + +ResetPasswordModal.defaultProps = { + show: false +}; + +ResetPasswordModal.propTypes = { + user: React.PropTypes.object, + team: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func +}; diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx index fa30de7b2..343f65131 100644 --- a/web/react/components/admin_console/select_team_modal.jsx +++ b/web/react/components/admin_console/select_team_modal.jsx @@ -1,124 +1,99 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -export default class SelectTeam extends React.Component { +var Modal = ReactBootstrap.Modal; + +export default class SelectTeamModal extends React.Component { constructor(props) { super(props); - this.state = { - }; + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); } + doSubmit(e) { + e.preventDefault(); + this.props.onModalSubmit(React.findDOMNode(this.refs.team).value); + } + doCancel() { + this.props.onModalDismissed(); + } render() { + if (this.props.teams == null) { + return
; + } + + var options = []; + + for (var key in this.props.teams) { + if (this.props.teams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + options.push( + + ); + } + } + return ( - @@ -244,7 +244,7 @@ export default class EmailSettings extends React.Component { /> {'false'} -

{'Typically set to true in production. When true Mattermost will attempt to send email notifications. Developers may set this field to false skipping sending emails for faster development.'}

+

{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}

@@ -268,7 +268,6 @@ export default class EmailSettings extends React.Component { /> {'true'} - -

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

+

{'Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'}

@@ -296,12 +295,12 @@ export default class EmailSettings extends React.Component { className='form-control' id='feedbackName' ref='feedbackName' - placeholder='Ex: "Mattermost", "System", "John Smith"' + placeholder='Ex: "Mattermost Notification", "System", "No-Reply"' defaultValue={this.props.config.EmailSettings.FeedbackName} onChange={this.handleChange} disabled={!this.state.sendEmailNotifications} /> -

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

+

{'Display name on email account used when sending notification emails from Mattermost.'}

@@ -323,7 +322,7 @@ export default class EmailSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.sendEmailNotifications} /> -

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

+

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

@@ -479,7 +478,7 @@ export default class EmailSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.sendEmailNotifications} /> -

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

+

{'32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'}

@@ -149,7 +149,7 @@ export default class LogSettings extends React.Component { -

{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}

+

{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}

@@ -157,7 +157,7 @@ export default class LogSettings extends React.Component {
-

{'Typically set to true in production. When true log files are written to the file specified in file location field below.'}

+

{'Typically set to true in production. When true, log files are written to the log file specified in file location field below.'}

@@ -205,7 +205,7 @@ export default class LogSettings extends React.Component { -

{'This setting determines the level of detail at which log events are written to the file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}

+

{'This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}

@@ -227,7 +227,7 @@ export default class LogSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.fileEnable} /> -

{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}

+

{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to a new file, for example mattermost.1.log, mattermost.2.log, and so forth.'}

-- cgit v1.2.3-1-g7c22 From 5548bdfd36a9aedc55657a9838c8da1229beb60b Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 21:53:37 -0700 Subject: Update rate_settings.jsx --- web/react/components/admin_console/rate_settings.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx index c05bf4a82..87a93a6a1 100644 --- a/web/react/components/admin_console/rate_settings.jsx +++ b/web/react/components/admin_console/rate_settings.jsx @@ -140,7 +140,7 @@ export default class RateSettings extends React.Component { /> {'false'} -

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

+

{'When true, APIs are throttled at rates specified below.'}

@@ -228,7 +228,7 @@ export default class RateSettings extends React.Component { className='control-label col-sm-4' htmlFor='VaryByHeader' > - {'Limit By Http Header:'} + {'Limit By HTTP Header:'}
-

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

+

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

-- cgit v1.2.3-1-g7c22 From 7e94080752c135d7f8fec7b4356f23f79c98c720 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 22:20:16 -0700 Subject: Update rate_settings.jsx --- web/react/components/admin_console/rate_settings.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx index 87a93a6a1..0081daca3 100644 --- a/web/react/components/admin_console/rate_settings.jsx +++ b/web/react/components/admin_console/rate_settings.jsx @@ -184,7 +184,7 @@ export default class RateSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.EnableRateLimiter} /> -

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

+

{'Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'}

@@ -193,7 +193,7 @@ export default class RateSettings extends React.Component { className='control-label col-sm-4' htmlFor='VaryByRemoteAddr' > - {'Limit By Remote Address: '} + {'Vary By Remote Address: '}
-

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

+

{'When true, rate limit API access by IP address.'}

@@ -228,7 +228,7 @@ export default class RateSettings extends React.Component { className='control-label col-sm-4' htmlFor='VaryByHeader' > - {'Limit By HTTP Header:'} + {'Vary By HTTP Header:'}
-

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

+

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

-- cgit v1.2.3-1-g7c22 From 1b5c32f8ab8fcbf84870fef5a717b85e1a3c90e8 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 22:23:06 -0700 Subject: Update privacy_settings.jsx --- web/react/components/admin_console/privacy_settings.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx index 8ce693925..affd8ae11 100644 --- a/web/react/components/admin_console/privacy_settings.jsx +++ b/web/react/components/admin_console/privacy_settings.jsx @@ -99,7 +99,7 @@ export default class PrivacySettings extends React.Component { /> {'false'} -

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

+

{'When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'}

@@ -132,7 +132,7 @@ export default class PrivacySettings extends React.Component { /> {'false'} -

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

+

{'When false, hides full name of users from other users including team owner and team administrators.'}

-- cgit v1.2.3-1-g7c22 From 5ca63153dc1fbadebae0a727c63ec18fbacff603 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 22:32:26 -0700 Subject: Update gitlab_settings.jsx --- web/react/components/admin_console/gitlab_settings.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx index f76655b89..1e10c5592 100644 --- a/web/react/components/admin_console/gitlab_settings.jsx +++ b/web/react/components/admin_console/gitlab_settings.jsx @@ -114,7 +114,7 @@ export default class GitLabSettings extends React.Component { /> {'false'} -

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

+

{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}

@@ -136,7 +136,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'Obtain this value via the instructions above for logging into GitLab.'}

@@ -158,7 +158,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'Obtain this value via the instructions above for logging into GitLab'}

@@ -175,12 +175,12 @@ export default class GitLabSettings extends React.Component { className='form-control' id='Scope' ref='Scope' - placeholder='Ex ""' + placeholder='Not currently used by GitLab. Please leave blank' defaultValue={this.props.config.GitLabSettings.Scope} onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}

@@ -202,7 +202,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'Enter /oauth/authorize (example http://localhost:3000/oauth/authorize).'}

@@ -224,7 +224,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'Enter /oauth/token.'}

@@ -246,7 +246,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Allow} /> -

{'Need help text.'}

+

{'Enter /api/v3/user.'}

-- cgit v1.2.3-1-g7c22 From 8af7386898737770b2b845c76c2810c7ea6697ae Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 22:38:23 -0700 Subject: Update log_settings.jsx --- web/react/components/admin_console/log_settings.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx index 781a937bf..1c39c60e8 100644 --- a/web/react/components/admin_console/log_settings.jsx +++ b/web/react/components/admin_console/log_settings.jsx @@ -101,7 +101,7 @@ export default class LogSettings extends React.Component { className='control-label col-sm-4' htmlFor='consoleEnable' > - {'Log to the Console: '} + {'Log To The Console: '}
-- cgit v1.2.3-1-g7c22 From 93255ab44bb784990e1304d89591a32561696206 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 25 Sep 2015 23:15:58 -0700 Subject: Renaming "Windows 10 Dark" to "Windows Dark" --- web/react/utils/constants.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 7497d8450..75e80bc7e 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -182,7 +182,7 @@ module.exports = { buttonColor: '#FFFFFF' }, windows10: { - type: 'Windows 10 Dark', + type: 'Windows Dark', sidebarBg: '#171717', sidebarText: '#eee', sidebarUnreadText: '#fff', -- cgit v1.2.3-1-g7c22 From 65cfe9883c34249dfa17d556f3a2e3655df196b3 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sat, 26 Sep 2015 17:22:04 -0700 Subject: Fixing broken page --- web/react/components/admin_console/email_settings.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index 32c832c6a..3b5ad2a1a 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -268,6 +268,7 @@ export default class EmailSettings extends React.Component { /> {'true'} +