diff options
29 files changed, 1307 insertions, 300 deletions
diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html new file mode 100644 index 000000000..5b96df944 --- /dev/null +++ b/api/templates/signin_change_body.html @@ -0,0 +1,43 @@ +{{define "signin_change_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 20px 10px; text-align:left;"> + <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You updated your sign-in method</h2> + <p>You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p> + </td> + </tr> + <tr> + {{template "email_info" . }} + </tr> + </table> + </td> + </tr> + <tr> + {{template "email_footer" . }} + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + + diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html new file mode 100644 index 000000000..b1d644a28 --- /dev/null +++ b/api/templates/signin_change_subject.html @@ -0,0 +1 @@ +{{define "signin_change_subject"}}You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}} diff --git a/api/user.go b/api/user.go index 1df8fff73..36a3fd5f9 100644 --- a/api/user.go +++ b/api/user.go @@ -47,6 +47,8 @@ func InitUser(r *mux.Router) { sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST") sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") + sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") + sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") @@ -225,6 +227,70 @@ func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppErro } } +func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { + var user *model.User + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("CreateOAuthUser", service+" oauth not avlailable on this server", "") + return nil + } else { + user = provider.GetUserFromJson(userData) + } + + if user == nil { + c.Err = model.NewAppError("CreateOAuthUser", "Could not create user out of "+service+" user object", "") + return nil + } + + suchan := Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) + euchan := Srv.Store.User().GetByEmail(team.Id, user.Email) + + if team.Email == "" { + team.Email = user.Email + if result := <-Srv.Store.Team().Update(team); result.Err != nil { + c.Err = result.Err + return nil + } + } else { + found := true + count := 0 + for found { + if found = IsUsernameTaken(user.Username, team.Id); c.Err != nil { + return nil + } else if found { + user.Username = user.Username + strconv.Itoa(count) + count += 1 + } + } + } + + if result := <-suchan; result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) + return nil + } + + if result := <-euchan; result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) + return nil + } + + user.TeamId = team.Id + user.EmailVerified = true + + ruser, err := CreateUser(team, user) + if err != nil { + c.Err = err + return nil + } + + Login(c, w, r, ruser, "") + if c.Err != nil { + return nil + } + + return ruser +} + func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { @@ -335,6 +401,11 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam } else { user := result.Data.(*model.User) + if len(user.AuthData) != 0 { + c.Err = model.NewAppError("LoginByEmail", "Please sign in using "+user.AuthService, "") + return nil + } + if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { Login(c, w, r, user, deviceId) return user @@ -344,10 +415,36 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam return nil } +func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { + authData := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("LoginByOAuth", service+" oauth not avlailable on this server", "") + return nil + } else { + authData = provider.GetAuthDataFromJson(userData) + } + + if len(authData) == 0 { + c.Err = model.NewAppError("LoginByOAuth", "Could not parse auth data out of "+service+" user object", "") + return nil + } + + var user *model.User + if result := <-Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { + c.Err = result.Err + return nil + } else { + user = result.Data.(*model.User) + Login(c, w, r, user, "") + return user + } +} + func checkUserLoginAttempts(c *Context, user *model.User) bool { if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { c.LogAuditWithUserId(user.Id, "fail") - c.Err = model.NewAppError("checkUserPassword", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id) + c.Err = model.NewAppError("checkUserLoginAttempts", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden return false } @@ -1660,21 +1757,22 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } } -func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri, loginHint string) { +func GetAuthorizationCode(c *Context, service, teamName string, props map[string]string, loginHint string) (string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso != nil && !sso.Enable { - c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) - c.Err.StatusCode = http.StatusBadRequest - return + return "", model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) } clientId := sso.Id endpoint := sso.AuthEndpoint scope := sso.Scope - stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)} - state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + props["hash"] = model.HashPassword(clientId) + props["team"] = teamName + state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) + + redirectUri := c.GetSiteURL() + "/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -1686,18 +1784,18 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te authUrl += "&login_hint=" + utils.UrlEncode(loginHint) } - http.Redirect(w, r, authUrl, http.StatusFound) + return authUrl, nil } -func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) { +func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, map[string]string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso == nil || !sso.Enable { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) } stateStr := "" if b, err := b64.StdEncoding.DecodeString(state); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error()) } else { stateStr = string(b) } @@ -1705,12 +1803,13 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser stateProps := model.MapFromJson(strings.NewReader(stateStr)) if !model.ComparePassword(stateProps["hash"], sso.Id) { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "") } - teamName := stateProps["team"] - if len(teamName) == 0 { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "") + ok := true + teamName := "" + if teamName, ok = stateProps["team"]; !ok { + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "") } tchan := Srv.Store.Team().GetByName(teamName) @@ -1730,20 +1829,20 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser var ar *model.AccessResponse if resp, err := client.Do(req); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) } else { ar = model.AccessResponseFromJson(resp.Body) if ar == nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "") } } if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) } if len(ar.AccessToken) == 0 { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") } p = url.Values{} @@ -1755,12 +1854,12 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser req.Header.Set("Authorization", "Bearer "+ar.AccessToken) if resp, err := client.Do(req); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) } else { if result := <-tchan; result.Err != nil { - return nil, nil, result.Err + return nil, nil, nil, result.Err } else { - return resp.Body, result.Data.(*model.Team), nil + return resp.Body, result.Data.(*model.Team), stateProps, nil } } @@ -1780,3 +1879,200 @@ func IsUsernameTaken(name string, teamId string) bool { return false } + +func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + password := props["password"] + if len(password) == 0 { + c.SetInvalidParam("switchToSSO", "password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("switchToSSO", "team_name") + return + } + + service := props["service"] + if len(service) == 0 { + c.SetInvalidParam("switchToSSO", "service") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("switchToSSO", "email") + 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, password) { + c.LogAuditWithUserId(user.Id, "fail - invalid password") + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO + stateProps["email"] = email + + m := map[string]string{} + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.LogAuditWithUserId(user.Id, "fail - oauth issue") + c.Err = err + return + } else { + m["follow_link"] = authUrl + } + + c.LogAuditWithUserId(user.Id, "success") + w.Write([]byte(model.MapToJson(m))) +} + +func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) { + authData := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("CompleteClaimWithOAuth", service+" oauth not avlailable on this server", "") + return + } else { + authData = provider.GetAuthDataFromJson(userData) + } + + if len(authData) == 0 { + c.Err = model.NewAppError("CompleteClaimWithOAuth", "Could not parse auth data out of "+service+" user object", "") + return + } + + if len(email) == 0 { + c.Err = model.NewAppError("CompleteClaimWithOAuth", "Blank email", "") + return + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData); result.Err != nil { + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") +} + +func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + password := props["password"] + if len(password) == 0 { + c.SetInvalidParam("switchToEmail", "password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("switchToEmail", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("switchToEmail", "email") + 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.Id != c.Session.UserId { + c.LogAudit("fail - user ids didn't match") + c.Err = model.NewAppError("switchToEmail", "Update password failed because context user_id did not match provided user's id", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil { + c.LogAudit("fail - database issue") + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password") + + RevokeAllSession(c, c.Session.UserId) + if c.Err != nil { + return + } + + m := map[string]string{} + m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(m))) +} + +func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { + go func() { + + subjectPage := NewServerTemplatePage("signin_change_subject") + subjectPage.Props["SiteURL"] = siteURL + subjectPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage := NewServerTemplatePage("signin_change_body") + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage.Props["TeamURL"] = teamURL + bodyPage.Props["Method"] = method + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send update password email successfully err=%v", err) + } + + }() +} diff --git a/api/user_test.go b/api/user_test.go index 731450321..ffa96cb66 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1085,3 +1085,106 @@ func TestStatuses(t *testing.T) { } } + +func TestSwitchToSSO(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()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + m := map[string]string{} + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - empty data") + } + + m["password"] = "pwd" + _, err := Client.SwitchToSSO(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 { + t.Fatal("should have failed - missing service, email") + } + + m["service"] = "someservice" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - missing email") + } + + m["team_name"] = "junk" + if _, err := Client.SwitchToSSO(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 { + t.Fatal("should have failed - bad email") + } + + m["email"] = ruser.Email + m["password"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad password") + } +} + +func TestSwitchToEmail(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()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) + + m := map[string]string{} + if _, err := Client.SwitchToSSO(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 { + t.Fatal("should have failed - empty data") + } + + m["password"] = "pwd" + _, err := Client.SwitchToSSO(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 { + t.Fatal("should have failed - missing email") + } + + m["email"] = ruser.Email + m["team_name"] = "junk" + if _, err := Client.SwitchToSSO(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 { + t.Fatal("should have failed - bad email") + } + + m["email"] = ruser2.Email + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - wrong user") + } +} diff --git a/config/config.json b/config/config.json index 516b96dab..5469608f8 100644 --- a/config/config.json +++ b/config/config.json @@ -100,4 +100,4 @@ "TokenEndpoint": "", "UserApiEndpoint": "" } -}
\ No newline at end of file +} diff --git a/doc/install/Production-Debian.md b/doc/install/Production-Debian.md index e33dd2960..ffe3d79e8 100644 --- a/doc/install/Production-Debian.md +++ b/doc/install/Production-Debian.md @@ -1,6 +1,6 @@ # (Community Guide) Production Installation on Debian Jessie (x64) -Note: This install guide has been generously contributed by the Mattermost community. It has not yet been tested by the core. We have [an open ticket](https://github.com/mattermost/platform/issues/1185) requesting community help testing and improving this guide. Once the community has confirmed we have multiple deployments on these instructions, we can update the text here. If you're installing on Debian anyway, please let us know any issues or instruciton improvements? https://github.com/mattermost/platform/issues/1185 +Note: This install guide has been generously contributed by the Mattermost community. It has not yet been tested by the core team. We have [an open ticket](https://github.com/mattermost/platform/issues/1185) requesting community help testing and improving this guide. Once the community has confirmed we have multiple deployments on these instructions, we can update the text here. If you're installing on Debian anyway, please let us know any issues or instruciton improvements? https://github.com/mattermost/platform/issues/1185 ## Install Debian Jessie (x64) diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md index edcc754f8..4480dedd2 100644 --- a/doc/install/Upgrade-Guide.md +++ b/doc/install/Upgrade-Guide.md @@ -36,6 +36,24 @@ If you're upgrading across multiple major releases, from 1.0.x to 1.2.x for exam The following instructions apply to updating installations of Mattermost v0.7-Beta to Mattermost 1.1. +## GitLab Mattermost Upgrade Troubleshooting + +#### Upgrading GitLab Mattermost when GitLab upgrade skips versions + +Mattermost is designed to be upgraded sequentially through major version releases. If you skip versions when upgrading GitLab, you may find a `panic: The database schema version of 1.1.0 cannot be upgraded. You must not skip a version` error in your `/var/log/gitlab/mattermost/current` directory. If so: + +1. Run `platform -version` to check your version of Mattermost +2. If your version of the Mattermost binary doesn't match the version listed in the database error message, downgrade the version of the Mattermost binary you are using by [following the manual upgrade steps for Mattermost](/var/log/gitlab/mattermost/current) and using the database schema version listed in the error messages for the version you select in Step 1) iv). +3. Once Mattermost is working again, you can use the same upgrade procedure to upgrade Mattermost version by version to your current GitLab version. After this is done, GitLab automation should continue to work for future upgrades, so long as you don't skip versions. + +| GitLab Version | Mattermost Version | +|----------------|---------------------| +| 8.1.x | v1.1.0 | +| 8.2.x | v1.2.1 | +| 8.3.x | v1.3.0 | + +## Upgrade Guide for Mattermost Beta Release + #### Upgrading Mattermost in GitLab 8.0 to GitLab 8.1 with omnibus Mattermost 0.7.1-beta in GitLab 8.0 was a pre-release of Mattermost and Mattermost v1.1.1 in GitLab 8.1 was [updated significantly](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) to get to a stable, forwards-compatible platform for Mattermost. @@ -48,7 +66,6 @@ The Mattermost team didn't think it made sense for GitLab omnibus to attempt an Optionally, you can use the new [System Console user interface](https://github.com/mattermost/platform/blob/master/doc/install/Configuration-Settings.md) to make changes to your new `config.json` file. - #### Upgrading Mattermost v0.7.1-beta to v1.1.1 _Note: [Mattermost v1.1.1](https://github.com/mattermost/platform/releases/tag/v1.1.1) is a special release of Mattermost v1.1 that upgrades the database to Mattermost v1.1 from EITHER Mattermost v0.7 or Mattermost v1.0. The following instructions are for upgrading from Mattermost v0.7.1-beta to v1.1.1 and skipping the upgrade to Mattermost v1.0._ diff --git a/model/client.go b/model/client.go index b9b97dedc..f1773f3c7 100644 --- a/model/client.go +++ b/model/client.go @@ -349,6 +349,24 @@ 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 { + 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) SwitchToEmail(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/switch_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) Command(channelId string, command string, suggest bool) (*Result, *AppError) { m := make(map[string]string) m["command"] = command diff --git a/model/oauth.go b/model/oauth.go index 448fd9dc9..8336e26ba 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -10,6 +10,13 @@ import ( "unicode/utf8" ) +const ( + OAUTH_ACTION_SIGNUP = "signup" + OAUTH_ACTION_LOGIN = "login" + OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso" + OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email" +) + type OAuthApp struct { Id string `json:"id"` CreatorId string `json:"creator_id"` diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 82a2ccd05..88c4f954b 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -266,7 +266,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, FailedAttempts = 0 WHERE Id = :UserId AND AuthData = ''", 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 = '', AuthService = '', FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil { result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error()) } else { result.Data = userId @@ -298,6 +298,28 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int) return storeChannel } +func (us SqlUserStore) UpdateAuthData(userId, service, authData string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt := model.GetMillis() + + if _, err := us.GetMaster().Exec("UPDATE Users SET Password = '', LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0, AuthService = :AuthService, AuthData = :AuthData WHERE Id = :UserId", map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData}); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdateAuthData", "We couldn't update the auth data", "id="+userId+", "+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) Get(id string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index dd08438f1..d1ee5e647 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -390,3 +390,34 @@ func TestUserStoreDelete(t *testing.T) { t.Fatal(err) } } + +func TestUserStoreUpdateAuthData(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(&u1)) + + service := "someservice" + authData := "1" + + if err := (<-store.User().UpdateAuthData(u1.Id, service, authData)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil { + t.Fatal(r1.Err) + } else { + user := r1.Data.(*model.User) + if user.AuthService != service { + t.Fatal("AuthService was not updated correctly") + } + if user.AuthData != authData { + t.Fatal("AuthData was not updated correctly") + } + if user.Password != "" { + t.Fatal("Password was not cleared properly") + } + } +} diff --git a/store/store.go b/store/store.go index 682195148..8e03c8ee7 100644 --- a/store/store.go +++ b/store/store.go @@ -111,6 +111,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 string) StoreChannel Get(id string) StoreChannel GetProfiles(teamId string) StoreChannel GetByEmail(teamId string, email string) StoreChannel diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index f71e1c9ab..3143bec22 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -37,10 +37,6 @@ export default class AboutBuildModal extends React.Component { <div className='col-sm-3 info__label'>{'Build Hash:'}</div> <div className='col-sm-9'>{config.BuildHash}</div> </div> - <div className='row'> - <div className='col-sm-3 info__label'>{'Enterprise Ready:'}</div> - <div className='col-sm-9'>{config.BuildEnterpriseReady}</div> - </div> </Modal.Body> <Modal.Footer> <button diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index d7582d682..e235819fe 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -172,7 +172,16 @@ export default class ServiceSettings extends React.Component { defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey} onChange={this.handleChange} /> - <p className='help-text'>{'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '}<a href='https://www.youtube.com/watch?v=Im69kzhpR3I'>{'https://www.youtube.com/watch?v=Im69kzhpR3I'}</a>{'. Leaving field blank disables the automatic generation of YouTube video previews from links.'}</p> + <p className='help-text'> + {'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '} + <a + href='https://www.youtube.com/watch?v=Im69kzhpR3I' + target='_blank' + > + {'https://www.youtube.com/watch?v=Im69kzhpR3I'} + </a> + {'. Leaving the field blank disables the automatic generation of YouTube video previews from links.'} + </p> </div> </div> diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx new file mode 100644 index 000000000..f38f558db --- /dev/null +++ b/web/react/components/claim/claim_account.jsx @@ -0,0 +1,53 @@ +// 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'; + +export default class ClaimAccount extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + render() { + let content; + if (this.props.email === '') { + content = <p>{'No email specified.'}</p>; + } else if (this.props.currentType === '' && this.props.newType !== '') { + content = ( + <EmailToSSO + email={this.props.email} + type={this.props.newType} + teamName={this.props.teamName} + teamDisplayName={this.props.teamDisplayName} + /> + ); + } else { + content = ( + <SSOToEmail + email={this.props.email} + currentType={this.props.currentType} + teamName={this.props.teamName} + teamDisplayName={this.props.teamDisplayName} + /> + ); + } + + return ( + <div> + {content} + </div> + ); + } +} + +ClaimAccount.defaultProps = { +}; +ClaimAccount.propTypes = { + currentType: React.PropTypes.string.isRequired, + newType: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/claim/email_to_sso.jsx b/web/react/components/claim/email_to_sso.jsx new file mode 100644 index 000000000..ac0cf876b --- /dev/null +++ b/web/react/components/claim/email_to_sso.jsx @@ -0,0 +1,97 @@ +// 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'; + +export default class EmailToSSO extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + var state = {}; + + var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = 'Please enter your password.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + postData.service = this.props.type; + + Client.switchToSSO(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'; + } + + const uiType = Utils.toTitleCase(this.props.type) + ' SSO'; + + return ( + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3>{'Switch Email/Password Account to ' + uiType}</h3> + <form onSubmit={this.submit}> + <p>{'Upon claiming your account, you will only be able to login with ' + Utils.toTitleCase(this.props.type) + ' SSO.'}</p> + <p>{'Enter the password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder='Password' + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + {'Switch account to ' + uiType} + </button> + </form> + </div> + </div> + ); + } +} + +EmailToSSO.defaultProps = { +}; +EmailToSSO.propTypes = { + type: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx new file mode 100644 index 000000000..0868b7f2f --- /dev/null +++ b/web/react/components/claim/sso_to_email.jsx @@ -0,0 +1,113 @@ +// 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'; + +export default class SSOToEmail extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + const state = {}; + + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = 'Please enter a password.'; + this.setState(state); + return; + } + + const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim(); + if (!confirmPassword || password !== confirmPassword) { + state.error = 'Passwords do not match.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + + Client.switchToEmail(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'; + } + + const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; + + return ( + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3>{'Switch ' + uiType + ' Account to Email'}</h3> + <form onSubmit={this.submit}> + <p>{'Upon changing your account type, you will only be able to login with your email and password.'}</p> + <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder='New Password' + spellCheck='false' + /> + </div> + <div className={formClass}> + <input + type='password' + className='form-control' + name='passwordconfirm' + ref='passwordconfirm' + placeholder='Confirm Password' + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + {'Switch ' + uiType + ' account to email and password'} + </button> + </form> + </div> + </div> + ); + } +} + +SSOToEmail.defaultProps = { +}; +SSOToEmail.propTypes = { + currentType: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 9afaa8b0d..1d9b3e906 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -1,10 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; import LoginEmail from './login_email.jsx'; import LoginLdap from './login_ldap.jsx'; +import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; + export default class Login extends React.Component { constructor(props) { super(props); @@ -40,15 +42,24 @@ export default class Login extends React.Component { ); } - const verifiedParam = Utils.getUrlParameter('verified'); - let verifiedBox = ''; - if (verifiedParam) { - verifiedBox = ( - <div className='alert alert-success'> - <i className='fa fa-check' /> - {' Email Verified'} - </div> - ); + const extraParam = Utils.getUrlParameter('extra'); + let extraBox = ''; + if (extraParam) { + let msg; + if (extraParam === Constants.SIGNIN_CHANGE) { + msg = ' Sign-in method changed successfully'; + } else if (extraParam === Constants.SIGNIN_VERIFIED) { + msg = ' Email Verified'; + } + + if (msg != null) { + extraBox = ( + <div className='alert alert-success'> + <i className='fa fa-check' /> + {msg} + </div> + ); + } } let emailSignup; @@ -124,7 +135,7 @@ export default class Login extends React.Component { <h5 className='margin--less'>{'Sign in to:'}</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> <h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2> - {verifiedBox} + {extraBox} {loginMessage} {emailSignup} {ldapLogin} diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index 9ebb55646..1506e3c98 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -162,7 +162,14 @@ export default class ManageIncomingHooks extends React.Component { return ( <div key='addIncomingHook'> - {'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'} + {'Create webhook URLs for use in external integrations. Please see '} + <a + href='http://mattermost.org/webhooks' + target='_blank' + > + {'http://mattermost.org/webhooks'} + </a> + {' to learn more.'} <div><label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label></div> <div className='row padding-top'> <div className='col-sm-10 padding-bottom'> diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index ede639691..17acf0f10 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -240,7 +240,14 @@ export default class ManageOutgoingHooks extends React.Component { return ( <div key='addOutgoingHook'> - {'Create webhooks to send new message events to an external integration. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'} + {'Create webhooks to send new message events to an external integration. Please see '} + <a + href='http://mattermost.org/webhooks' + target='_blank' + > + {'http://mattermost.org/webhooks'} + </a> + {' to learn more.'} <div><label className='control-label padding-top x2'>{'Add a new outgoing webhook'}</label></div> <div className='padding-top divider-light'></div> <div className='padding-top'> diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 96c3985d0..1ff0a2913 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -29,17 +29,16 @@ export default class UserSettingsDisplay extends React.Component { this.handleNameRadio = this.handleNameRadio.bind(this); this.handleFont = this.handleFont.bind(this); this.updateSection = this.updateSection.bind(this); + this.updateState = this.updateState.bind(this); + this.deactivate = this.deactivate.bind(this); this.state = getDisplayStateFromStores(); - this.selectedFont = this.state.selectedFont; } handleSubmit() { const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat); const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont); - this.selectedFont = this.state.selectedFont; - savePreferences([timePreference, namePreference, fontPreference], () => { PreferenceStore.emitChange(); @@ -61,9 +60,19 @@ export default class UserSettingsDisplay extends React.Component { this.setState({selectedFont}); } updateSection(section) { - this.setState(getDisplayStateFromStores()); + this.updateState(); this.props.updateSection(section); } + updateState() { + const newState = getDisplayStateFromStores(); + if (!Utils.areObjectsEqual(newState, this.state)) { + this.handleFont(newState.selectedFont); + this.setState(newState); + } + } + deactivate() { + this.updateState(); + } render() { const serverError = this.state.serverError || null; let clockSection; @@ -266,9 +275,6 @@ export default class UserSettingsDisplay extends React.Component { submit={this.handleSubmit} server_error={serverError} updateSection={(e) => { - if (this.selectedFont !== this.state.selectedFont) { - this.handleFont(this.selectedFont); - } this.updateSection(''); e.preventDefault(); }} diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index fa2fecf07..d9c5f58a9 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -6,6 +6,9 @@ import SettingItemMax from '../setting_item_max.jsx'; import AccessHistoryModal from '../access_history_modal.jsx'; import ActivityLogModal from '../activity_log_modal.jsx'; import ToggleModalButton from '../toggle_modal_button.jsx'; + +import TeamStore from '../../stores/team_store.jsx'; + import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import Constants from '../../utils/constants.jsx'; @@ -18,9 +21,19 @@ export default class SecurityTab extends React.Component { this.updateCurrentPassword = this.updateCurrentPassword.bind(this); this.updateNewPassword = this.updateNewPassword.bind(this); this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - this.setupInitialState = this.setupInitialState.bind(this); + this.getDefaultState = this.getDefaultState.bind(this); + this.createPasswordSection = this.createPasswordSection.bind(this); + this.createSignInSection = this.createSignInSection.bind(this); - this.state = this.setupInitialState(); + this.state = this.getDefaultState(); + } + getDefaultState() { + return { + currentPassword: '', + newPassword: '', + confirmPassword: '', + authService: this.props.user.auth_service + }; } submitPassword(e) { e.preventDefault(); @@ -51,13 +64,13 @@ export default class SecurityTab extends React.Component { data.new_password = newPassword; Client.updatePassword(data, - function success() { + () => { this.props.updateSection(''); AsyncClient.getMe(); - this.setState(this.setupInitialState()); - }.bind(this), - function fail(err) { - var state = this.setupInitialState(); + this.setState(this.getDefaultState()); + }, + (err) => { + var state = this.getDefaultState(); if (err.message) { state.serverError = err.message; } else { @@ -65,7 +78,7 @@ export default class SecurityTab extends React.Component { } state.passwordError = ''; this.setState(state); - }.bind(this) + } ); } updateCurrentPassword(e) { @@ -77,86 +90,60 @@ export default class SecurityTab extends React.Component { updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); } - setupInitialState() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; - } - render() { - var serverError; - if (this.state.serverError) { - serverError = this.state.serverError; - } - var passwordError; - if (this.state.passwordError) { - passwordError = this.state.passwordError; - } + createPasswordSection() { + let updateSectionStatus; - var updateSectionStatus; - var passwordSection; - if (this.props.activeSection === 'password') { - var inputs = []; - var submit = null; - - if (this.props.user.auth_service === '') { - inputs.push( - <div - key='currentPasswordUpdateForm' - className='form-group' - > - <label className='col-sm-5 control-label'>Current Password</label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateCurrentPassword} - value={this.state.currentPassword} - /> - </div> - </div> - ); - inputs.push( - <div - key='newPasswordUpdateForm' - className='form-group' - > - <label className='col-sm-5 control-label'>New Password</label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateNewPassword} - value={this.state.newPassword} - /> - </div> + if (this.props.activeSection === 'password' && this.props.user.auth_service === '') { + const inputs = []; + + inputs.push( + <div + key='currentPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'>{'Current Password'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateCurrentPassword} + value={this.state.currentPassword} + /> </div> - ); - inputs.push( - <div - key='retypeNewPasswordUpdateForm' - className='form-group' - > - <label className='col-sm-5 control-label'>Retype New Password</label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateConfirmPassword} - value={this.state.confirmPassword} - /> - </div> + </div> + ); + inputs.push( + <div + key='newPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'>{'New Password'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateNewPassword} + value={this.state.newPassword} + /> </div> - ); - - submit = this.submitPassword; - } else { - inputs.push( - <div - key='oauthPasswordInfo' - className='form-group' - > - <label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> + </div> + ); + inputs.push( + <div + key='retypeNewPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'>{'Retype New Password'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateConfirmPassword} + value={this.state.confirmPassword} + /> </div> - ); - } + </div> + ); updateSectionStatus = function resetSection(e) { this.props.updateSection(''); @@ -164,51 +151,157 @@ export default class SecurityTab extends React.Component { e.preventDefault(); }.bind(this); - passwordSection = ( + return ( <SettingItemMax title='Password' inputs={inputs} - submit={submit} - server_error={serverError} - client_error={passwordError} + submit={this.submitPassword} + server_error={this.state.serverError} + client_error={this.state.passwordError} updateSection={updateSectionStatus} /> ); - } else { - var describe; - if (this.props.user.auth_service === '') { - var d = new Date(this.props.user.last_password_update); - var hour = '12'; - if (d.getHours() % 12) { - hour = String(d.getHours() % 12); - } - var min = String(d.getMinutes()); - if (d.getMinutes() < 10) { - min = '0' + d.getMinutes(); - } - var timeOfDay = ' am'; - if (d.getHours() >= 12) { - timeOfDay = ' pm'; - } + } + + var describe; + var d = new Date(this.props.user.last_password_update); + var hour = '12'; + if (d.getHours() % 12) { + hour = String(d.getHours() % 12); + } + var min = String(d.getMinutes()); + if (d.getMinutes() < 10) { + min = '0' + d.getMinutes(); + } + var timeOfDay = ' am'; + if (d.getHours() >= 12) { + timeOfDay = ' pm'; + } - describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; - } else { - describe = 'Log in done through GitLab'; + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; + + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); + + return ( + <SettingItemMin + title='Password' + describe={describe} + updateSection={updateSectionStatus} + /> + ); + } + createSignInSection() { + let updateSectionStatus; + const user = this.props.user; + + if (this.props.activeSection === 'signin') { + const inputs = []; + const teamName = TeamStore.getCurrent().name; + + let emailOption; + if (global.window.mm_config.EnableSignUpWithEmail === 'true' && user.auth_service !== '') { + emailOption = ( + <div> + <a + className='btn btn-primary' + href={'/' + teamName + '/claim?email=' + user.email} + > + {'Switch to using email and password'} + </a> + <br/> + </div> + ); } - updateSectionStatus = function updateSection() { - this.props.updateSection('password'); + let gitlabOption; + if (global.window.mm_config.EnableSignUpWithGitLab === 'true' && user.auth_service === '') { + gitlabOption = ( + <div> + <a + className='btn btn-primary' + href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GITLAB_SERVICE} + > + {'Switch to using GitLab SSO'} + </a> + <br/> + </div> + ); + } + + let googleOption; + if (global.window.mm_config.EnableSignUpWithGoogle === 'true' && user.auth_service === '') { + googleOption = ( + <div> + <a + className='btn btn-primary' + href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GOOGLE_SERVICE} + > + {'Switch to using Google SSO'} + </a> + <br/> + </div> + ); + } + + inputs.push( + <div key='userSignInOption'> + {emailOption} + {gitlabOption} + <br/> + {googleOption} + </div> + ); + + updateSectionStatus = function updateSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); }.bind(this); - passwordSection = ( - <SettingItemMin - title='Password' - describe={describe} + const extraInfo = <span>{'You may only have one sign-in method at a time. Switching sign-in method will send an email notifying you if the change was successful.'}</span>; + + return ( + <SettingItemMax + title='Sign-in Method' + extraInfo={extraInfo} + inputs={inputs} + server_error={this.state.serverError} updateSection={updateSectionStatus} /> ); } + updateSectionStatus = function updateSection() { + this.props.updateSection('signin'); + }.bind(this); + + let describe = 'Email and Password'; + if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = 'GitLab SSO'; + } + + return ( + <SettingItemMin + title='Sign-in Method' + describe={describe} + updateSection={updateSectionStatus} + /> + ); + } + render() { + const passwordSection = this.createPasswordSection(); + let signInSection; + + let numMethods = 0; + numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; + numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; + + if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) { + signInSection = this.createSignInSection(); + } + return ( <div> <div className='modal-header'> @@ -233,9 +326,11 @@ export default class SecurityTab extends React.Component { </h4> </div> <div className='user-settings'> - <h3 className='tab-header'>Security Settings</h3> + <h3 className='tab-header'>{'Security Settings'}</h3> <div className='divider-dark first'/> {passwordSection} + <div className='divider-light'/> + {signInSection} <div className='divider-dark'/> <br></br> <ToggleModalButton diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 7edf6283b..196a44bd0 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -31,19 +31,12 @@ export default class ViewImageModal extends React.Component { this.onMouseEnterImage = this.onMouseEnterImage.bind(this); this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this); - const loaded = []; - const progress = []; - for (var i = 0; i < this.props.filenames.length; i++) { - loaded.push(false); - progress.push(0); - } - this.state = { imgId: this.props.startId, - fileInfo: new Map(), + fileInfo: null, imgHeight: '100%', - loaded, - progress, + loaded: Utils.fillArray(false, this.props.filenames.length), + progress: Utils.fillArray(0, this.props.filenames.length), showFooter: false }; } @@ -104,17 +97,28 @@ export default class ViewImageModal extends React.Component { } else if (nextProps.show === false && this.props.show === true) { this.onModalHidden(); } + + if (!Utils.areObjectsEqual(this.props.filenames, nextProps.filenames)) { + this.setState({ + loaded: Utils.fillArray(false, nextProps.filenames.length), + progress: Utils.fillArray(0, nextProps.filenames.length) + }); + } } onFileStoreChange(filename) { const id = this.props.filenames.indexOf(filename); - if (id !== -1 && !this.state.loaded[id]) { - const fileInfo = this.state.fileInfo; - fileInfo.set(filename, FileStore.getInfo(filename)); - this.setState({fileInfo}); + if (id !== -1) { + if (id === this.state.imgId) { + this.setState({ + fileInfo: FileStore.getInfo(filename) + }); + } - this.loadImage(id, filename); + if (!this.state.loaded[id]) { + this.loadImage(id, filename); + } } } @@ -132,6 +136,10 @@ export default class ViewImageModal extends React.Component { return; } + this.setState({ + fileInfo: FileStore.getInfo(filename) + }); + if (!this.state.loaded[id]) { this.loadImage(id, filename); } @@ -227,8 +235,8 @@ export default class ViewImageModal extends React.Component { var content; if (this.state.loaded[this.state.imgId]) { - // if a file has been loaded, we also have its info - const fileInfo = this.state.fileInfo.get(filename); + // this.state.fileInfo is for the current image and we shoudl have it before we load the image + const fileInfo = this.state.fileInfo; const extension = Utils.splitFileLocation(filename).ext; const fileType = Utils.getFileType(extension); diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx new file mode 100644 index 000000000..bca203d96 --- /dev/null +++ b/web/react/pages/claim_account.jsx @@ -0,0 +1,19 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ClaimAccount from '../components/claim/claim_account.jsx'; + +function setupClaimAccountPage(props) { + ReactDOM.render( + <ClaimAccount + email={props.Email} + currentType={props.CurrentType} + newType={props.NewType} + teamName={props.TeamName} + teamDisplayName={props.TeamDisplayName} + />, + document.getElementById('claim') + ); +} + +global.window.setup_claim_account_page = setupClaimAccountPage; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 8a4cee589..e1c331aff 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -228,6 +228,40 @@ export function resetPassword(data, success, error) { track('api', 'api_users_reset_password'); } +export function switchToSSO(data, success, error) { + $.ajax({ + url: '/api/v1/users/switch_to_sso', + 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); + error(e); + } + }); + + track('api', 'api_users_switch_to_sso'); +} + +export function switchToEmail(data, success, error) { + $.ajax({ + url: '/api/v1/users/switch_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); + error(e); + } + }); + + track('api', 'api_users_switch_to_email'); +} + export function logout() { track('api', 'api_users_logout'); var currentTeamUrl = TeamStore.getCurrentTeamUrl(); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index ea4921417..0298ce533 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -117,6 +117,8 @@ export default { GITLAB_SERVICE: 'gitlab', GOOGLE_SERVICE: 'google', EMAIL_SERVICE: 'email', + SIGNIN_CHANGE: 'signin_change', + SIGNIN_VERIFIED: 'verified', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 24d27b10a..a808c9be3 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -1261,3 +1261,13 @@ export function isFeatureEnabled(feature) { export function isSystemMessage(post) { return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); } + +export function fillArray(value, length) { + const arr = []; + + for (let i = 0; i < length; i++) { + arr.push(value); + } + + return arr; +} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html new file mode 100644 index 000000000..6c9f36fa7 --- /dev/null +++ b/web/templates/claim_account.html @@ -0,0 +1,16 @@ +{{define "claim_account"}} +<!DOCTYPE html> +<html> +{{template "head" . }} +<body class="white"> + <div class="container-fluid"> + <div class="inner__wrap"> + <div class="row content" id="claim"></div> + </div> + </div> + <script> + window.setup_claim_account_page({{ .Props }}); + </script> +</body> +</html> +{{end}} diff --git a/web/web.go b/web/web.go index f1e8471b8..6e0e8df32 100644 --- a/web/web.go +++ b/web/web.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/gorilla/mux" "github.com/mattermost/platform/api" - "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -71,8 +70,7 @@ func InitWeb() { mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET") - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET") + mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") @@ -92,6 +90,7 @@ func InitWeb() { mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. @@ -565,7 +564,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { return } else { c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?verified=true&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) return } } @@ -687,89 +686,63 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { } } - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP - api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, "") + if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } } -func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { +func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") - uri := c.GetSiteURL() + "/signup/" + service + "/complete" + uri := c.GetSiteURL() + "/" + service + "/complete" - if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { c.Err = err return } else { - var user *model.User - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "") - return - } else { - user = provider.GetUserFromJson(body) - } - - if user == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "") - return - } - - suchan := api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) - euchan := api.Srv.Store.User().GetByEmail(team.Id, user.Email) - - if team.Email == "" { - team.Email = user.Email - if result := <-api.Srv.Store.Team().Update(team); result.Err != nil { - c.Err = result.Err - return + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + api.CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) } - } else { - found := true - count := 0 - for found { - if found = api.IsUsernameTaken(user.Username, team.Id); c.Err != nil { - return - } else if found { - user.Username = user.Username + strconv.Itoa(count) - count += 1 - } + break + case model.OAUTH_ACTION_LOGIN: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) + } + break } - - if result := <-suchan; result.Err == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) - return - } - - if result := <-euchan; result.Err == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) - return - } - - user.TeamId = team.Id - user.EmailVerified = true - - ruser, err := api.CreateUser(team, user) - if err != nil { - c.Err = err - return - } - - api.Login(c, w, r, ruser, "") - - if c.Err != nil { - return - } - - page := NewHtmlTemplatePage("home", "Home") - page.Team = team - page.User = ruser - page.Render(c, w) } } @@ -791,57 +764,14 @@ func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - redirectUri := c.GetSiteURL() + "/login/" + service + "/complete" - - api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, loginHint) -} - -func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/login/" + service + "/complete" + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN - if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { c.Err = err return } else { - authData := "" - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "") - return - } else { - authData = provider.GetAuthDataFromJson(body) - } - - if len(authData) == 0 { - c.Err = model.NewAppError("loginCompleteOAuth", "Could not parse auth data out of "+service+" user object", "") - return - } - - var user *model.User - if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { - c.Err = result.Err - return - } else { - user = result.Data.(*model.User) - api.Login(c, w, r, user, "") - - if c.Err != nil { - return - } - - page := NewHtmlTemplatePage("home", "Home") - page.Team = team - page.User = user - page.Render(c, w) - - root(c, w, r) - } + http.Redirect(w, r, authUrl, http.StatusFound) } } @@ -1172,3 +1102,58 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("ok")) } + +func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !CheckBrowserCompatability(c, r) { + return + } + + params := mux.Vars(r) + teamName := params["team"] + email := r.URL.Query().Get("email") + newType := r.URL.Query().Get("new_type") + + var team *model.Team + if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { + l4g.Error("Couldn't find team name=%v, err=%v", teamName, tResult.Err.Message) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) + return + } else { + team = tResult.Data.(*model.Team) + } + + authType := "" + if len(email) != 0 { + if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { + l4g.Error("Couldn't find user teamid=%v, email=%v, err=%v", team.Id, email, uResult.Err.Message) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) + return + } else { + user := uResult.Data.(*model.User) + authType = user.AuthService + + // if user is not logged in to their SSO account, ask them to log in + if len(authType) != 0 && user.Id != c.Session.UserId { + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL + stateProps["email"] = email + + if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } + } + } + } + + page := NewHtmlTemplatePage("claim_account", "Claim Account") + page.Props["Email"] = email + page.Props["CurrentType"] = authType + page.Props["NewType"] = newType + page.Props["TeamDisplayName"] = team.DisplayName + page.Props["TeamName"] = team.Name + + page.Render(c, w) +} |