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 ( -