Merge branch 'release-3.0'
Этот коммит содержится в:
@@ -457,7 +457,8 @@ func TestAdminResetPassword(t *testing.T) {
|
||||
t.Fatal("Should have errored - password too short")
|
||||
}
|
||||
|
||||
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"}
|
||||
authData := model.NewId()
|
||||
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"}
|
||||
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
|
||||
LinkUserToTeam(user2, team)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
|
||||
|
||||
@@ -39,17 +39,17 @@ func checkUserPassword(user *model.User, password string) *model.AppError {
|
||||
}
|
||||
}
|
||||
|
||||
func checkLdapUserPasswordAndAllCriteria(ldapId, password, mfaToken string) (*model.User, *model.AppError) {
|
||||
func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) {
|
||||
ldapInterface := einterfaces.GetLdapInterface()
|
||||
|
||||
if ldapInterface == nil {
|
||||
if ldapInterface == nil || ldapId == nil {
|
||||
err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if ldapUser, err := ldapInterface.DoLogin(ldapId, password); err != nil {
|
||||
if ldapUser, err := ldapInterface.DoLogin(*ldapId, password); err != nil {
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
@@ -600,8 +600,11 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
|
||||
return
|
||||
} else {
|
||||
ssoUser := provider.GetUserFromJson(userData)
|
||||
authData = ssoUser.AuthData
|
||||
ssoEmail = ssoUser.Email
|
||||
|
||||
if ssoUser.AuthData != nil {
|
||||
authData = *ssoUser.AuthData
|
||||
}
|
||||
}
|
||||
|
||||
if len(authData) == 0 {
|
||||
@@ -628,7 +631,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil {
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
}
|
||||
|
||||
22
api/user.go
22
api/user.go
@@ -494,8 +494,11 @@ func getUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppErro
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap,
|
||||
ldapAvailable,
|
||||
); result.Err != nil {
|
||||
|
||||
); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
|
||||
// don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
|
||||
result.Err.StatusCode = http.StatusBadRequest
|
||||
return nil, result.Err
|
||||
} else if result.Err != nil {
|
||||
if !ldapAvailable {
|
||||
// failed to find user and no LDAP server to fall back on
|
||||
result.Err.StatusCode = http.StatusBadRequest
|
||||
@@ -535,7 +538,7 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if result := <-Srv.Store.User().GetByAuth(authData, service); result.Err != nil {
|
||||
if result := <-Srv.Store.User().GetByAuth(&authData, service); result.Err != nil {
|
||||
if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
|
||||
return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "")
|
||||
}
|
||||
@@ -1289,7 +1292,8 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
rusers[0].Password = ""
|
||||
rusers[0].AuthData = ""
|
||||
rusers[0].AuthData = new(string)
|
||||
*rusers[0].AuthData = ""
|
||||
w.Write([]byte(rusers[0].ToJson()))
|
||||
}
|
||||
}
|
||||
@@ -1337,7 +1341,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := result.Data.(*model.User)
|
||||
|
||||
if user.AuthData != "" {
|
||||
if user.AuthData != nil && *user.AuthData != "" {
|
||||
c.LogAudit("failed - tried to update user password who was logged in through oauth")
|
||||
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
@@ -1653,7 +1657,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if len(user.AuthData) != 0 {
|
||||
if user.AuthData != nil && len(*user.AuthData) != 0 {
|
||||
c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id)
|
||||
return
|
||||
}
|
||||
@@ -1749,7 +1753,7 @@ func ResetPassword(c *Context, userId, newPassword string) *model.AppError {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if len(user.AuthData) != 0 && !c.IsSystemAdmin() {
|
||||
if user.AuthData != nil && len(*user.AuthData) != 0 && !c.IsSystemAdmin() {
|
||||
return model.NewLocAppError("ResetPassword", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id)
|
||||
|
||||
}
|
||||
@@ -2148,13 +2152,13 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
ldapInterface := einterfaces.GetLdapInterface()
|
||||
if ldapInterface == nil {
|
||||
if ldapInterface == nil || user.AuthData == 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 {
|
||||
if err := ldapInterface.CheckPassword(*user.AuthData, ldapPassword); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail - ldap authentication failed")
|
||||
c.Err = err
|
||||
return
|
||||
|
||||
@@ -1109,7 +1109,8 @@ func TestSendPasswordReset(t *testing.T) {
|
||||
t.Fatal("Should have errored - bad email")
|
||||
}
|
||||
|
||||
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"}
|
||||
authData := model.NewId()
|
||||
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"}
|
||||
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
|
||||
LinkUserToTeam(user2, team)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
|
||||
@@ -1178,7 +1179,8 @@ func TestResetPassword(t *testing.T) {
|
||||
recovery = result.Data.(*model.PasswordRecovery)
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", "1", ""); result.Err != nil {
|
||||
authData := model.NewId()
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, ""); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1341,7 +1341,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.upgrade_30_body.info",
|
||||
"translation": "<h3 style='font-weight: normal; margin-top: 10px;'>YOUR DUPLICATE ACCOUNTS HAVE BEEN UPDATED</h3>Your Mattermost server is being upgraded to Version 3.0, which lets you use a single account across multiple teams.<br/><br/>You are receiving this email because the upgrade process has detected your account had the same email or username as other accounts on the server.<br/><br/>The following updates have been made: <br/><br/>{{if .EmailChanged }}- The duplicate email of an account on the `/{{.TeamName}}` team was changed to `{{.Email}}`. If you use email and password to login, you can use this new email address for login.<br/><br/>{{end}}{{if .UsernameChanged }}- The duplicate username of an account on the team site `/{{.TeamName}}` has been changed to `{{.Username}}` to avoid confusion with other accounts.<br/><br/>{{end}} RECOMMENDED ACTION: <br/><br/>It is recommended that you login to your teams used by your duplicate accounts and add your primary account to the team and any public channels and private groups which you wish to continue using. <br/><br/>This gives your primary account access to all public channel and private group history. You can continue to access the direct message history of your duplicate accounts by logging in with their credentials. <br/><br/>FOR MORE INFORMATION: <br/><br/>For more information on the upgrade to Mattermost 3.0 please see: <a href='http://www.mattermost.org/upgrading-to-mattermost-3-0/'>http://www.mattermost.org/upgrading-to-mattermost-3-0/</a><br/><br/>"
|
||||
"translation": "<h3 style='font-weight: normal; margin-top: 10px;'>YOUR DUPLICATE ACCOUNTS HAVE BEEN UPDATED</h3>Your Mattermost server is being upgraded to Version 3.0, which lets you use a single account across multiple teams.<br/><br/>You are receiving this email because the upgrade process has detected your account had the same email or username as other accounts on the server.<br/><br/>The following updates have been made: <br/><br/>{{if .EmailChanged }}- The duplicate email of an account on the `/{{.TeamName}}` team was changed to `{{.Email}}`. You will need to use email and password to login, you can use this new email address for login.<br/><br/>{{end}}{{if .UsernameChanged }}- The duplicate username of an account on the team site `/{{.TeamName}}` has been changed to `{{.Username}}` to avoid confusion with other accounts.<br/><br/>{{end}} RECOMMENDED ACTION: <br/><br/>It is recommended that you login to your teams used by your duplicate accounts and add your primary account to the team and any public channels and private groups which you wish to continue using. <br/><br/>This gives your primary account access to all public channel and private group history. You can continue to access the direct message history of your duplicate accounts by logging in with their credentials. <br/><br/>FOR MORE INFORMATION: <br/><br/>For more information on the upgrade to Mattermost 3.0 please see: <a href='http://www.mattermost.org/upgrading-to-mattermost-3-0/'>http://www.mattermost.org/upgrading-to-mattermost-3-0/</a><br/><br/>"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.upgrade_30_subject.info",
|
||||
|
||||
@@ -373,14 +373,15 @@ func cmdUpdateDb30() {
|
||||
|
||||
uniqueEmails := make(map[string]bool)
|
||||
uniqueUsernames := make(map[string]bool)
|
||||
primaryUsers := convertTeamTo30(team.Name, team, uniqueEmails, uniqueUsernames)
|
||||
uniqueAuths := make(map[string]bool)
|
||||
primaryUsers := convertTeamTo30(team.Name, team, uniqueEmails, uniqueUsernames, uniqueAuths)
|
||||
|
||||
l4g.Info("Upgraded %v users", len(primaryUsers))
|
||||
|
||||
for _, otherTeam := range teams {
|
||||
if otherTeam.Id != team.Id {
|
||||
l4g.Info("Upgrading team %v", otherTeam.Name)
|
||||
users := convertTeamTo30(team.Name, otherTeam, uniqueEmails, uniqueUsernames)
|
||||
users := convertTeamTo30(team.Name, otherTeam, uniqueEmails, uniqueUsernames, uniqueAuths)
|
||||
l4g.Info("Upgraded %v users", len(users))
|
||||
|
||||
}
|
||||
@@ -400,6 +401,18 @@ func cmdUpdateDb30() {
|
||||
flushLogAndExit(1)
|
||||
}
|
||||
|
||||
if _, err := store.GetMaster().Exec(`
|
||||
UPDATE Users
|
||||
SET
|
||||
AuthData = NULL
|
||||
WHERE
|
||||
AuthData = ''
|
||||
`,
|
||||
); err != nil {
|
||||
l4g.Error("Failed to update AuthData types details=%v", err)
|
||||
flushLogAndExit(1)
|
||||
}
|
||||
|
||||
extraLength := store.GetMaxLengthOfColumnIfExists("Audits", "ExtraInfo")
|
||||
if len(extraLength) > 0 && extraLength != "1024" {
|
||||
store.AlterColumnTypeIfExists("Audits", "ExtraInfo", "VARCHAR(1024)", "VARCHAR(1024)")
|
||||
@@ -424,6 +437,7 @@ func cmdUpdateDb30() {
|
||||
store.RemoveIndexIfExists("idx_users_team_id", "Users")
|
||||
store.CreateUniqueIndexIfNotExists("idx_users_email_unique", "Users", "Email")
|
||||
store.CreateUniqueIndexIfNotExists("idx_users_username_unique", "Users", "Username")
|
||||
store.CreateUniqueIndexIfNotExists("idx_users_authdata_unique", "Users", "AuthData")
|
||||
store.RemoveColumnIfExists("Teams", "AllowTeamListing")
|
||||
store.RemoveColumnIfExists("Users", "TeamId")
|
||||
}
|
||||
@@ -448,12 +462,13 @@ type UserForUpgrade struct {
|
||||
Email string
|
||||
Roles string
|
||||
TeamId string
|
||||
AuthData *string
|
||||
}
|
||||
|
||||
func convertTeamTo30(primaryTeamName string, team *TeamForUpgrade, uniqueEmails map[string]bool, uniqueUsernames map[string]bool) []*UserForUpgrade {
|
||||
func convertTeamTo30(primaryTeamName string, team *TeamForUpgrade, uniqueEmails map[string]bool, uniqueUsernames map[string]bool, uniqueAuths map[string]bool) []*UserForUpgrade {
|
||||
store := api.Srv.Store.(*store.SqlStore)
|
||||
var users []*UserForUpgrade
|
||||
if _, err := store.GetMaster().Select(&users, "SELECT Users.Id, Users.Username, Users.Email, Users.Roles, Users.TeamId FROM Users WHERE Users.TeamId = :TeamId", map[string]interface{}{"TeamId": team.Id}); err != nil {
|
||||
if _, err := store.GetMaster().Select(&users, "SELECT Users.Id, Users.Username, Users.Email, Users.Roles, Users.TeamId, Users.AuthData FROM Users WHERE Users.TeamId = :TeamId", map[string]interface{}{"TeamId": team.Id}); err != nil {
|
||||
l4g.Error("Failed to load profiles for team details=%v", err)
|
||||
flushLogAndExit(1)
|
||||
}
|
||||
@@ -530,13 +545,19 @@ func convertTeamTo30(primaryTeamName string, team *TeamForUpgrade, uniqueEmails
|
||||
}
|
||||
}
|
||||
|
||||
if user.AuthData != nil && *user.AuthData != "" && uniqueAuths[*user.AuthData] {
|
||||
shouldUpdateUser = true
|
||||
}
|
||||
|
||||
if shouldUpdateUser {
|
||||
if _, err := store.GetMaster().Exec(`
|
||||
UPDATE Users
|
||||
SET
|
||||
Email = :Email,
|
||||
Username = :Username,
|
||||
Roles = :Roles
|
||||
Roles = :Roles,
|
||||
AuthService = '',
|
||||
AuthData = NULL
|
||||
WHERE
|
||||
Id = :Id
|
||||
`,
|
||||
@@ -590,6 +611,10 @@ func convertTeamTo30(primaryTeamName string, team *TeamForUpgrade, uniqueEmails
|
||||
|
||||
uniqueEmails[user.Email] = true
|
||||
uniqueUsernames[user.Username] = true
|
||||
|
||||
if user.AuthData != nil && *user.AuthData != "" {
|
||||
uniqueAuths[*user.AuthData] = true
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
|
||||
@@ -47,7 +47,7 @@ func userFromGitLabUser(glu *GitLabUser) *model.User {
|
||||
}
|
||||
strings.TrimSpace(user.Email)
|
||||
user.Email = glu.Email
|
||||
user.AuthData = strconv.FormatInt(glu.Id, 10)
|
||||
*user.AuthData = strconv.FormatInt(glu.Id, 10)
|
||||
user.AuthService = model.USER_AUTH_SERVICE_GITLAB
|
||||
|
||||
return user
|
||||
|
||||
@@ -37,7 +37,7 @@ type User struct {
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
AuthData string `json:"auth_data,omitempty"`
|
||||
AuthData *string `json:"auth_data,omitempty"`
|
||||
AuthService string `json:"auth_service"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
@@ -99,15 +99,15 @@ func (u *User) IsValid() *AppError {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.pwd.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
if len(u.AuthData) > 128 {
|
||||
if u.AuthData != nil && len(*u.AuthData) > 128 {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
if len(u.AuthData) > 0 && len(u.AuthService) == 0 {
|
||||
if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
if len(u.Password) > 0 && len(u.AuthData) > 0 {
|
||||
if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
@@ -130,6 +130,10 @@ func (u *User) PreSave() {
|
||||
u.Username = NewId()
|
||||
}
|
||||
|
||||
if u.AuthData != nil && *u.AuthData == "" {
|
||||
u.AuthData = nil
|
||||
}
|
||||
|
||||
u.Username = strings.ToLower(u.Username)
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
u.Locale = strings.ToLower(u.Locale)
|
||||
@@ -165,6 +169,10 @@ func (u *User) PreUpdate() {
|
||||
u.Locale = strings.ToLower(u.Locale)
|
||||
u.UpdateAt = GetMillis()
|
||||
|
||||
if u.AuthData != nil && *u.AuthData == "" {
|
||||
u.AuthData = nil
|
||||
}
|
||||
|
||||
if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
|
||||
u.SetDefaultNotifications()
|
||||
} else if _, ok := u.NotifyProps["mention_keys"]; ok {
|
||||
@@ -237,7 +245,8 @@ func (u *User) IsAway() bool {
|
||||
// Remove any private data from the user object
|
||||
func (u *User) Sanitize(options map[string]bool) {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
u.MfaSecret = ""
|
||||
|
||||
if len(options) != 0 && !options["email"] {
|
||||
@@ -255,7 +264,8 @@ func (u *User) Sanitize(options map[string]bool) {
|
||||
func (u *User) ClearNonProfileFields() {
|
||||
u.UpdateAt = 0
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
u.AuthService = ""
|
||||
u.MfaActive = false
|
||||
u.MfaSecret = ""
|
||||
@@ -376,7 +386,8 @@ func (u *User) IsLDAPUser() bool {
|
||||
|
||||
func (u *User) PreExport() {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
u.LastActivityAt = 0
|
||||
u.LastPingAt = 0
|
||||
u.LastPasswordUpdate = 0
|
||||
@@ -429,7 +440,7 @@ func HashPassword(password string) string {
|
||||
// ComparePassword compares the hash
|
||||
func ComparePassword(hash string, password string) bool {
|
||||
|
||||
if len(password) == 0 {
|
||||
if len(password) == 0 || len(hash) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
|
||||
table.ColMap("Id").SetMaxSize(26)
|
||||
table.ColMap("Username").SetMaxSize(64).SetUnique(true)
|
||||
table.ColMap("Password").SetMaxSize(128)
|
||||
table.ColMap("AuthData").SetMaxSize(128)
|
||||
table.ColMap("AuthData").SetMaxSize(128).SetUnique(true)
|
||||
table.ColMap("AuthService").SetMaxSize(32)
|
||||
table.ColMap("Email").SetMaxSize(128).SetUnique(true)
|
||||
table.ColMap("Nickname").SetMaxSize(64)
|
||||
@@ -265,7 +265,7 @@ func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChanne
|
||||
|
||||
updateAt := model.GetMillis()
|
||||
|
||||
if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, AuthData = '', AuthService = '', EmailVerified = true, FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil {
|
||||
if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, AuthData = NULL, AuthService = '', EmailVerified = true, FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlUserStore.UpdatePassword", "store.sql_user.update_password.app_error", nil, "id="+userId+", "+err.Error())
|
||||
} else {
|
||||
result.Data = userId
|
||||
@@ -297,7 +297,7 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int)
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (us SqlUserStore) UpdateAuthData(userId, service, authData, email string) StoreChannel {
|
||||
func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
@@ -513,7 +513,8 @@ func (us SqlUserStore) GetAllProfiles() StoreChannel {
|
||||
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
@@ -564,7 +565,8 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
|
||||
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
@@ -623,7 +625,8 @@ func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
|
||||
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
@@ -665,7 +668,8 @@ func (us SqlUserStore) GetProfileByIds(userIds []string) StoreChannel {
|
||||
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
@@ -696,7 +700,8 @@ func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel {
|
||||
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
@@ -734,20 +739,27 @@ func (us SqlUserStore) GetByEmail(email string) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (us SqlUserStore) GetByAuth(authData string, authService string) StoreChannel {
|
||||
func (us SqlUserStore) GetByAuth(authData *string, authService string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
if authData == nil || *authData == "" {
|
||||
result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", MISSING_AUTH_ACCOUNT_ERROR, nil, "authData='', authService="+authService)
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
}
|
||||
|
||||
user := model.User{}
|
||||
|
||||
if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE AuthData = :AuthData AND AuthService = :AuthService", map[string]interface{}{"AuthData": authData, "AuthService": authService}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", MISSING_AUTH_ACCOUNT_ERROR, nil, "authData="+authData+", authService="+authService+", "+err.Error())
|
||||
result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", MISSING_AUTH_ACCOUNT_ERROR, nil, "authData="+*authData+", authService="+authService+", "+err.Error())
|
||||
} else {
|
||||
result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", "store.sql_user.get_by_auth.other.app_error", nil, "authData="+authData+", authService="+authService+", "+err.Error())
|
||||
result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", "store.sql_user.get_by_auth.other.app_error", nil, "authData="+*authData+", authService="+authService+", "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,7 +869,8 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
|
||||
} else {
|
||||
for _, u := range users {
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
u.AuthData = new(string)
|
||||
*u.AuthData = ""
|
||||
}
|
||||
|
||||
result.Data = users
|
||||
|
||||
@@ -458,9 +458,11 @@ func TestUserStoreGetByAuthData(t *testing.T) {
|
||||
|
||||
teamId := model.NewId()
|
||||
|
||||
auth := "123" + model.NewId()
|
||||
|
||||
u1 := &model.User{}
|
||||
u1.Email = model.NewId()
|
||||
u1.AuthData = "123" + model.NewId()
|
||||
u1.AuthData = &auth
|
||||
u1.AuthService = "service"
|
||||
Must(store.User().Save(u1))
|
||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
|
||||
@@ -469,7 +471,8 @@ func TestUserStoreGetByAuthData(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (<-store.User().GetByAuth("", "")).Err; err == nil {
|
||||
rauth := ""
|
||||
if err := (<-store.User().GetByAuth(&rauth, "")).Err; err == nil {
|
||||
t.Fatal("Should have failed because of missing auth data")
|
||||
}
|
||||
}
|
||||
@@ -497,19 +500,23 @@ func TestUserStoreGetByUsername(t *testing.T) {
|
||||
func TestUserStoreGetForLogin(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
auth := model.NewId()
|
||||
|
||||
u1 := &model.User{
|
||||
Email: model.NewId(),
|
||||
Username: model.NewId(),
|
||||
AuthService: model.USER_AUTH_SERVICE_GITLAB,
|
||||
AuthData: model.NewId(),
|
||||
AuthData: &auth,
|
||||
}
|
||||
Must(store.User().Save(u1))
|
||||
|
||||
auth2 := model.NewId()
|
||||
|
||||
u2 := &model.User{
|
||||
Email: model.NewId(),
|
||||
Username: model.NewId(),
|
||||
AuthService: model.USER_AUTH_SERVICE_LDAP,
|
||||
AuthData: model.NewId(),
|
||||
AuthData: &auth2,
|
||||
}
|
||||
Must(store.User().Save(u2))
|
||||
|
||||
@@ -525,14 +532,14 @@ func TestUserStoreGetForLogin(t *testing.T) {
|
||||
t.Fatal("Should have gotten user1 by email")
|
||||
}
|
||||
|
||||
if result := <-store.User().GetForLogin(u2.AuthData, true, true, true); result.Err != nil {
|
||||
if result := <-store.User().GetForLogin(*u2.AuthData, true, true, true); result.Err != nil {
|
||||
t.Fatal("Should have gotten user by LDAP AuthData", result.Err)
|
||||
} else if result.Data.(*model.User).Id != u2.Id {
|
||||
t.Fatal("Should have gotten user2 by LDAP AuthData")
|
||||
}
|
||||
|
||||
// prevent getting user by AuthData when they're not an LDAP user
|
||||
if result := <-store.User().GetForLogin(u1.AuthData, true, true, true); result.Err == nil {
|
||||
if result := <-store.User().GetForLogin(*u1.AuthData, true, true, true); result.Err == nil {
|
||||
t.Fatal("Should not have gotten user by non-LDAP AuthData")
|
||||
}
|
||||
|
||||
@@ -545,23 +552,26 @@ func TestUserStoreGetForLogin(t *testing.T) {
|
||||
t.Fatal("Should have failed to get user1 by email")
|
||||
}
|
||||
|
||||
if result := <-store.User().GetForLogin(u2.AuthData, true, true, false); result.Err == nil {
|
||||
if result := <-store.User().GetForLogin(*u2.AuthData, true, true, false); result.Err == nil {
|
||||
t.Fatal("Should have failed to get user3 by LDAP AuthData")
|
||||
}
|
||||
|
||||
auth3 := model.NewId()
|
||||
|
||||
// test a special case where two users will have conflicting login information so we throw a special error
|
||||
u3 := &model.User{
|
||||
Email: model.NewId(),
|
||||
Username: model.NewId(),
|
||||
AuthService: model.USER_AUTH_SERVICE_LDAP,
|
||||
AuthData: model.NewId(),
|
||||
AuthData: &auth3,
|
||||
}
|
||||
Must(store.User().Save(u3))
|
||||
|
||||
u4 := &model.User{
|
||||
Email: model.NewId(),
|
||||
Username: model.NewId(),
|
||||
AuthService: model.USER_AUTH_SERVICE_LDAP,
|
||||
AuthData: u3.Username,
|
||||
AuthData: &u3.Username,
|
||||
}
|
||||
Must(store.User().Save(u4))
|
||||
|
||||
@@ -620,9 +630,9 @@ func TestUserStoreUpdateAuthData(t *testing.T) {
|
||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
|
||||
|
||||
service := "someservice"
|
||||
authData := "1"
|
||||
authData := model.NewId()
|
||||
|
||||
if err := (<-store.User().UpdateAuthData(u1.Id, service, authData, "")).Err; err != nil {
|
||||
if err := (<-store.User().UpdateAuthData(u1.Id, service, &authData, "")).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -633,7 +643,7 @@ func TestUserStoreUpdateAuthData(t *testing.T) {
|
||||
if user.AuthService != service {
|
||||
t.Fatal("AuthService was not updated correctly")
|
||||
}
|
||||
if user.AuthData != authData {
|
||||
if *user.AuthData != authData {
|
||||
t.Fatal("AuthData was not updated correctly")
|
||||
}
|
||||
if user.Password != "" {
|
||||
|
||||
@@ -126,7 +126,7 @@ type UserStore interface {
|
||||
UpdateLastActivityAt(userId string, time int64) StoreChannel
|
||||
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
|
||||
UpdatePassword(userId, newPassword string) StoreChannel
|
||||
UpdateAuthData(userId, service, authData, email string) StoreChannel
|
||||
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
|
||||
UpdateMfaSecret(userId, secret string) StoreChannel
|
||||
UpdateMfaActive(userId string, active bool) StoreChannel
|
||||
Get(id string) StoreChannel
|
||||
@@ -136,7 +136,7 @@ type UserStore interface {
|
||||
GetDirectProfiles(userId string) StoreChannel
|
||||
GetProfileByIds(userId []string) StoreChannel
|
||||
GetByEmail(email string) StoreChannel
|
||||
GetByAuth(authData string, authService string) StoreChannel
|
||||
GetByAuth(authData *string, authService string) StoreChannel
|
||||
GetByUsername(username string) StoreChannel
|
||||
GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel
|
||||
VerifyEmail(userId string) StoreChannel
|
||||
|
||||
@@ -1203,7 +1203,7 @@ export default class Client {
|
||||
end(this.handleResponse.bind(this, 'listTeamCommands', success, error));
|
||||
}
|
||||
|
||||
regenCommandToken = (commandId, suggest, success, error) => {
|
||||
regenCommandToken = (commandId, success, error) => {
|
||||
request.
|
||||
post(`${this.getCommandsRoute()}/regen_token`).
|
||||
set(this.defaultHeaders).
|
||||
|
||||
@@ -75,24 +75,25 @@ export default class Audits extends React.Component {
|
||||
<div>
|
||||
<ComplianceReports/>
|
||||
|
||||
<div className='panel'>
|
||||
<div className='panel audit-panel'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.audits.title'
|
||||
defaultMessage='User Activity'
|
||||
defaultMessage='User Activity Logs'
|
||||
/>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-link pull-right'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<i className='fa fa-refresh'></i>
|
||||
<FormattedMessage
|
||||
id='admin.audits.reload'
|
||||
defaultMessage='Reload User Activity Logs'
|
||||
/>
|
||||
</button>
|
||||
</h3>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.audits.reload'
|
||||
defaultMessage='Reload'
|
||||
/>
|
||||
</button>
|
||||
<div className='audit__panel'>
|
||||
<div className='audit-panel__table'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -266,121 +266,120 @@ export default class ComplianceReports extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='panel'>
|
||||
<div className='panel compliance-panel'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.title'
|
||||
defaultMessage='Compliance Reports'
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan='5'
|
||||
style={{paddingBottom: '6px'}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.desc'
|
||||
defaultMessage='Job Name:'
|
||||
/>
|
||||
<input
|
||||
style={{width: '425px'}}
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='desc'
|
||||
ref='desc'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.desc_placeholder', 'Ex "Audit 445 for HR"')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.from'
|
||||
defaultMessage='From:'
|
||||
/>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='from'
|
||||
ref='from'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.from_placeholder', 'Ex "2016-03-11"')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{paddingLeft: '4px'}}>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.to'
|
||||
defaultMessage='To:'
|
||||
/>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='to'
|
||||
ref='to'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.to_placeholder', 'Ex "2016-03-15"')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{paddingLeft: '4px'}}>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.emails'
|
||||
defaultMessage='Emails:'
|
||||
/>
|
||||
<input
|
||||
style={{width: '325px'}}
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='emails'
|
||||
ref='emails'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.emails_placeholder', 'Ex "bill@example.com, bob@example.com"')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{paddingLeft: '4px'}}>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.keywords'
|
||||
defaultMessage='Keywords:'
|
||||
/>
|
||||
<input
|
||||
style={{width: '250px'}}
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='keywords'
|
||||
ref='keywords'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.keywords_placeholder', 'Ex "shorting stock"')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
id='run-button'
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.runReport}
|
||||
style={{marginTop: '20px', marginLeft: '20px'}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.run'
|
||||
defaultMessage='Run'
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{serverError}
|
||||
<div style={{marginTop: '20px'}}>
|
||||
<div className='row'>
|
||||
<div className='col-sm-6 col-md-4 form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.desc'
|
||||
defaultMessage='Job Name:'
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='desc'
|
||||
ref='desc'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.desc_placeholder', 'E.g. "Audit 445 for HR"')}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-3 col-md-2 form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.from'
|
||||
defaultMessage='From:'
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='from'
|
||||
ref='from'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.from_placeholder', 'E.g. "2016-03-11"')}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-3 col-md-2 form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.to'
|
||||
defaultMessage='To:'
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='to'
|
||||
ref='to'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.to_placeholder', 'E.g. "2016-03-15"')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col-sm-6 col-md-4 form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.emails'
|
||||
defaultMessage='Emails:'
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='emails'
|
||||
ref='emails'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.emails_placeholder', 'E.g. "bill@example.com, bob@example.com"')}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-6 col-md-4 form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.keywords'
|
||||
defaultMessage='Keywords:'
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='keywords'
|
||||
ref='keywords'
|
||||
placeholder={Utils.localizeMessage('admin.compliance_reports.keywords_placeholder', 'E.g. "shorting stock"')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='clearfix'>
|
||||
<button
|
||||
id='run-button'
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.reload}
|
||||
onClick={this.runReport}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.reload'
|
||||
defaultMessage='Reload'
|
||||
id='admin.compliance_reports.run'
|
||||
defaultMessage='Run Compliance Report'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='compliance__panel'>
|
||||
{serverError}
|
||||
<div className='text-right'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-link'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<i className='fa fa-refresh'></i>
|
||||
<FormattedMessage
|
||||
id='admin.compliance_reports.reload'
|
||||
defaultMessage='Reload Completed Compliance Reports'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='compliance-panel__table'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import IntegrationStore from 'stores/integration_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
@@ -20,16 +21,18 @@ export default class InstalledCommands extends React.Component {
|
||||
this.regenCommandToken = this.regenCommandToken.bind(this);
|
||||
this.deleteCommand = this.deleteCommand.bind(this);
|
||||
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.state = {
|
||||
commands: IntegrationStore.getCommands(),
|
||||
loading: !IntegrationStore.hasReceivedCommands()
|
||||
commands: IntegrationStore.getCommands(teamId),
|
||||
loading: !IntegrationStore.hasReceivedCommands(teamId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
IntegrationStore.addChangeListener(this.handleIntegrationChange);
|
||||
|
||||
if (window.mm_config.EnableCommands === 'true' && this.state.loading) {
|
||||
if (window.mm_config.EnableCommands === 'true') {
|
||||
AsyncClient.listTeamCommands();
|
||||
}
|
||||
}
|
||||
@@ -39,9 +42,11 @@ export default class InstalledCommands extends React.Component {
|
||||
}
|
||||
|
||||
handleIntegrationChange() {
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.setState({
|
||||
commands: IntegrationStore.getCommands(),
|
||||
loading: !IntegrationStore.hasReceivedCommands()
|
||||
commands: IntegrationStore.getCommands(teamId),
|
||||
loading: !IntegrationStore.hasReceivedCommands(teamId)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import IntegrationStore from 'stores/integration_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
@@ -19,16 +20,18 @@ export default class InstalledIncomingWebhooks extends React.Component {
|
||||
|
||||
this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
|
||||
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.state = {
|
||||
incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
|
||||
loading: !IntegrationStore.hasReceivedIncomingWebhooks()
|
||||
incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId),
|
||||
loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
IntegrationStore.addChangeListener(this.handleIntegrationChange);
|
||||
|
||||
if (window.mm_config.EnableIncomingWebhooks === 'true' && this.state.loading) {
|
||||
if (window.mm_config.EnableIncomingWebhooks === 'true') {
|
||||
AsyncClient.listIncomingHooks();
|
||||
}
|
||||
}
|
||||
@@ -38,9 +41,11 @@ export default class InstalledIncomingWebhooks extends React.Component {
|
||||
}
|
||||
|
||||
handleIntegrationChange() {
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.setState({
|
||||
incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
|
||||
loading: !IntegrationStore.hasReceivedIncomingWebhooks()
|
||||
incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId),
|
||||
loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import IntegrationStore from 'stores/integration_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
@@ -20,16 +21,18 @@ export default class InstalledOutgoingWebhooks extends React.Component {
|
||||
this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
|
||||
this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
|
||||
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.state = {
|
||||
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(),
|
||||
loading: !IntegrationStore.hasReceivedOutgoingWebhooks()
|
||||
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId),
|
||||
loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
IntegrationStore.addChangeListener(this.handleIntegrationChange);
|
||||
|
||||
if (window.mm_config.EnableOutgoingWebhooks === 'true' && this.state.loading) {
|
||||
if (window.mm_config.EnableOutgoingWebhooks === 'true') {
|
||||
AsyncClient.listOutgoingHooks();
|
||||
}
|
||||
}
|
||||
@@ -39,9 +42,11 @@ export default class InstalledOutgoingWebhooks extends React.Component {
|
||||
}
|
||||
|
||||
handleIntegrationChange() {
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
|
||||
this.setState({
|
||||
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(),
|
||||
loading: !IntegrationStore.hasReceivedOutgoingWebhooks()
|
||||
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId),
|
||||
loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,7 @@ export default class Login extends React.Component {
|
||||
browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId));
|
||||
return;
|
||||
} else if (err.id === 'store.sql_user.get_for_login.app_error' ||
|
||||
err.id === 'ent.ldap.do_login.user_not_registered.app_error' ||
|
||||
err.id === 'ent.ldap.do_login.user_filtered.app_error') {
|
||||
err.id === 'ent.ldap.do_login.user_not_registered.app_error') {
|
||||
this.setState({
|
||||
showMfa: false,
|
||||
serverError: (
|
||||
@@ -98,6 +97,7 @@ export default class Login extends React.Component {
|
||||
});
|
||||
} else if (err.id === 'api.user.check_user_password.invalid.app_error' || err.id === 'ent.ldap.do_login.invalid_password.app_error') {
|
||||
this.setState({
|
||||
showMfa: false,
|
||||
serverError: (
|
||||
<FormattedMessage
|
||||
id='login.invalidPassword'
|
||||
|
||||
@@ -514,7 +514,7 @@ export default class Sidebar extends React.Component {
|
||||
<div className='sidebar__divider__text'>
|
||||
<FormattedMessage
|
||||
id='sidebar.otherMembers'
|
||||
defaultMessage='Other teams'
|
||||
defaultMessage='Outside this team'
|
||||
/>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
"add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)",
|
||||
"add_outgoing_webhook.triggerWords": "Trigger Words (One Per Line)",
|
||||
"add_outgoing_webhook.triggerWordsOrChannelRequired": "A valid channel or a list of trigger words is required",
|
||||
"admin.audits.reload": "Reload",
|
||||
"admin.audits.title": "User Activity",
|
||||
"admin.audits.reload": "Reload User Activity Logs",
|
||||
"admin.audits.title": "User Activity Logs",
|
||||
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
|
||||
"admin.compliance.directoryExample": "Ex \"./data/\"",
|
||||
"admin.compliance.directoryTitle": "Compliance Directory Location:",
|
||||
@@ -91,18 +91,18 @@
|
||||
"admin.compliance.title": "Compliance Settings",
|
||||
"admin.compliance.true": "true",
|
||||
"admin.compliance_reports.desc": "Job Name:",
|
||||
"admin.compliance_reports.desc_placeholder": "Ex \"Audit 445 for HR\"",
|
||||
"admin.compliance_reports.desc_placeholder": "E.g. \"Audit 445 for HR\"",
|
||||
"admin.compliance_reports.emails": "Emails:",
|
||||
"admin.compliance_reports.emails_placeholder": "Ex \"bill@example.com, bob@example.com\"",
|
||||
"admin.compliance_reports.emails_placeholder": "E.g. \"bill@example.com, bob@example.com\"",
|
||||
"admin.compliance_reports.from": "From:",
|
||||
"admin.compliance_reports.from_placeholder": "Ex \"2016-03-11\"",
|
||||
"admin.compliance_reports.from_placeholder": "E.g. \"2016-03-11\"",
|
||||
"admin.compliance_reports.keywords": "Keywords:",
|
||||
"admin.compliance_reports.keywords_placeholder": "Ex \"shorting stock\"",
|
||||
"admin.compliance_reports.reload": "Reload",
|
||||
"admin.compliance_reports.run": "Run",
|
||||
"admin.compliance_reports.keywords_placeholder": "E.g. \"shorting stock\"",
|
||||
"admin.compliance_reports.reload": "Reload Completed Compliance Reports",
|
||||
"admin.compliance_reports.run": "Run Compliance Report",
|
||||
"admin.compliance_reports.title": "Compliance Reports",
|
||||
"admin.compliance_reports.to": "To:",
|
||||
"admin.compliance_reports.to_placeholder": "Ex \"2016-03-15\"",
|
||||
"admin.compliance_reports.to_placeholder": "E.g. \"2016-03-15\"",
|
||||
"admin.compliance_table.desc": "Description",
|
||||
"admin.compliance_table.download": "Download",
|
||||
"admin.compliance_table.params": "Params",
|
||||
@@ -1130,7 +1130,7 @@
|
||||
"sidebar.direct": "Direct Messages",
|
||||
"sidebar.more": "More",
|
||||
"sidebar.moreElips": "More...",
|
||||
"sidebar.otherMembers": "Other teams",
|
||||
"sidebar.otherMembers": "Outside this team",
|
||||
"sidebar.pg": "Private Groups",
|
||||
"sidebar.removeList": "Remove from list",
|
||||
"sidebar.tutorialScreen1": "<h4>Channels</h4><p><strong>Channels</strong> organize conversations across different topics. They’re open to everyone on your team. To send private communications use <strong>Direct Messages</strong> for a single person or <strong>Private Groups</strong> for multiple people.</p>",
|
||||
|
||||
@@ -1130,7 +1130,7 @@
|
||||
"sidebar.direct": "Mensajes Directos",
|
||||
"sidebar.more": "Más",
|
||||
"sidebar.moreElips": "Más...",
|
||||
"sidebar.otherMembers": "Otros equipos",
|
||||
"sidebar.otherMembers": "Fuera de este equipo",
|
||||
"sidebar.pg": "Grupos Privados",
|
||||
"sidebar.removeList": "Remover de la lista",
|
||||
"sidebar.tutorialScreen1": "<h4>Canales</h4><p><strong>Canales</strong> organizan las conversaciones en diferentes tópicos. Son abiertos para cualquier persona de tu equipo. Para enviar comunicaciones privadas con una sola persona utiliza <strong>Mensajes Directos</strong> o con multiples personas utilizando <strong>Grupos Privados</strong>.</p>",
|
||||
@@ -1341,7 +1341,9 @@
|
||||
"user.settings.general.title": "Configuración General",
|
||||
"user.settings.general.uploadImage": "Pinchar 'Editar' para subir una imagen.",
|
||||
"user.settings.general.username": "Nombre de usuario",
|
||||
"user.settings.general.usernameInfo": "Escoge algo que sea fácil de reconocer y recordar para tus compañeros.",
|
||||
"user.settings.general.usernameReserved": "Este nombre de usuario está reservado, por favor escoge otro",
|
||||
"user.settings.general.usernameRestrictions": "El nombre de usuario debe comenzar con una letra y debe contener entre {min} y {max} caracteres en minúscula creado con numeros, letras y los símbolos '.', '-', y '_'.",
|
||||
"user.settings.general.validEmail": "Por favor ingresa una dirección de correo electrónico válida",
|
||||
"user.settings.general.validImage": "Sólo pueden ser utilizadas imágenes JPG o PNG en el perfil",
|
||||
"user.settings.import_theme.cancel": "Cancelar",
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.compliance-panel,
|
||||
.audit-panel {
|
||||
.row {
|
||||
> .form-group {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-popover {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -160,17 +160,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compliance__panel,
|
||||
.audit__panel {
|
||||
background-color: $white;
|
||||
border: 1px solid $border-gray;
|
||||
height: 70vh;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app__content {
|
||||
color: #333;
|
||||
|
||||
|
||||
38
webapp/sass/routes/_compliance.scss
Обычный файл
38
webapp/sass/routes/_compliance.scss
Обычный файл
@@ -0,0 +1,38 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
.compliance-panel__table,
|
||||
.audit-panel__table {
|
||||
background-color: $white;
|
||||
border: 1px solid $border-gray;
|
||||
margin-top: 10px;
|
||||
max-height: 70vh;
|
||||
min-height: 100px;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compliance-panel,
|
||||
.audit-panel {
|
||||
.row {
|
||||
> .form-group {
|
||||
padding-left: 0;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-refresh {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.compliance-panel {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
@import 'activity-log';
|
||||
@import 'admin-console';
|
||||
@import 'backstage';
|
||||
@import 'compliance';
|
||||
@import 'docs';
|
||||
@import 'error-page';
|
||||
@import 'loading';
|
||||
|
||||
@@ -15,14 +15,11 @@ class IntegrationStore extends EventEmitter {
|
||||
|
||||
this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
|
||||
|
||||
this.incomingWebhooks = [];
|
||||
this.receivedIncomingWebhooks = false;
|
||||
this.incomingWebhooks = new Map();
|
||||
|
||||
this.outgoingWebhooks = [];
|
||||
this.receivedOutgoingWebhooks = false;
|
||||
this.outgoingWebhooks = new Map();
|
||||
|
||||
this.commands = [];
|
||||
this.receivedCommands = false;
|
||||
this.commands = new Map();
|
||||
}
|
||||
|
||||
addChangeListener(callback) {
|
||||
@@ -37,100 +34,119 @@ class IntegrationStore extends EventEmitter {
|
||||
this.emit(CHANGE_EVENT);
|
||||
}
|
||||
|
||||
hasReceivedIncomingWebhooks() {
|
||||
return this.receivedIncomingWebhooks;
|
||||
hasReceivedIncomingWebhooks(teamId) {
|
||||
return this.incomingWebhooks.has(teamId);
|
||||
}
|
||||
|
||||
getIncomingWebhooks() {
|
||||
return this.incomingWebhooks;
|
||||
getIncomingWebhooks(teamId) {
|
||||
return this.incomingWebhooks.get(teamId) || [];
|
||||
}
|
||||
|
||||
setIncomingWebhooks(incomingWebhooks) {
|
||||
this.incomingWebhooks = incomingWebhooks;
|
||||
this.receivedIncomingWebhooks = true;
|
||||
setIncomingWebhooks(teamId, incomingWebhooks) {
|
||||
this.incomingWebhooks.set(teamId, incomingWebhooks);
|
||||
}
|
||||
|
||||
addIncomingWebhook(incomingWebhook) {
|
||||
this.incomingWebhooks.push(incomingWebhook);
|
||||
const teamId = incomingWebhook.team_id;
|
||||
const incomingWebhooks = this.getIncomingWebhooks(teamId);
|
||||
|
||||
incomingWebhooks.push(incomingWebhook);
|
||||
|
||||
this.setIncomingWebhooks(teamId, incomingWebhooks);
|
||||
}
|
||||
|
||||
removeIncomingWebhook(id) {
|
||||
for (let i = 0; i < this.incomingWebhooks.length; i++) {
|
||||
if (this.incomingWebhooks[i].id === id) {
|
||||
this.incomingWebhooks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
removeIncomingWebhook(teamId, id) {
|
||||
let incomingWebhooks = this.getIncomingWebhooks(teamId);
|
||||
|
||||
incomingWebhooks = incomingWebhooks.filter((incomingWebhook) => incomingWebhook.id !== id);
|
||||
|
||||
this.setIncomingWebhooks(teamId, incomingWebhooks);
|
||||
}
|
||||
|
||||
hasReceivedOutgoingWebhooks() {
|
||||
return this.receivedOutgoingWebhooks;
|
||||
hasReceivedOutgoingWebhooks(teamId) {
|
||||
return this.outgoingWebhooks.has(teamId);
|
||||
}
|
||||
|
||||
getOutgoingWebhooks() {
|
||||
return this.outgoingWebhooks;
|
||||
getOutgoingWebhooks(teamId) {
|
||||
return this.outgoingWebhooks.get(teamId) || [];
|
||||
}
|
||||
|
||||
setOutgoingWebhooks(outgoingWebhooks) {
|
||||
this.outgoingWebhooks = outgoingWebhooks;
|
||||
this.receivedOutgoingWebhooks = true;
|
||||
setOutgoingWebhooks(teamId, outgoingWebhooks) {
|
||||
this.outgoingWebhooks.set(teamId, outgoingWebhooks);
|
||||
}
|
||||
|
||||
addOutgoingWebhook(outgoingWebhook) {
|
||||
this.outgoingWebhooks.push(outgoingWebhook);
|
||||
const teamId = outgoingWebhook.team_id;
|
||||
const outgoingWebhooks = this.getOutgoingWebhooks(teamId);
|
||||
|
||||
outgoingWebhooks.push(outgoingWebhook);
|
||||
|
||||
this.setOutgoingWebhooks(teamId, outgoingWebhooks);
|
||||
}
|
||||
|
||||
updateOutgoingWebhook(outgoingWebhook) {
|
||||
for (let i = 0; i < this.outgoingWebhooks.length; i++) {
|
||||
if (this.outgoingWebhooks[i].id === outgoingWebhook.id) {
|
||||
this.outgoingWebhooks[i] = outgoingWebhook;
|
||||
const teamId = outgoingWebhook.team_id;
|
||||
const outgoingWebhooks = this.getOutgoingWebhooks(teamId);
|
||||
|
||||
for (let i = 0; i < outgoingWebhooks.length; i++) {
|
||||
if (outgoingWebhooks[i].id === outgoingWebhook.id) {
|
||||
outgoingWebhooks[i] = outgoingWebhook;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setOutgoingWebhooks(teamId, outgoingWebhooks);
|
||||
}
|
||||
|
||||
removeOutgoingWebhook(id) {
|
||||
for (let i = 0; i < this.outgoingWebhooks.length; i++) {
|
||||
if (this.outgoingWebhooks[i].id === id) {
|
||||
this.outgoingWebhooks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
removeOutgoingWebhook(teamId, id) {
|
||||
let outgoingWebhooks = this.getOutgoingWebhooks(teamId);
|
||||
|
||||
outgoingWebhooks = outgoingWebhooks.filter((outgoingWebhook) => outgoingWebhook.id !== id);
|
||||
|
||||
this.setOutgoingWebhooks(teamId, outgoingWebhooks);
|
||||
}
|
||||
|
||||
hasReceivedCommands() {
|
||||
return this.receivedCommands;
|
||||
hasReceivedCommands(teamId) {
|
||||
return this.commands.has(teamId);
|
||||
}
|
||||
|
||||
getCommands() {
|
||||
return this.commands;
|
||||
getCommands(teamId) {
|
||||
return this.commands.get(teamId) || [];
|
||||
}
|
||||
|
||||
setCommands(commands) {
|
||||
this.commands = commands;
|
||||
this.receivedCommands = true;
|
||||
setCommands(teamId, commands) {
|
||||
this.commands.set(teamId, commands);
|
||||
}
|
||||
|
||||
addCommand(command) {
|
||||
this.commands.push(command);
|
||||
const teamId = command.team_id;
|
||||
const commands = this.getCommands(teamId);
|
||||
|
||||
commands.push(command);
|
||||
|
||||
this.setCommands(teamId, commands);
|
||||
}
|
||||
|
||||
updateCommand(command) {
|
||||
for (let i = 0; i < this.commands.length; i++) {
|
||||
if (this.commands[i].id === command.id) {
|
||||
this.commands[i] = command;
|
||||
const teamId = command.team_id;
|
||||
const commands = this.getCommands(teamId);
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
if (commands[i].id === command.id) {
|
||||
commands[i] = command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setCommands(teamId, commands);
|
||||
}
|
||||
|
||||
removeCommand(id) {
|
||||
for (let i = 0; i < this.commands.length; i++) {
|
||||
if (this.commands[i].id === id) {
|
||||
this.commands.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
removeCommand(teamId, id) {
|
||||
let commands = this.getCommands(teamId);
|
||||
|
||||
commands = commands.filter((command) => command.id !== id);
|
||||
|
||||
this.setCommands(teamId, commands);
|
||||
}
|
||||
|
||||
handleEventPayload(payload) {
|
||||
@@ -138,7 +154,7 @@ class IntegrationStore extends EventEmitter {
|
||||
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECEIVED_INCOMING_WEBHOOKS:
|
||||
this.setIncomingWebhooks(action.incomingWebhooks);
|
||||
this.setIncomingWebhooks(action.teamId, action.incomingWebhooks);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_INCOMING_WEBHOOK:
|
||||
@@ -146,11 +162,11 @@ class IntegrationStore extends EventEmitter {
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.REMOVED_INCOMING_WEBHOOK:
|
||||
this.removeIncomingWebhook(action.id);
|
||||
this.removeIncomingWebhook(action.teamId, action.id);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS:
|
||||
this.setOutgoingWebhooks(action.outgoingWebhooks);
|
||||
this.setOutgoingWebhooks(action.teamId, action.outgoingWebhooks);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_OUTGOING_WEBHOOK:
|
||||
@@ -162,11 +178,11 @@ class IntegrationStore extends EventEmitter {
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.REMOVED_OUTGOING_WEBHOOK:
|
||||
this.removeOutgoingWebhook(action.id);
|
||||
this.removeOutgoingWebhook(action.teamId, action.id);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_COMMANDS:
|
||||
this.setCommands(action.commands);
|
||||
this.setCommands(action.teamId, action.commands);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_COMMAND:
|
||||
@@ -178,7 +194,7 @@ class IntegrationStore extends EventEmitter {
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.REMOVED_COMMAND:
|
||||
this.removeCommand(action.id);
|
||||
this.removeCommand(action.teamId, action.id);
|
||||
this.emitChange();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1145,6 +1145,7 @@ export function listIncomingHooks() {
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS,
|
||||
teamId: Client.teamId,
|
||||
incomingWebhooks: data
|
||||
});
|
||||
},
|
||||
@@ -1168,6 +1169,7 @@ export function listOutgoingHooks() {
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS,
|
||||
teamId: Client.teamId,
|
||||
outgoingWebhooks: data
|
||||
});
|
||||
},
|
||||
@@ -1230,6 +1232,7 @@ export function deleteIncomingHook(id) {
|
||||
() => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.REMOVED_INCOMING_WEBHOOK,
|
||||
teamId: Client.teamId,
|
||||
id
|
||||
});
|
||||
},
|
||||
@@ -1245,6 +1248,7 @@ export function deleteOutgoingHook(id) {
|
||||
() => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.REMOVED_OUTGOING_WEBHOOK,
|
||||
teamId: Client.teamId,
|
||||
id
|
||||
});
|
||||
},
|
||||
@@ -1282,6 +1286,7 @@ export function listTeamCommands() {
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_COMMANDS,
|
||||
teamId: Client.teamId,
|
||||
commands: data
|
||||
});
|
||||
},
|
||||
@@ -1321,6 +1326,7 @@ export function deleteCommand(id) {
|
||||
() => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.REMOVED_COMMAND,
|
||||
teamId: Client.teamId,
|
||||
id
|
||||
});
|
||||
},
|
||||
|
||||
Ссылка в новой задаче
Block a user