diff options
47 files changed, 1094 insertions, 409 deletions
diff --git a/api/user.go b/api/user.go index aadf735d0..6803a946c 100644 --- a/api/user.go +++ b/api/user.go @@ -51,8 +51,6 @@ func InitUser(r *mux.Router) { sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") - sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") - sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") @@ -67,6 +65,11 @@ func InitUser(r *mux.Router) { sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/image", ApiUserRequired(getProfileImage)).Methods("GET") + + sr.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST") + sr.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).Methods("POST") + sr.Handle("/claim/email_to_ldap", ApiAppHandler(emailToLdap)).Methods("POST") + sr.Handle("/claim/ldap_to_email", ApiAppHandler(ldapToEmail)).Methods("POST") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -2047,30 +2050,30 @@ func IsUsernameTaken(name string, teamId string) bool { return false } -func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) { +func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) password := props["password"] if len(password) == 0 { - c.SetInvalidParam("switchToSSO", "password") + c.SetInvalidParam("emailToOAuth", "password") return } teamName := props["team_name"] if len(teamName) == 0 { - c.SetInvalidParam("switchToSSO", "team_name") + c.SetInvalidParam("emailToOAuth", "team_name") return } service := props["service"] if len(service) == 0 { - c.SetInvalidParam("switchToSSO", "service") + c.SetInvalidParam("emailToOAuth", "service") return } email := props["email"] if len(email) == 0 { - c.SetInvalidParam("switchToSSO", "email") + c.SetInvalidParam("emailToOAuth", "email") return } @@ -2162,24 +2165,24 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") } -func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { +func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) password := props["password"] if len(password) == 0 { - c.SetInvalidParam("switchToEmail", "password") + c.SetInvalidParam("oauthToEmail", "password") return } teamName := props["team_name"] if len(teamName) == 0 { - c.SetInvalidParam("switchToEmail", "team_name") + c.SetInvalidParam("oauthToEmail", "team_name") return } email := props["email"] if len(email) == 0 { - c.SetInvalidParam("switchToEmail", "email") + c.SetInvalidParam("oauthToEmail", "email") return } @@ -2205,7 +2208,7 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { if user.Id != c.Session.UserId { c.LogAudit("fail - user ids didn't match") - c.Err = model.NewLocAppError("switchToEmail", "api.user.switch_to_email.context.app_error", nil, "") + c.Err = model.NewLocAppError("oauthToEmail", "api.user.oauth_to_email.context.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden return } @@ -2230,6 +2233,176 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } +func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("emailToLdap", "email") + return + } + + emailPassword := props["email_password"] + if len(emailPassword) == 0 { + c.SetInvalidParam("emailToLdap", "email_password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("emailToLdap", "team_name") + return + } + + ldapId := props["ldap_id"] + if len(ldapId) == 0 { + c.SetInvalidParam("emailToLdap", "ldap_id") + return + } + + ldapPassword := props["ldap_password"] + if len(ldapPassword) == 0 { + c.SetInvalidParam("emailToLdap", "ldap_password") + return + } + + c.LogAudit("attempt") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.LogAudit("fail - couldn't get team") + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.LogAudit("fail - couldn't get user") + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, emailPassword) { + c.LogAuditWithUserId(user.Id, "fail - invalid email password") + return + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + ldapInterface := einterfaces.GetLdapInterface() + if ldapInterface == nil { + c.Err = model.NewLocAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if err := ldapInterface.SwitchToEmail(user.Id, ldapId, ldapPassword); err != nil { + c.LogAuditWithUserId(user.Id, "fail - ldap switch failed") + c.Err = err + return + } + + sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "LDAP") + + m := map[string]string{} + m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(m))) +} + +func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("ldapToEmail", "email") + return + } + + emailPassword := props["email_password"] + if len(emailPassword) == 0 { + c.SetInvalidParam("ldapToEmail", "email_password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("ldapToEmail", "team_name") + return + } + + ldapPassword := props["ldap_password"] + if len(ldapPassword) == 0 { + c.SetInvalidParam("ldapToEmail", "ldap_password") + return + } + + c.LogAudit("attempt") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.LogAudit("fail - couldn't get team") + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.LogAudit("fail - couldn't get user") + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if user.AuthService != model.USER_AUTH_SERVICE_LDAP { + c.Err = model.NewLocAppError("ldapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "") + return + } + + ldapInterface := einterfaces.GetLdapInterface() + if ldapInterface == nil { + c.Err = model.NewLocAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if err := ldapInterface.CheckPassword(user.AuthData, ldapPassword); err != nil { + c.LogAuditWithUserId(user.Id, "fail - ldap authentication failed") + c.Err = err + return + } + + if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil { + c.LogAudit("fail - database issue") + c.Err = result.Err + return + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) + + m := map[string]string{} + m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(m))) +} + func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { diff --git a/api/user_test.go b/api/user_test.go index a175adb1b..86cda0390 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1161,7 +1161,7 @@ func TestStatuses(t *testing.T) { } -func TestSwitchToSSO(t *testing.T) { +func TestEmailToOAuth(t *testing.T) { Setup() team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -1172,45 +1172,45 @@ func TestSwitchToSSO(t *testing.T) { store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) m := map[string]string{} - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - empty data") } m["password"] = "pwd" - _, err := Client.SwitchToSSO(m) + _, err := Client.EmailToOAuth(m) if err == nil { t.Fatal("should have failed - missing team_name, service, email") } m["team_name"] = team.Name - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - missing service, email") } m["service"] = "someservice" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - missing email") } m["team_name"] = "junk" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - bad team name") } m["team_name"] = team.Name m["email"] = "junk" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - bad email") } m["email"] = ruser.Email m["password"] = "junk" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.EmailToOAuth(m); err == nil { t.Fatal("should have failed - bad password") } } -func TestSwitchToEmail(t *testing.T) { +func TestOAuthToEmail(t *testing.T) { Setup() team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -1225,45 +1225,158 @@ func TestSwitchToEmail(t *testing.T) { store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) m := map[string]string{} - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - not logged in") } Client.LoginByEmail(team.Name, user.Email, user.Password) - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - empty data") } m["password"] = "pwd" - _, err := Client.SwitchToSSO(m) + _, err := Client.OAuthToEmail(m) if err == nil { t.Fatal("should have failed - missing team_name, service, email") } m["team_name"] = team.Name - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - missing email") } m["email"] = ruser.Email m["team_name"] = "junk" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - bad team name") } m["team_name"] = team.Name m["email"] = "junk" - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - bad email") } m["email"] = ruser2.Email - if _, err := Client.SwitchToSSO(m); err == nil { + if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - wrong user") } } +func TestLDAPToEmail(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + m := map[string]string{} + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - empty data") + } + + m["email_password"] = "pwd" + _, err := Client.LDAPToEmail(m) + if err == nil { + t.Fatal("should have failed - missing team_name, ldap_password, email") + } + + m["team_name"] = team.Name + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - missing email, ldap_password") + } + + m["ldap_password"] = "pwd" + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - missing email") + } + + m["email"] = ruser.Email + m["team_name"] = "junk" + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - bad team name") + } + + m["team_name"] = team.Name + m["email"] = "junk" + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - bad email") + } + + m["email"] = user.Email + if _, err := Client.LDAPToEmail(m); err == nil { + t.Fatal("should have failed - user is not an LDAP user") + } +} + +func TestEmailToLDAP(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + m := map[string]string{} + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - empty data") + } + + m["email_password"] = "pwd" + _, err := Client.EmailToLDAP(m) + if err == nil { + t.Fatal("should have failed - missing team_name, ldap_id, ldap_password, email") + } + + m["team_name"] = team.Name + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - missing email, ldap_password, ldap_id") + } + + m["ldap_id"] = "someid" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - missing email, ldap_password") + } + + m["ldap_password"] = "pwd" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - missing email") + } + + m["email"] = ruser.Email + m["team_name"] = "junk" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - bad team name") + } + + m["team_name"] = team.Name + m["email"] = "junk" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - bad email") + } + + m["email"] = user.Email + m["email_password"] = "junk" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - bad password") + } + + m["email_password"] = "pwd" + if _, err := Client.EmailToLDAP(m); err == nil { + t.Fatal("should have failed - missing ldap bits or user") + } +} + func TestMeLoggedIn(t *testing.T) { Setup() diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 1c5938322..2977ab812 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -11,6 +11,7 @@ type LdapInterface interface { DoLogin(team *model.Team, id string, password string) (*model.User, *model.AppError) GetUser(id string) (*model.User, *model.AppError) CheckPassword(id string, password string) *model.AppError + SwitchToEmail(userId, ldapId, ldapPassword string) *model.AppError } var theLdapInterface LdapInterface diff --git a/i18n/en.json b/i18n/en.json index 3180e8dbc..6811d86e3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1540,10 +1540,22 @@ "translation": "Failed to send welcome email successfully err=%v" }, { - "id": "api.user.switch_to_email.context.app_error", + "id": "api.user.oauth_to_email.context.app_error", "translation": "Update password failed because context user_id did not match provided user's id" }, { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "LDAP not available on this server" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "This user account does use LDAP" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "LDAP not available on this server" + }, + { "id": "api.user.update_active.permissions.app_error", "translation": "You do not have the appropriate permissions" }, diff --git a/i18n/es.json b/i18n/es.json index 97471ee98..2c062dd00 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -1540,7 +1540,7 @@ "translation": "Falla al enviar el correo de bienvenida err=%v" }, { - "id": "api.user.switch_to_email.context.app_error", + "id": "api.user.oauth_to_email.context.app_error", "translation": "La actualización de la contraseña falló porque el user_id del contexto no coincide con el id de usuario provisto" }, { diff --git a/i18n/pt.json b/i18n/pt.json index 15bc42835..6bcf9e072 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -1540,7 +1540,7 @@ "translation": "Falha ao enviar com sucesso boas vindas por email err=%v" }, { - "id": "api.user.switch_to_email.context.app_error", + "id": "api.user.oauth_to_email.context.app_error", "translation": "Atualização de senha falhou devido ao contexto user_id não combinar com id de usuário fornecido" }, { diff --git a/model/client.go b/model/client.go index f5c8ad641..ee26ae64e 100644 --- a/model/client.go +++ b/model/client.go @@ -332,8 +332,8 @@ func (c *Client) GetSessions(id string) (*Result, *AppError) { } } -func (c *Client) SwitchToSSO(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/switch_to_sso", MapToJson(m)); err != nil { +func (c *Client) EmailToOAuth(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/claim/email_to_sso", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -341,8 +341,26 @@ func (c *Client) SwitchToSSO(m map[string]string) (*Result, *AppError) { } } -func (c *Client) SwitchToEmail(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/switch_to_email", MapToJson(m)); err != nil { +func (c *Client) OAuthToEmail(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/claim/oauth_to_email", MapToJson(m)); 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) LDAPToEmail(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/claim/ldap_to_email", MapToJson(m)); 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) EmailToLDAP(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/claim/ldap_to_email", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), diff --git a/model/ldap.go b/model/ldap.go new file mode 100644 index 000000000..5fde06a6e --- /dev/null +++ b/model/ldap.go @@ -0,0 +1,8 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +const ( + USER_AUTH_SERVICE_LDAP = "ldap" +) diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx index 94a10c97f..9c49c3879 100644 --- a/webapp/components/access_history_modal.jsx +++ b/webapp/components/access_history_modal.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import {Modal} from 'react-bootstrap'; import LoadingScreen from './loading_screen.jsx'; import AuditTable from './audit_table.jsx'; @@ -36,11 +35,8 @@ class AccessHistoryModal extends React.Component { } onShow() { AsyncClient.getAudits(); - - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); } } onHide() { diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx index 9a4ff3ef2..f1dd4a26a 100644 --- a/webapp/components/activity_log_modal.jsx +++ b/webapp/components/activity_log_modal.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import UserStore from 'stores/user_store.jsx'; import * as Client from 'utils/client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -56,11 +55,8 @@ export default class ActivityLogModal extends React.Component { } onShow() { AsyncClient.getSessions(); - - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); } } onHide() { diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 7cd713942..369fa2dbb 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -80,6 +80,7 @@ export default class ChannelHeader extends React.Component { SearchStore.addSearchChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); + $('.sidebar--left .dropdown-menu').perfectScrollbar(); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); diff --git a/webapp/components/claim/claim_account.jsx b/webapp/components/claim/claim.jsx index b6495e283..464187c37 100644 --- a/webapp/components/claim/claim_account.jsx +++ b/webapp/components/claim/claim.jsx @@ -1,16 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import EmailToSSO from './email_to_sso.jsx'; -import SSOToEmail from './sso_to_email.jsx'; import TeamStore from 'stores/team_store.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import React from 'react'; import logoImage from 'images/logo.png'; -export default class ClaimAccount extends React.Component { +export default class Claim extends React.Component { constructor(props) { super(props); @@ -39,7 +37,7 @@ export default class ClaimAccount extends React.Component { const team = TeamStore.getByName(this.state.teamName); let displayName = ''; if (team) { - displayName = team.displayName; + displayName = team.display_name; } this.setState({ teamDisplayName: displayName @@ -49,39 +47,6 @@ export default class ClaimAccount extends React.Component { this.updateStateFromStores(); } render() { - if (this.state.teamDisplayName === '') { - return (<div/>); - } - let content; - if (this.state.email === '') { - content = ( - <p> - <FormattedMessage - id='claim.account.noEmail' - defaultMessage='No email specified' - /> - </p> - ); - } else if (this.state.oldType === '' && this.state.newType !== '') { - content = ( - <EmailToSSO - email={this.state.email} - type={this.state.newType} - teamName={this.state.teamName} - teamDisplayName={this.state.teamDisplayName} - /> - ); - } else { - content = ( - <SSOToEmail - email={this.state.email} - currentType={this.state.oldType} - teamName={this.state.teamName} - teamDisplayName={this.state.teamDisplayName} - /> - ); - } - return ( <div> <div className='signup-header'> @@ -99,7 +64,13 @@ export default class ClaimAccount extends React.Component { src={logoImage} /> <div id='claim'> - {content} + {React.cloneElement(this.props.children, { + teamName: this.state.teamName, + teamDisplayName: this.state.teamDisplayName, + currentType: this.state.oldType, + newType: this.state.newType, + email: this.state.email + })} </div> </div> </div> @@ -108,9 +79,10 @@ export default class ClaimAccount extends React.Component { } } -ClaimAccount.defaultProps = { +Claim.defaultProps = { }; -ClaimAccount.propTypes = { +Claim.propTypes = { params: React.PropTypes.object.isRequired, - location: React.PropTypes.object.isRequired + location: React.PropTypes.object.isRequired, + children: React.PropTypes.node }; diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx new file mode 100644 index 000000000..f3046fa74 --- /dev/null +++ b/webapp/components/claim/components/email_to_ldap.jsx @@ -0,0 +1,169 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; + +export default class EmailToLDAP extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + var state = {}; + + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.'); + this.setState(state); + return; + } + + const ldapId = ReactDOM.findDOMNode(this.refs.ldapid).value.trim(); + if (!ldapId) { + state.error = Utils.localizeMessage('claim.email_to_ldap.ldapIdError', 'Please enter your LDAP ID.'); + this.setState(state); + return; + } + + const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim(); + if (!ldapPassword) { + state.error = Utils.localizeMessage('claim.email_to_ldap.ldapPasswordError', 'Please enter your LDAP password.'); + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.email_password = password; + postData.ldap_id = ldapId; + postData.ldap_password = ldapPassword; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + + Client.emailToLDAP(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + var error = null; + if (this.state.error) { + error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + return ( + <div> + <h3> + <FormattedMessage + id='claim.email_to_ldap.title' + defaultMessage='Switch Email/Password Account to LDAP' + /> + </h3> + <form onSubmit={this.submit}> + <p> + <FormattedMessage + id='claim.email_to_ldap.ssoType' + defaultMessage='Upon claiming your account, you will only be able to login with LDAP' + /> + </p> + <p> + <FormattedMessage + id='claim.email_to_ldap.ssoNote' + defaultMessage='You must already have a valid LDAP account' + /> + </p> + <p> + <FormattedMessage + id='claim.email_to_ldap.enterPwd' + defaultMessage='Enter the password for your {team} {site} email account' + values={{ + team: this.props.teamDisplayName, + site: global.window.mm_config.SiteName + }} + /> + </p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder={Utils.localizeMessage('claim.email_to_ldap.pwd', 'Password')} + spellCheck='false' + /> + </div> + <p> + <FormattedMessage + id='claim.email_to_ldap.enterLdapPwd' + defaultMessage='Enter the ID and password for your LDAP account' + values={{ + team: this.props.teamDisplayName, + site: global.window.mm_config.SiteName + }} + /> + </p> + <div className={formClass}> + <input + type='text' + className='form-control' + name='ldapId' + ref='ldapid' + placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapId', 'LDAP ID')} + spellCheck='false' + /> + </div> + <div className={formClass}> + <input + type='password' + className='form-control' + name='ldapPassword' + ref='ldappassword' + placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'LDAP Password')} + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='claim.email_to_ldap.switchTo' + defaultMessage='Switch account to LDAP' + /> + </button> + </form> + </div> + ); + } +} + +EmailToLDAP.defaultProps = { +}; +EmailToLDAP.propTypes = { + email: React.PropTypes.string, + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/webapp/components/claim/email_to_sso.jsx b/webapp/components/claim/components/email_to_oauth.jsx index d09449247..f3e370a4a 100644 --- a/webapp/components/claim/email_to_sso.jsx +++ b/webapp/components/claim/components/email_to_oauth.jsx @@ -1,26 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; import * as Client from 'utils/client.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -const holders = defineMessages({ - pwdError: { - id: 'claim.email_to_sso.pwdError', - defaultMessage: 'Please enter your password.' - }, - pwd: { - id: 'claim.email_to_sso.pwd', - defaultMessage: 'Password' - } -}); - import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; -class EmailToSSO extends React.Component { +export default class EmailToOAuth extends React.Component { constructor(props) { super(props); @@ -34,7 +22,7 @@ class EmailToSSO extends React.Component { var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!password) { - state.error = this.props.intl.formatMessage(holders.pwdError); + state.error = Utils.localizeMessage('claim.email_to_oauth.pwdError', 'Please enter your password.'); this.setState(state); return; } @@ -46,9 +34,9 @@ class EmailToSSO extends React.Component { postData.password = password; postData.email = this.props.email; postData.team_name = this.props.teamName; - postData.service = this.props.type; + postData.service = this.props.newType; - Client.switchToSSO(postData, + Client.emailToOAuth(postData, (data) => { if (data.follow_link) { window.location.href = data.follow_link; @@ -70,41 +58,41 @@ class EmailToSSO extends React.Component { formClass += ' has-error'; } - const uiType = Utils.toTitleCase(this.props.type) + ' SSO'; + const uiType = Utils.toTitleCase(this.props.newType) + ' SSO'; return ( <div> <h3> <FormattedMessage - id='claim.email_to_sso.title' + id='claim.email_to_oauth.title' defaultMessage='Switch Email/Password Account to {uiType}' values={{ - uiType: uiType + uiType }} /> </h3> <form onSubmit={this.submit}> <p> <FormattedMessage - id='claim.email_to_sso.ssoType' + id='claim.email_to_oauth.ssoType' defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO' values={{ - type: Utils.toTitleCase(this.props.type) + type: Utils.toTitleCase(this.props.newType) }} /> </p> <p> <FormattedMessage - id='claim.email_to_sso.ssoNote' + id='claim.email_to_oauth.ssoNote' defaultMessage='You must already have a valid {type} account' values={{ - type: Utils.toTitleCase(this.props.type) + type: Utils.toTitleCase(this.props.newType) }} /> </p> <p> <FormattedMessage - id='claim.email_to_sso.enterPwd' + id='claim.email_to_oauth.enterPwd' defaultMessage='Enter the password for your {team} {site} account' values={{ team: this.props.teamDisplayName, @@ -118,7 +106,7 @@ class EmailToSSO extends React.Component { className='form-control' name='password' ref='password' - placeholder={this.props.intl.formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage('claim.email_to_oauth.pwd', 'Password')} spellCheck='false' /> </div> @@ -128,10 +116,10 @@ class EmailToSSO extends React.Component { className='btn btn-primary' > <FormattedMessage - id='claim.email_to_sso.switchTo' + id='claim.email_to_oauth.switchTo' defaultMessage='Switch account to {uiType}' values={{ - uiType: uiType + uiType }} /> </button> @@ -141,14 +129,11 @@ class EmailToSSO extends React.Component { } } -EmailToSSO.defaultProps = { +EmailToOAuth.defaultProps = { }; -EmailToSSO.propTypes = { - intl: intlShape.isRequired, - type: React.PropTypes.string.isRequired, - email: React.PropTypes.string.isRequired, - teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired +EmailToOAuth.propTypes = { + newType: React.PropTypes.string, + email: React.PropTypes.string, + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string }; - -export default injectIntl(EmailToSSO); diff --git a/webapp/components/claim/components/ldap_to_email.jsx b/webapp/components/claim/components/ldap_to_email.jsx new file mode 100644 index 000000000..b4ffd4944 --- /dev/null +++ b/webapp/components/claim/components/ldap_to_email.jsx @@ -0,0 +1,167 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; + +export default class LDAPToEmail extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + var state = {}; + + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = Utils.localizeMessage('claim.ldap_to_email.pwdError', 'Please enter your password.'); + this.setState(state); + return; + } + + const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim(); + if (!confirmPassword || password !== confirmPassword) { + state.error = Utils.localizeMessage('claim.ldap_to_email.pwdNotMatch', 'Passwords do not match.'); + this.setState(state); + return; + } + + const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim(); + if (!ldapPassword) { + state.error = Utils.localizeMessage('claim.ldap_to_email.ldapPasswordError', 'Please enter your LDAP password.'); + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.email_password = password; + postData.ldap_password = ldapPassword; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + + Client.ldapToEmail(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + var error = null; + if (this.state.error) { + error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + return ( + <div> + <h3> + <FormattedMessage + id='claim.ldap_to_email.title' + defaultMessage='Switch LDAP Account to Email/Password' + /> + </h3> + <form onSubmit={this.submit}> + <p> + <FormattedMessage + id='claim.ldap_to_email.ssoType' + defaultMessage='Upon claiming your account, you will only be able to login with your email and password' + /> + </p> + <p> + <FormattedMessage + id='claim.ldap_to_email.email' + defaultMessage='You will use the email {email} to login' + values={{ + email: this.props.email + }} + /> + </p> + <p> + <FormattedMessage + id='claim.ldap_to_email.enterLdapPwd' + defaultMessage='Enter your LDAP password for your {team} {site} email account' + values={{ + team: this.props.teamDisplayName, + site: global.window.mm_config.SiteName + }} + /> + </p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='ldapPassword' + ref='ldappassword' + placeholder={Utils.localizeMessage('claim.ldap_to_email.ldapPwd', 'LDAP Password')} + spellCheck='false' + /> + </div> + <p> + <FormattedMessage + id='claim.ldap_to_email.enterPwd' + defaultMessage='Enter a new password for your email account' + /> + </p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder={Utils.localizeMessage('claim.ldap_to_email.pwd', 'Password')} + spellCheck='false' + /> + </div> + <div className={formClass}> + <input + type='password' + className='form-control' + name='passwordconfirm' + ref='passwordconfirm' + placeholder={Utils.localizeMessage('claim.ldap_to_email.confirm', 'Confirm Password')} + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='claim.ldap_to_email.switchTo' + defaultMessage='Switch account to email/password' + /> + </button> + </form> + </div> + ); + } +} + +LDAPToEmail.defaultProps = { +}; +LDAPToEmail.propTypes = { + email: React.PropTypes.string, + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/webapp/components/claim/sso_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx index a41e09969..476677aeb 100644 --- a/webapp/components/claim/sso_to_email.jsx +++ b/webapp/components/claim/components/oauth_to_email.jsx @@ -1,34 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; import * as Client from 'utils/client.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -const holders = defineMessages({ - enterPwd: { - id: 'claim.sso_to_email.enterPwd', - defaultMessage: 'Please enter a password.' - }, - pwdNotMatch: { - id: 'claim.sso_to_email.pwdNotMatch', - defaultMessage: 'Password do not match.' - }, - newPwd: { - id: 'claim.sso_to_email.newPwd', - defaultMessage: 'New Password' - }, - confirm: { - id: 'claim.sso_to_email.confirm', - defaultMessage: 'Confirm Password' - } -}); - import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; -class SSOToEmail extends React.Component { +export default class OAuthToEmail extends React.Component { constructor(props) { super(props); @@ -37,20 +17,19 @@ class SSOToEmail extends React.Component { this.state = {}; } submit(e) { - const {formatMessage} = this.props.intl; e.preventDefault(); const state = {}; const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!password) { - state.error = formatMessage(holders.enterPwd); + state.error = Utils.localizeMessage('claim.oauth_to_email.enterPwd', 'Please enter a password.'); this.setState(state); return; } const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim(); if (!confirmPassword || password !== confirmPassword) { - state.error = formatMessage(holders.pwdNotMatch); + state.error = Utils.localizeMessage('claim.oauth_to_email.pwdNotMatch', 'Password do not match.'); this.setState(state); return; } @@ -63,7 +42,7 @@ class SSOToEmail extends React.Component { postData.email = this.props.email; postData.team_name = this.props.teamName; - Client.switchToEmail(postData, + Client.oauthToEmail(postData, (data) => { if (data.follow_link) { window.location.href = data.follow_link; @@ -75,7 +54,6 @@ class SSOToEmail extends React.Component { ); } render() { - const {formatMessage} = this.props.intl; var error = null; if (this.state.error) { error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; @@ -92,7 +70,7 @@ class SSOToEmail extends React.Component { <div> <h3> <FormattedMessage - id='claim.sso_to_email.title' + id='claim.oauth_to_email.title' defaultMessage='Switch {type} Account to Email' values={{ type: uiType @@ -102,13 +80,13 @@ class SSOToEmail extends React.Component { <form onSubmit={this.submit}> <p> <FormattedMessage - id='claim.sso_to_email.description' + id='claim.oauth_to_email.description' defaultMessage='Upon changing your account type, you will only be able to login with your email and password.' /> </p> <p> <FormattedMessage - id='claim.sso_to_email_newPwd' + id='claim.oauth_to_email_newPwd' defaultMessage='Enter a new password for your {team} {site} account' values={{ team: this.props.teamDisplayName, @@ -122,7 +100,7 @@ class SSOToEmail extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.newPwd)} + placeholder={Utils.localizeMessage('claim.oauth_to_email.newPwd', 'New Password')} spellCheck='false' /> </div> @@ -132,7 +110,7 @@ class SSOToEmail extends React.Component { className='form-control' name='passwordconfirm' ref='passwordconfirm' - placeholder={formatMessage(holders.confirm)} + placeholder={Utils.localizeMessage('claim.oauth_to_email.confirm', 'Confirm Password')} spellCheck='false' /> </div> @@ -142,7 +120,7 @@ class SSOToEmail extends React.Component { className='btn btn-primary' > <FormattedMessage - id='claim.sso_to_email.switchTo' + id='claim.oauth_to_email.switchTo' defaultMessage='Switch {type} to email and password' values={{ type: uiType @@ -155,14 +133,11 @@ class SSOToEmail extends React.Component { } } -SSOToEmail.defaultProps = { +OAuthToEmail.defaultProps = { }; -SSOToEmail.propTypes = { - intl: intlShape.isRequired, - currentType: React.PropTypes.string.isRequired, - email: React.PropTypes.string.isRequired, - teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string +OAuthToEmail.propTypes = { + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string, + currentType: React.PropTypes.string, + email: React.PropTypes.string }; - -export default injectIntl(SSOToEmail); diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx index 0a55b2968..caf9a0ee5 100644 --- a/webapp/components/edit_post_modal.jsx +++ b/webapp/components/edit_post_modal.jsx @@ -27,8 +27,8 @@ const holders = defineMessages({ import React from 'react'; class EditPostModal extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.handleEdit = this.handleEdit.bind(this); this.handleEditInput = this.handleEditInput.bind(this); @@ -56,12 +56,13 @@ class EditPostModal extends React.Component { updatedPost.id = this.state.post_id; updatedPost.channel_id = this.state.channel_id; - Client.updatePost(updatedPost, - function success() { + Client.updatePost( + updatedPost, + () => { AsyncClient.getPosts(updatedPost.channel_id); window.scrollTo(0, 0); }, - function error(err) { + (err) => { AsyncClient.dispatchError(err, 'updatePost'); } ); @@ -106,11 +107,11 @@ class EditPostModal extends React.Component { componentDidMount() { var self = this; - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function onHidden() { + $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => { self.setState({editText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''}); }); - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) { + $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { var button = e.relatedTarget; if (!button) { return; @@ -118,12 +119,11 @@ class EditPostModal extends React.Component { self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refocusid')}); }); - $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() { - self.refs.editbox.resize(); - $('#edit_textbox').get(0).focus(); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => { + self.refs.editbox.focus(); }); - $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() { + $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', () => { if (self.state.refocusId !== '') { setTimeout(() => { $(self.state.refocusId).get(0).focus(); @@ -221,4 +221,4 @@ EditPostModal.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(EditPostModal);
\ No newline at end of file +export default injectIntl(EditPostModal); diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index d567183ac..1f8fd6133 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -176,12 +175,6 @@ class InviteMemberModal extends React.Component { }); } - componentDidUpdate(prevProps, prevState) { - if (!prevState.show && this.state.show) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } - } - addInviteFields() { var count = this.state.idCount + 1; var inviteIds = this.state.inviteIds; diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 7ddb6b83d..c6f7b50b1 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -6,6 +6,7 @@ import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -84,7 +85,7 @@ export default class LoggedIn extends React.Component { // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { return; } @@ -94,7 +95,7 @@ export default class LoggedIn extends React.Component { if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { + if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { return; } diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index 7d048019c..819c7f590 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; + import UserStore from 'stores/user_store.jsx'; import {Popover, Overlay} from 'react-bootstrap'; import * as Utils from 'utils/utils.jsx'; @@ -20,6 +22,10 @@ export default class PopoverListMembers extends React.Component { this.closePopover = this.closePopover.bind(this); } + componentDidUpdate() { + $('.member-list__popover .popover-content').perfectScrollbar(); + } + componentWillMount() { this.setState({showPopover: false}); } diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index 2760765eb..f0324d7ce 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -46,6 +46,9 @@ export default class RhsThread extends React.Component { window.addEventListener('resize', this.handleResize); this.mounted = true; + if (!Utils.isMobile()) { + $('.sidebar--right .post-right__scroll').perfectScrollbar(); + } } componentDidUpdate() { if ($('.post-right__scroll')[0]) { diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 7619e41cd..c5baf50ef 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -61,6 +61,9 @@ export default class SearchResults extends React.Component { UserStore.addChangeListener(this.onUserChange); this.resize(); window.addEventListener('resize', this.handleResize); + if (!Utils.isMobile()) { + $('.sidebar--right .search-items-container').perfectScrollbar(); + } } shouldComponentUpdate(nextProps, nextState) { diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index d8d825fd4..0e1b7dd0e 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -163,6 +163,9 @@ export default class Sidebar extends React.Component { componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); + if (!Utils.isMobile()) { + $('.sidebar--left .nav-pills__container').perfectScrollbar(); + } } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index e3ec63194..b7f0e3a36 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -3,11 +3,14 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; + import Constants from 'utils/constants.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import SuggestionStore from 'stores/suggestion_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import TextareaAutosize from 'react-textarea-autosize'; + const KeyCodes = Constants.KeyCodes; import React from 'react'; @@ -44,7 +47,13 @@ export default class SuggestionBox extends React.Component { getTextbox() { // this is to support old code that looks at the input/textarea DOM nodes - return ReactDOM.findDOMNode(this.refs.textbox); + let textbox = this.refs.textbox; + + if (!(textbox instanceof HTMLElement)) { + textbox = ReactDOM.findDOMNode(textbox); + } + + return textbox; } handleDocumentClick(e) { @@ -132,7 +141,8 @@ export default class SuggestionBox extends React.Component { ); } else if (this.props.type === 'textarea') { textbox = ( - <textarea + <TextareaAutosize + id={this.suggestionId} ref='textbox' {...newProps} /> @@ -163,5 +173,6 @@ SuggestionBox.propTypes = { // explicitly name any input event handlers we override and need to manually call onChange: React.PropTypes.func, - onKeyDown: React.PropTypes.func + onKeyDown: React.PropTypes.func, + onHeightChange: React.PropTypes.func }; diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx index 7dbbd680a..c19787993 100644 --- a/webapp/components/team_settings_modal.jsx +++ b/webapp/components/team_settings_modal.jsx @@ -5,6 +5,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import SettingsSidebar from './settings_sidebar.jsx'; import TeamSettings from './team_settings.jsx'; +import * as Utils from 'utils/utils.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -49,9 +50,16 @@ class TeamSettingsModal extends React.Component { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); + + if (!Utils.isMobile()) { + $('.settings-modal .settings-content').perfectScrollbar(); + } } updateTab(tab) { this.setState({activeTab: tab, activeSection: ''}); + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + } } updateSection(section) { this.setState({activeSection: section}); diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index 1a395072e..371c581e5 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import AtMentionProvider from './suggestion/at_mention_provider.jsx'; import CommandProvider from './suggestion/command_provider.jsx'; import EmoticonProvider from './suggestion/emoticon_provider.jsx'; @@ -29,7 +28,7 @@ export default class Textbox extends React.Component { this.onRecievedError = this.onRecievedError.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); - this.resize = this.resize.bind(this); + this.handleHeightChange = this.handleHeightChange.bind(this); this.showPreview = this.showPreview.bind(this); this.state = { @@ -54,8 +53,6 @@ export default class Textbox extends React.Component { componentDidMount() { ErrorStore.addChangeListener(this.onRecievedError); - - this.resize(); } componentWillUnmount() { @@ -72,10 +69,6 @@ export default class Textbox extends React.Component { } } - componentDidUpdate() { - this.resize(); - } - handleKeyPress(e) { this.props.onKeyPress(e); } @@ -86,50 +79,28 @@ export default class Textbox extends React.Component { } } - focus() { - this.refs.message.getTextbox().focus(); - } + handleHeightChange(height) { + const textbox = $(this.refs.message.getTextbox()); + const wrapper = $(this.refs.wrapper); - resize() { - const textbox = this.refs.message.getTextbox(); - const $textbox = $(textbox); - const $wrapper = $(ReactDOM.findDOMNode(this.refs.wrapper)); + const maxHeight = parseInt(textbox.css('max-height'), 10); - const padding = parseInt($textbox.css('padding-bottom'), 10) + parseInt($textbox.css('padding-top'), 10); - const borders = parseInt($textbox.css('border-bottom-width'), 10) + parseInt($textbox.css('border-top-width'), 10); - const maxHeight = parseInt($textbox.css('max-height'), 10) - borders; - - // set the height to auto and remove the scrollbar so we can get the actual size of the contents - $textbox.css('height', 'auto').css('overflow-y', 'hidden'); - - let height = textbox.scrollHeight - padding; - - if (height + padding > maxHeight) { - height = maxHeight - padding; - - // turn scrollbar on and move over attachment icon to compensate for that - $textbox.css('overflow-y', 'scroll'); - $wrapper.closest('.post-body__cell').addClass('scroll'); + // move over attachment icon to compensate for the scrollbar + if (height > maxHeight) { + wrapper.closest('.post-body__cell').addClass('scroll'); } else { - $wrapper.closest('.post-body__cell').removeClass('scroll'); + wrapper.closest('.post-body__cell').removeClass('scroll'); } + } - // set the textarea to be the proper height - $textbox.height(height); - - // set the wrapper height to match the height of the textbox including padding and borders - $wrapper.height(height + padding + borders); - - if (this.state.preview) { - $(ReactDOM.findDOMNode(this.refs.preview)).height(height + borders); - } + focus() { + this.refs.message.getTextbox().focus(); } showPreview(e) { e.preventDefault(); e.target.blur(); this.setState({preview: !this.state.preview}); - this.resize(); } render() { @@ -157,7 +128,7 @@ export default class Textbox extends React.Component { ); } - let helpText = ( + const helpText = ( <div style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.5' : '0'}} className='help_format_text' @@ -174,12 +145,16 @@ export default class Textbox extends React.Component { defaultMessage='_italic_' /> </i> - <span>~~<strike> - <FormattedMessage - id='textbox.strike' - defaultMessage='strike' - /> - </strike>~~ </span> + <span> + {'~~'} + <strike> + <FormattedMessage + id='textbox.strike' + defaultMessage='strike' + /> + </strike> + {'~~ '} + </span> <code> <FormattedMessage id='textbox.inlinecode' @@ -214,16 +189,17 @@ export default class Textbox extends React.Component { spellCheck='true' autoComplete='off' autoCorrect='off' - rows='1' maxLength={Constants.MAX_POST_LEN} placeholder={this.props.createMessage} value={this.props.messageText} onUserInput={this.props.onUserInput} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} + onHeightChange={this.handleHeightChange} style={{visibility: this.state.preview ? 'hidden' : 'visible'}} listComponent={SuggestionList} providers={this.suggestionProviders} + channelId={this.props.channelId} /> <div ref='preview' diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx index b5d42decc..3652723be 100644 --- a/webapp/components/user_list.jsx +++ b/webapp/components/user_list.jsx @@ -24,11 +24,16 @@ export default class UserList extends React.Component { }); } else { content = ( - <div key='no-users-found'> - <FormattedMessage - id='user_list.notFound' - defaultMessage='No users found :(' - /> + <div + key='no-users-found' + className='no-channel-message' + > + <p className='primary-message'> + <FormattedMessage + id='user_list.notFound' + defaultMessage='No users found :(' + /> + </p> </div> ); } diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx index bd1df6ea5..d1c1f0fe2 100644 --- a/webapp/components/user_settings/user_settings_modal.jsx +++ b/webapp/components/user_settings/user_settings_modal.jsx @@ -64,7 +64,6 @@ class UserSettingsModal extends React.Component { constructor(props) { super(props); - this.handleShow = this.handleShow.bind(this); this.handleHide = this.handleHide.bind(this); this.handleHidden = this.handleHidden.bind(this); this.handleCollapse = this.handleCollapse.bind(this); @@ -95,24 +94,13 @@ class UserSettingsModal extends React.Component { } componentDidMount() { - if (this.props.show) { - this.handleShow(); - } UserStore.addChangeListener(this.onUserChanged); } - componentDidUpdate(prevProps) { - if (this.props.show && !prevProps.show) { - this.handleShow(); - } + componentDidUpdate() { UserStore.removeChangeListener(this.onUserChanged); - } - - handleShow() { - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').perfectScrollbar(); } } @@ -222,6 +210,10 @@ class UserSettingsModal extends React.Component { active_section: '' }); } + + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + } } updateSection(section, skipConfirm) { diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index e42de91ea..283d2c425 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -15,6 +15,7 @@ import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; +import {Link} from 'react-router'; const holders = defineMessages({ currentPasswordError: { @@ -268,17 +269,24 @@ class SecurityTab extends React.Component { let emailOption; if (global.window.mm_config.EnableSignUpWithEmail === 'true' && user.auth_service !== '') { + let link; + if (user.auth_service === Constants.LDAP_SERVICE) { + link = '/' + teamName + '/claim/ldap_to_email?email=' + encodeURIComponent(user.email); + } else { + link = '/' + teamName + '/claim/oauth_to_email?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service; + } + emailOption = ( <div> - <a + <Link className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service} + to={link} > <FormattedMessage id='user.settings.security.switchEmail' defaultMessage='Switch to using email and password' /> - </a> + </Link> <br/> </div> ); @@ -288,15 +296,15 @@ class SecurityTab extends React.Component { if (global.window.mm_config.EnableSignUpWithGitLab === 'true' && user.auth_service === '') { gitlabOption = ( <div> - <a + <Link className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GITLAB_SERVICE} + to={'/' + teamName + '/claim/email_to_oauth?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GITLAB_SERVICE} > <FormattedMessage id='user.settings.security.switchGitlab' defaultMessage='Switch to using GitLab SSO' /> - </a> + </Link> <br/> </div> ); @@ -306,15 +314,33 @@ class SecurityTab extends React.Component { if (global.window.mm_config.EnableSignUpWithGoogle === 'true' && user.auth_service === '') { googleOption = ( <div> - <a + <Link className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GOOGLE_SERVICE} + to={'/' + teamName + '/claim/email_to_oauth?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GOOGLE_SERVICE} > <FormattedMessage id='user.settings.security.switchGoogle' defaultMessage='Switch to using Google SSO' /> - </a> + </Link> + <br/> + </div> + ); + } + + let ldapOption; + if (global.window.mm_config.EnableLdap === 'true' && user.auth_service === '') { + ldapOption = ( + <div> + <Link + className='btn btn-primary' + to={'/' + teamName + '/claim/email_to_ldap?email=' + encodeURIComponent(user.email)} + > + <FormattedMessage + id='user.settings.security.switchLdap' + defaultMessage='Switch to using LDAP' + /> + </Link> <br/> </div> ); @@ -325,6 +351,7 @@ class SecurityTab extends React.Component { {emailOption} {gitlabOption} <br/> + {ldapOption} {googleOption} </div> ); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index d9411df07..34e938e69 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -633,21 +633,45 @@ "choose_auth_page.ldapCreate": "Create new team with LDAP Account", "choose_auth_page.noSignup": "No sign-up methods configured, please contact your system administrator.", "claim.account.noEmail": "No email specified", - "claim.email_to_sso.enterPwd": "Enter the password for your {team} {site} account", - "claim.email_to_sso.pwd": "Password", - "claim.email_to_sso.pwdError": "Please enter your password.", - "claim.email_to_sso.ssoNote": "You must already have a valid {type} account", - "claim.email_to_sso.ssoType": "Upon claiming your account, you will only be able to login with {type} SSO", - "claim.email_to_sso.switchTo": "Switch account to {uiType}", - "claim.email_to_sso.title": "Switch Email/Password Account to {uiType}", - "claim.sso_to_email.confirm": "Confirm Password", - "claim.sso_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.", - "claim.sso_to_email.enterPwd": "Please enter a password.", - "claim.sso_to_email.newPwd": "New Password", - "claim.sso_to_email.pwdNotMatch": "Password do not match.", - "claim.sso_to_email.switchTo": "Switch {type} to email and password", - "claim.sso_to_email.title": "Switch {type} Account to Email", - "claim.sso_to_email_newPwd": "Enter a new password for your {team} {site} account", + "claim.email_to_oauth.enterPwd": "Enter the password for your {team} {site} account", + "claim.email_to_oauth.pwd": "Password", + "claim.email_to_oauth.pwdError": "Please enter your password.", + "claim.email_to_oauth.ssoNote": "You must already have a valid {type} account", + "claim.email_to_oauth.ssoType": "Upon claiming your account, you will only be able to login with {type} SSO", + "claim.email_to_oauth.switchTo": "Switch account to {uiType}", + "claim.email_to_oauth.title": "Switch Email/Password Account to {uiType}", + "claim.oauth_to_email.confirm": "Confirm Password", + "claim.oauth_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.", + "claim.oauth_to_email.enterPwd": "Please enter a password.", + "claim.oauth_to_email.newPwd": "New Password", + "claim.oauth_to_email.pwdNotMatch": "Password do not match.", + "claim.oauth_to_email.switchTo": "Switch {type} to email and password", + "claim.oauth_to_email.title": "Switch {type} Account to Email", + "claim.oauth_to_email.newPwd": "Enter a new password for your {team} {site} account", + "claim.email_to_ldap.pwdError": "Please enter your password.", + "claim.email_to_ldap.ldapIdError": "Please enter your LDAP ID.", + "claim.email_to_ldap.ldapPasswordError": "Please enter your LDAP password.", + "claim.email_to_ldap.title": "Switch Email/Password Account to LDAP", + "claim.email_to_ldap.ssoType": "Upon claiming your account, you will only be able to login with LDAP", + "claim.email_to_ldap.ssoNote": "You must already have a valid LDAP account", + "claim.email_to_ldap.enterPwd": "Enter the password for your {team} {site} email account", + "claim.email_to_ldap.pwd": "Password", + "claim.email_to_ldap.enterLdapPwd": "Enter the ID and password for your LDAP account", + "claim.email_to_ldap.ldapId": "LDAP ID", + "claim.email_to_ldap.ldapPwd": "LDAP Password", + "claim.email_to_ldap.switchTo": "Switch account to LDAP", + "claim.ldap_to_email.pwdError": "Please enter your password.", + "claim.ldap_to_email.pwdNotMatch": "Passwords do not match.", + "claim.ldap_to_email.ldapPasswordError": "Please enter your LDAP password.", + "claim.ldap_to_email.title": "Switch LDAP Account to Email/Password", + "claim.ldap_to_email.ssoType": "Upon claiming your account, you will only be able to login with your email and password", + "claim.ldap_to_email.email": "You will use the email {email} to login", + "claim.ldap_to_email.enterLdapPwd": "Enter your LDAP password for your {team} {site} email account", + "claim.ldap_to_email.ldapPwd": "LDAP Password", + "claim.ldap_to_email.enterPwd": "Enter a new password for your email account", + "claim.ldap_to_email.pwd": "Password", + "claim.ldap_to_email.confirm": "Confirm Password", + "claim.ldap_to_email.switchTo": "Switch account to email/password", "confirm_modal.cancel": "Cancel", "create_comment.addComment": "Add a comment...", "create_comment.comment": "Add Comment", @@ -1335,6 +1359,7 @@ "user.settings.security.switchEmail": "Switch to using email and password", "user.settings.security.switchGitlab": "Switch to using GitLab SSO", "user.settings.security.switchGoogle": "Switch to using Google SSO", + "user.settings.security.switchLda": "Switch to using LDAP", "user.settings.security.title": "Security Settings", "user.settings.security.viewHistory": "View Access History", "user_list.notFound": "No users found :(", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 8852dd3c6..9c1665c4e 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -633,21 +633,21 @@ "choose_auth_page.ldapCreate": "Crea un nuevo equipo con tu cuenta de LDAP", "choose_auth_page.noSignup": "No hay métodos de inicio de sesión configurad, por favor contacte al administrador de sistemasos", "claim.account.noEmail": "No se especifico un correo electrónico.", - "claim.email_to_sso.enterPwd": "Ingresa la contraseña para tu cuenta para {team} {site}", - "claim.email_to_sso.pwd": "Contraseña", - "claim.email_to_sso.pwdError": "Por favor introduce tu contraseña.", - "claim.email_to_sso.ssoNote": "Debes tener una cuenta válida con {type}", - "claim.email_to_sso.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type} SSO", - "claim.email_to_sso.switchTo": "Cambiar cuenta a {uiType}", - "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", - "claim.sso_to_email.confirm": "Confirmar Contraseña", - "claim.sso_to_email.description": "Al cambiar el tipo de cuenta, sólo podrás iniciar sesión con tu correo electrónico y contraseña.", - "claim.sso_to_email.enterPwd": "Por favor ingresa una contraseña.", - "claim.sso_to_email.newPwd": "Nueva Contraseña", - "claim.sso_to_email.pwdNotMatch": "Las contraseñas no coinciden.", - "claim.sso_to_email.switchTo": "Cambiar {type} a correo electrónico y contraseña", - "claim.sso_to_email.title": "Cambiar la cuenta de {type} a Correo Electrónico", - "claim.sso_to_email_newPwd": "Ingresa una nueva contraseña para tu cuenta de {team} {site}", + "claim.email_to_oauth.enterPwd": "Ingresa la contraseña para tu cuenta para {team} {site}", + "claim.email_to_oauth.pwd": "Contraseña", + "claim.email_to_oauth.pwdError": "Por favor introduce tu contraseña.", + "claim.email_to_oauth.ssoNote": "Debes tener una cuenta válida con {type}", + "claim.email_to_oauth.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type} SSO", + "claim.email_to_oauth.switchTo": "Cambiar cuenta a {uiType}", + "claim.email_to_oauth.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", + "claim.oauth_to_email.confirm": "Confirmar Contraseña", + "claim.oauth_to_email.description": "Al cambiar el tipo de cuenta, sólo podrás iniciar sesión con tu correo electrónico y contraseña.", + "claim.oauth_to_email.enterPwd": "Por favor ingresa una contraseña.", + "claim.oauth_to_email.newPwd": "Nueva Contraseña", + "claim.oauth_to_email.pwdNotMatch": "Las contraseñas no coinciden.", + "claim.oauth_to_email.switchTo": "Cambiar {type} a correo electrónico y contraseña", + "claim.oauth_to_email.title": "Cambiar la cuenta de {type} a Correo Electrónico", + "claim.oauth_to_email.newPwd": "Ingresa una nueva contraseña para tu cuenta de {team} {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Agregar un comentario...", "create_comment.comment": "Agregar Comentario", diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json index 41d3bbc1c..0b06b77af 100644 --- a/webapp/i18n/pt.json +++ b/webapp/i18n/pt.json @@ -639,21 +639,21 @@ "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP", "choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email específicado", - "claim.email_to_sso.enterPwd": "Entre a senha para o sua conta {team} {site}", - "claim.email_to_sso.pwd": "Senha", - "claim.email_to_sso.pwdError": "Por favor digite a sua senha.", - "claim.email_to_sso.ssoNote": "Você precisa já ter uma conta {type} válida", - "claim.email_to_sso.ssoType": "Ao retirar a sua conta, você só vai ser capaz de logar com SSO {type}", - "claim.email_to_sso.switchTo": "Trocar a conta para {uiType}", - "claim.email_to_sso.title": "Trocar E-mail/Senha da Conta para {uiType}", - "claim.sso_to_email.confirm": "Confirmar senha", - "claim.sso_to_email.description": "Após a alteração do tipo de conta, você só vai ser capaz de logar com seu e-mail e senha.", - "claim.sso_to_email.enterPwd": "Por favor entre uma senha.", - "claim.sso_to_email.newPwd": "Nova Senha", - "claim.sso_to_email.pwdNotMatch": "As senha não correspondem.", - "claim.sso_to_email.switchTo": "Trocar {type} para email e senha", - "claim.sso_to_email.title": "Trocar Conta {type} para E-mail", - "claim.sso_to_email_newPwd": "Entre a nova senha para o sua conta {team} {site}", + "claim.email_to_oauth.enterPwd": "Entre a senha para o sua conta {team} {site}", + "claim.email_to_oauth.pwd": "Senha", + "claim.email_to_oauth.pwdError": "Por favor digite a sua senha.", + "claim.email_to_oauth.ssoNote": "Você precisa já ter uma conta {type} válida", + "claim.email_to_oauth.ssoType": "Ao retirar a sua conta, você só vai ser capaz de logar com SSO {type}", + "claim.email_to_oauth.switchTo": "Trocar a conta para {uiType}", + "claim.email_to_oauth.title": "Trocar E-mail/Senha da Conta para {uiType}", + "claim.oauth_to_email.confirm": "Confirmar senha", + "claim.oauth_to_email.description": "Após a alteração do tipo de conta, você só vai ser capaz de logar com seu e-mail e senha.", + "claim.oauth_to_email.enterPwd": "Por favor entre uma senha.", + "claim.oauth_to_email.newPwd": "Nova Senha", + "claim.oauth_to_email.pwdNotMatch": "As senha não correspondem.", + "claim.oauth_to_email.switchTo": "Trocar {type} para email e senha", + "claim.oauth_to_email.title": "Trocar Conta {type} para E-mail", + "claim.oauth_to_email.newPwd": "Entre a nova senha para o sua conta {team} {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Adicionar um comentário...", "create_comment.comment": "Adicionar Comentário", diff --git a/webapp/package.json b/webapp/package.json index cdfba8ef0..6f50962a4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -24,6 +24,7 @@ "react-dom": "0.14.7", "react-intl": "2.0.0-rc-1", "react-router": "2.0.1", + "react-textarea-autosize": "3.3.0", "twemoji": "1.4.1", "velocity-animate": "1.2.3" }, @@ -53,8 +54,8 @@ "webpack": "webpack/webpack#master" }, "scripts": { - "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .", - "build": "webpack", - "run": "webpack --progress --watch" + "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .", + "build": "webpack", + "run": "webpack --progress --watch" } } diff --git a/webapp/root.jsx b/webapp/root.jsx index 6ab5e80a0..f4d9a0e47 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import $ from 'jquery'; +require('perfect-scrollbar/jquery')($); import 'bootstrap/dist/css/bootstrap.css'; import 'jasny-bootstrap/dist/css/jasny-bootstrap.css'; @@ -35,7 +36,6 @@ import SignupUserComplete from 'components/signup_user_complete.jsx'; import ShouldVerifyEmail from 'components/should_verify_email.jsx'; import DoVerifyEmail from 'components/do_verify_email.jsx'; import AdminConsole from 'components/admin_console/admin_controller.jsx'; -import ClaimAccount from 'components/claim/claim_account.jsx'; import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -46,6 +46,12 @@ import UsernamePage from 'components/signup_team_complete/components/team_signup import PasswordPage from 'components/signup_team_complete/components/team_signup_password_page.jsx'; import FinishedPage from 'components/signup_team_complete/components/team_signup_finished.jsx'; +import Claim from 'components/claim/claim.jsx'; +import EmailToOAuth from 'components/claim/components/email_to_oauth.jsx'; +import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx'; +import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx'; +import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx'; + import {addLocaleData} from 'react-intl'; import enLocaleData from 'react-intl/locale-data/en'; import esLocaleData from 'react-intl/locale-data/es'; @@ -308,10 +314,6 @@ function renderRootComponent() { component={Login} /> <Route - path='claim' - component={ClaimAccount} - /> - <Route path='reset_password' component={PasswordResetSendLink} /> @@ -319,6 +321,27 @@ function renderRootComponent() { path='reset_password_complete' component={PasswordResetForm} /> + <Route + path='claim' + component={Claim} + > + <Route + path='oauth_to_email' + component={OAuthToEmail} + /> + <Route + path='email_to_oauth' + component={EmailToOAuth} + /> + <Route + path='email_to_ldap' + component={EmailToLDAP} + /> + <Route + path='ldap_to_email' + component={LDAPToEmail} + /> + </Route> </Route> </Route> </Route> diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index 94378aabe..4e2049857 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -9,6 +9,7 @@ } .modal-body { + max-height: calc(90vh - 62px); overflow: auto; padding: 20px 15px; diff --git a/webapp/sass/components/_suggestion-list.scss b/webapp/sass/components/_suggestion-list.scss index 0100026ca..39269642d 100644 --- a/webapp/sass/components/_suggestion-list.scss +++ b/webapp/sass/components/_suggestion-list.scss @@ -8,6 +8,7 @@ .suggestion-list--top { position: absolute; + bottom: 100%; } .suggestion-list__content { diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss index 9b631d00f..e75d2556b 100644 --- a/webapp/sass/layout/_headers.scss +++ b/webapp/sass/layout/_headers.scss @@ -134,6 +134,10 @@ @include alpha-property(background, $black, .1); } + a { + text-decoration: none; + } + .user__name { color: $white; } diff --git a/webapp/sass/layout/_navigation.scss b/webapp/sass/layout/_navigation.scss index 87e4b4d27..3daf6e56b 100644 --- a/webapp/sass/layout/_navigation.scss +++ b/webapp/sass/layout/_navigation.scss @@ -89,7 +89,7 @@ &.info-popover { @include background-size(100% 100%); - background: url('../images/info__icon.png'); + background-image: url('../images/info__icon.png'); cursor: pointer; height: 19px; position: relative; diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index 9d5be239a..4170483db 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -7,9 +7,7 @@ line-height: 20px; min-height: 36px; overflow-x: hidden; - position: absolute; resize: none; - top: 0px; white-space: pre-wrap; word-wrap: break-word; diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss index 4c65d0a65..3a5e74570 100644 --- a/webapp/sass/layout/_sidebar-left.scss +++ b/webapp/sass/layout/_sidebar-left.scss @@ -1,7 +1,6 @@ @charset 'UTF-8'; .sidebar--left { - background: #fafafa; border-right: $border-gray; height: 100%; left: 0; diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index 0b47e5ab6..7e53713c0 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -10,12 +10,8 @@ width: 100%; } - .row { - margin: 0; - } - h3 { - border-bottom: 1px solid #ddd; + border-bottom: 1px solid alpha-color($black, .1); font-weight: 600; margin: 1em 0; padding-bottom: .5em; @@ -75,11 +71,18 @@ width: 17px; } + &.divider { + @include alpha-property(background, $black, .1); + } + > a { &:hover, - &.active, &:focus { - background-color: #eaeaea; + @include alpha-property(background, $black, .1); + } + + &.active { + background-color: transparent; } } diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx index 66190f6a2..bba146e38 100644 --- a/webapp/stores/browser_store.jsx +++ b/webapp/stores/browser_store.jsx @@ -8,6 +8,8 @@ function getPrefix() { return global.window.mm_current_user_id + '_'; } + console.log('BrowserStore tried to operate without user present'); //eslint-disable-line no-console + return 'unknown_'; } @@ -34,46 +36,35 @@ class BrowserStoreClass { } checkVersion() { - var currentVersion = sessionStorage.getItem('storage_version'); + var currentVersion = this.getGlobalItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { - sessionStorage.clear(); + this.clearAll(); try { - sessionStorage.setItem('storage_version', global.window.mm_config.Version); + this.setGlobalItem('storage_version', global.window.mm_config.Version); } catch (e) { // Do nothing } } } - getItem(name, defaultValue) { - var result = null; - try { - result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); - } catch (err) { - result = null; - } - - if (result === null && typeof defaultValue !== 'undefined') { - result = defaultValue; - } - - return result; + setItem(name, value) { + this.setGlobalItem(getPrefix() + name, value); } - setItem(name, value) { - sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + getItem(name, defaultValue) { + return this.getGlobalItem(getPrefix() + name, defaultValue); } removeItem(name) { - sessionStorage.removeItem(getPrefix() + name); + this.removeGlobalItem(getPrefix() + name); } setGlobalItem(name, value) { try { if (this.isLocalStorageSupported()) { - localStorage.setItem(getPrefix() + name, JSON.stringify(value)); + localStorage.setItem(name, JSON.stringify(value)); } else { - sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + sessionStorage.setItem(name, JSON.stringify(value)); } } catch (err) { console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console @@ -87,9 +78,9 @@ class BrowserStoreClass { var result = null; try { if (this.isLocalStorageSupported()) { - result = JSON.parse(localStorage.getItem(getPrefix() + name)); + result = JSON.parse(localStorage.getItem(name)); } else { - result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + result = JSON.parse(sessionStorage.getItem(name)); } } catch (err) { result = null; @@ -104,18 +95,18 @@ class BrowserStoreClass { removeGlobalItem(name) { if (this.isLocalStorageSupported()) { - localStorage.removeItem(getPrefix() + name); + localStorage.removeItem(name); } else { - sessionStorage.removeItem(getPrefix() + name); + sessionStorage.removeItem(name); } } getLastServerVersion() { - return sessionStorage.getItem('last_server_version'); + return this.getGlobalItem('last_server_version'); } setLastServerVersion(version) { - sessionStorage.setItem('last_server_version', version); + this.setGlobalItem('last_server_version', version); } signalLogout() { @@ -185,6 +176,7 @@ class BrowserStoreClass { const logoutId = sessionStorage.getItem('__logout__'); sessionStorage.clear(); + localStorage.clear(); if (logoutId) { sessionStorage.setItem('__logout__', logoutId); @@ -222,4 +214,3 @@ class BrowserStoreClass { var BrowserStore = new BrowserStoreClass(); export default BrowserStore; -window.BrowserStore = BrowserStore; diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx index 776375a82..7c695a335 100644 --- a/webapp/stores/error_store.jsx +++ b/webapp/stores/error_store.jsx @@ -35,15 +35,15 @@ class ErrorStoreClass extends EventEmitter { } getLastError() { - return BrowserStore.getItem('last_error'); + return BrowserStore.getGlobalItem('last_error'); } storeLastError(error) { - BrowserStore.setItem('last_error', error); + BrowserStore.setGlobalItem('last_error', error); } getConnectionErrorCount() { - var count = BrowserStore.getItem('last_error_conn'); + var count = BrowserStore.getGlobalItem('last_error_conn'); if (count == null) { return 0; @@ -53,12 +53,12 @@ class ErrorStoreClass extends EventEmitter { } setConnectionErrorCount(count) { - BrowserStore.setItem('last_error_conn', count); + BrowserStore.setGlobalItem('last_error_conn', count); } clearLastError() { - BrowserStore.removeItem('last_error'); - BrowserStore.removeItem('last_error_conn'); + BrowserStore.removeGlobalItem('last_error'); + BrowserStore.removeGlobalItem('last_error_conn'); } } diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 98cc2f3f1..4213e6e8c 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -16,7 +16,10 @@ const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); - this.profileCache = null; + this.profiles = {}; + this.statuses = {}; + this.sessions = {}; + this.audits = {}; this.currentUserId = ''; } @@ -135,11 +138,7 @@ class UserStoreClass extends EventEmitter { } getProfiles() { - if (this.profileCache !== null) { - return this.profileCache; - } - - return BrowserStore.getItem('profiles', {}); + return this.profiles; } getActiveOnlyProfiles(skipCurrent) { @@ -171,47 +170,38 @@ class UserStoreClass extends EventEmitter { } saveProfile(profile) { - var ps = this.getProfiles(); - ps[profile.id] = profile; - this.profileCache = ps; - BrowserStore.setItem('profiles', ps); + this.profiles[profile.id] = profile; } saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (this.profileCache) { - const currentUser = this.profileCache[currentId]; - if (currentUser) { - if (currentId in profiles) { - delete profiles[currentId]; - } - - this.profileCache = profiles; - this.profileCache[currentId] = currentUser; - } else { - this.profileCache = profiles; + const currentUser = this.profiles[currentId]; + if (currentUser) { + if (currentId in this.profiles) { + delete this.profiles[currentId]; } + + this.profiles = profiles; + this.profiles[currentId] = currentUser; } else { - this.profileCache = profiles; + this.profiles = profiles; } - - BrowserStore.setItem('profiles', profiles); } setSessions(sessions) { - BrowserStore.setItem('sessions', sessions); + this.sessions = sessions; } getSessions() { - return BrowserStore.getItem('sessions', {loading: true}); + return this.sessions; } setAudits(audits) { - BrowserStore.setItem('audits', audits); + this.audits = audits; } getAudits() { - return BrowserStore.getItem('audits', {loading: true}); + return this.audits; } getCurrentMentionKeys() { @@ -252,7 +242,7 @@ class UserStoreClass extends EventEmitter { } pSetStatuses(statuses) { - BrowserStore.setItem('statuses', statuses); + this.statuses = statuses; } setStatus(userId, status) { @@ -263,7 +253,7 @@ class UserStoreClass extends EventEmitter { } getStatuses() { - return BrowserStore.getItem('statuses', {}); + return this.statuses; } getStatus(id) { diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx index e29cf71d3..d42767d31 100644 --- a/webapp/utils/client.jsx +++ b/webapp/utils/client.jsx @@ -256,38 +256,72 @@ export function resetPassword(data, success, error) { track('api', 'api_users_reset_password'); } -export function switchToSSO(data, success, error) { +export function emailToOAuth(data, success, error) { $.ajax({ - url: '/api/v1/users/switch_to_sso', + url: '/api/v1/users/claim/email_to_oauth', dataType: 'json', contentType: 'application/json', type: 'POST', data: JSON.stringify(data), success, error: function onError(xhr, status, err) { - var e = handleError('switchToSSO', xhr, status, err); + var e = handleError('emailToOAuth', xhr, status, err); error(e); } }); - track('api', 'api_users_switch_to_sso'); + track('api', 'api_users_email_to_oauth'); } -export function switchToEmail(data, success, error) { +export function oauthToEmail(data, success, error) { $.ajax({ - url: '/api/v1/users/switch_to_email', + url: '/api/v1/users/claim/oauth_to_email', dataType: 'json', contentType: 'application/json', type: 'POST', data: JSON.stringify(data), success, error: function onError(xhr, status, err) { - var e = handleError('switchToEmail', xhr, status, err); + var e = handleError('oauthToEmail', xhr, status, err); error(e); } }); - track('api', 'api_users_switch_to_email'); + track('api', 'api_users_oauth_to_email'); +} + +export function emailToLDAP(data, success, error) { + $.ajax({ + url: '/api/v1/users/claim/email_to_ldap', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('emailToLDAP', xhr, status, err); + error(e); + } + }); + + track('api', 'api_users_email_to_ldap'); +} + +export function ldapToEmail(data, success, error) { + $.ajax({ + url: '/api/v1/users/claim/ldap_to_email', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('ldapToEmail', xhr, status, err); + error(e); + } + }); + + track('api', 'api_users_ldap_to_email'); } export function logout(success, error) { diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 859348c73..ae9fdc5fb 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -185,6 +185,7 @@ export default { OFFTOPIC_CHANNEL: 'off-topic', GITLAB_SERVICE: 'gitlab', GOOGLE_SERVICE: 'google', + LDAP_SERVICE: 'ldap', EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 5f16baf1f..1379455ca 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -687,7 +687,7 @@ export function applyTheme(theme) { } if (theme.centerChannelBg) { - changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content, .modal .modal-back', 'background:' + theme.centerChannelBg, 1); + changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1); changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1); changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); |