From f5fec3a157e6c9146a0c4e28dd5f70e6c066affd Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 28 Aug 2015 08:37:55 -0400 Subject: Added the ability to create a team with SSO services and added the ability to turn off email sign up. --- api/team.go | 74 ++++++++++++ api/user.go | 22 +++- config/config.json | 3 +- docker/dev/config_docker.json | 13 ++- docker/local/config_docker.json | 13 ++- model/gitlab.go | 2 +- model/google.go | 6 +- model/team.go | 59 +++++++++- model/team_test.go | 59 ++++++++++ model/user.go | 60 ++++++++-- model/user_test.go | 42 +++++++ model/utils.go | 25 ----- model/utils_test.go | 44 -------- utils/config.go | 19 ++++ web/react/components/login.jsx | 80 +++++++++---- web/react/components/signup_team.jsx | 106 +++++++----------- web/react/components/signup_user_complete.jsx | 51 +++++---- web/react/components/signup_user_oauth.jsx | 87 -------------- web/react/components/team_signup_choose_auth.jsx | 88 +++++++++++++++ web/react/components/team_signup_with_email.jsx | 82 ++++++++++++++ web/react/components/team_signup_with_sso.jsx | 137 +++++++++++++++++++++++ web/react/pages/signup_team.jsx | 6 +- web/react/pages/signup_user_oauth.jsx | 11 -- web/react/utils/client.jsx | 15 +++ web/react/utils/constants.jsx | 1 + web/sass-files/sass/partials/_responsive.scss | 8 ++ web/sass-files/sass/partials/_signup.scss | 42 ++++--- web/templates/signup_team.html | 2 +- web/templates/signup_user_oauth.html | 26 ----- web/web.go | 45 ++++++-- 30 files changed, 883 insertions(+), 345 deletions(-) delete mode 100644 web/react/components/signup_user_oauth.jsx create mode 100644 web/react/components/team_signup_choose_auth.jsx create mode 100644 web/react/components/team_signup_with_email.jsx create mode 100644 web/react/components/team_signup_with_sso.jsx delete mode 100644 web/react/pages/signup_user_oauth.jsx delete mode 100644 web/templates/signup_user_oauth.html diff --git a/api/team.go b/api/team.go index eaa0d2695..d123b7dfa 100644 --- a/api/team.go +++ b/api/team.go @@ -23,6 +23,7 @@ func InitTeam(r *mux.Router) { sr := r.PathPrefix("/teams").Subrouter() sr.Handle("/create", ApiAppHandler(createTeam)).Methods("POST") sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") + sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") @@ -35,6 +36,11 @@ func InitTeam(r *mux.Router) { } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } m := model.MapFromJson(r.Body) email := strings.ToLower(strings.TrimSpace(m["email"])) @@ -74,7 +80,70 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } +func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + if !utils.IsServiceAllowed(service) { + c.SetInvalidParam("createTeamFromSSO", "service") + return + } + + team := model.TeamFromJson(r.Body) + + if team == nil { + c.SetInvalidParam("createTeamFromSSO", "team") + return + } + + team.PreSave() + + team.Name = model.CleanTeamName(team.Name) + + if err := team.IsValid(); err != nil { + c.Err = err + return + } + + team.Id = "" + + found := true + count := 0 + for found { + if found = FindTeamByName(c, team.Name, "true"); c.Err != nil { + return + } else if found { + team.Name = team.Name + strconv.Itoa(count) + count += 1 + } + } + + team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault + + if result := <-Srv.Store.Team().Save(team); result.Err != nil { + c.Err = result.Err + return + } else { + rteam := result.Data.(*model.Team) + + if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { + c.Err = nil + return + } + + data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + w.Write([]byte(model.MapToJson(data))) + + } + +} + func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } teamSignup := model.TeamSignupFromJson(r.Body) @@ -170,6 +239,11 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { } func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } team := model.TeamFromJson(r.Body) diff --git a/api/user.go b/api/user.go index 05ccd03e8..3796dde2a 100644 --- a/api/user.go +++ b/api/user.go @@ -58,6 +58,11 @@ func InitUser(r *mux.Router) { } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } user := model.UserFromJson(r.Body) @@ -181,7 +186,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { if result := <-Srv.Store.User().Save(user); result.Err != nil { c.Err = result.Err - l4g.Error("Filae err=%v", result.Err) + l4g.Error("Couldn't save the user err=%v", result.Err) return nil } else { ruser := result.Data.(*model.User) @@ -1426,3 +1431,18 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser } } + +func IsUsernameTaken(name string, teamId string) bool { + + if !model.IsValidUsername(name) { + return false + } + + if result := <-Srv.Store.User().GetByUsername(teamId, name); result.Err != nil { + return false + } else { + return true + } + + return false +} diff --git a/config/config.json b/config/config.json index 6c915e290..e0f3232ed 100644 --- a/config/config.json +++ b/config/config.json @@ -22,7 +22,8 @@ "AnalyticsUrl": "", "UseLocalStorage": true, "StorageDirectory": "./data/", - "AllowedLoginAttempts": 10 + "AllowedLoginAttempts": 10, + "AllowEmailSignUp": true }, "SSOSettings": { "gitlab": { diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 0fa51cfd4..f566cea61 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -22,16 +22,27 @@ "AnalyticsUrl": "", "UseLocalStorage": true, "StorageDirectory": "/mattermost/data/", - "AllowedLoginAttempts": 10 + "AllowedLoginAttempts": 10, + "AllowEmailSignUp": true }, "SSOSettings": { "gitlab": { "Allow": false, "Secret" : "", "Id": "", + "Scope": "", "AuthEndpoint": "", "TokenEndpoint": "", "UserApiEndpoint": "" + }, + "google": { + "Allow": false, + "Secret": "", + "Id": "", + "Scope": "email profile", + "AuthEndpoint": "https://accounts.google.com/o/oauth2/auth", + "TokenEndpoint": "https://www.googleapis.com/oauth2/v3/token", + "UserApiEndpoint": "https://www.googleapis.com/plus/v1/people/me" } }, "SqlSettings": { diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 0fa51cfd4..f566cea61 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -22,16 +22,27 @@ "AnalyticsUrl": "", "UseLocalStorage": true, "StorageDirectory": "/mattermost/data/", - "AllowedLoginAttempts": 10 + "AllowedLoginAttempts": 10, + "AllowEmailSignUp": true }, "SSOSettings": { "gitlab": { "Allow": false, "Secret" : "", "Id": "", + "Scope": "", "AuthEndpoint": "", "TokenEndpoint": "", "UserApiEndpoint": "" + }, + "google": { + "Allow": false, + "Secret": "", + "Id": "", + "Scope": "email profile", + "AuthEndpoint": "https://accounts.google.com/o/oauth2/auth", + "TokenEndpoint": "https://www.googleapis.com/oauth2/v3/token", + "UserApiEndpoint": "https://www.googleapis.com/plus/v1/people/me" } }, "SqlSettings": { diff --git a/model/gitlab.go b/model/gitlab.go index 9adcac189..d281b6ea0 100644 --- a/model/gitlab.go +++ b/model/gitlab.go @@ -23,7 +23,7 @@ type GitLabUser struct { func UserFromGitLabUser(glu *GitLabUser) *User { user := &User{} - user.Username = glu.Username + user.Username = CleanUsername(glu.Username) splitName := strings.Split(glu.Name, " ") if len(splitName) == 2 { user.FirstName = splitName[0] diff --git a/model/google.go b/model/google.go index 2a1eb3caa..bc65d0817 100644 --- a/model/google.go +++ b/model/google.go @@ -23,11 +23,6 @@ type GoogleUser struct { func UserFromGoogleUser(gu *GoogleUser) *User { user := &User{} - if len(gu.Nickname) > 0 { - user.Username = gu.Nickname - } else { - user.Username = strings.ToLower(strings.Replace(gu.DisplayName, " ", "", -1)) - } user.FirstName = gu.Names["givenName"] user.LastName = gu.Names["familyName"] user.Nickname = gu.Nickname @@ -35,6 +30,7 @@ func UserFromGoogleUser(gu *GoogleUser) *User { for _, e := range gu.Emails { if e["type"] == "account" { user.Email = e["value"] + user.Username = CleanUsername(strings.Split(user.Email, "@")[0]) } } diff --git a/model/team.go b/model/team.go index e7005625b..95e2757c8 100644 --- a/model/team.go +++ b/model/team.go @@ -5,7 +5,10 @@ package model import ( "encoding/json" + "fmt" "io" + "regexp" + "strings" ) const ( @@ -93,7 +96,7 @@ func (o *Team) IsValid() *AppError { return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) } - if !IsValidEmail(o.Email) { + if len(o.Email) > 0 && !IsValidEmail(o.Email) { return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) } @@ -140,3 +143,57 @@ func (o *Team) PreSave() { func (o *Team) PreUpdate() { o.UpdateAt = GetMillis() } + +func IsReservedTeamName(s string) bool { + s = strings.ToLower(s) + + for _, value := range reservedName { + if strings.Index(s, value) == 0 { + return true + } + } + + return false +} + +func IsValidTeamName(s string) bool { + + if !IsValidAlphaNum(s) { + return false + } + + if len(s) <= 3 { + return false + } + + return true +} + +var validTeamNameCharacter = regexp.MustCompile(`^[a-z0-9-]$`) + +func CleanTeamName(s string) string { + s = strings.ToLower(strings.Replace(s, " ", "-", -1)) + + for _, value := range reservedName { + if strings.Index(s, value) == 0 { + s = strings.Replace(s, value, "", -1) + } + } + + s = strings.TrimSpace(s) + + for _, c := range s { + char := fmt.Sprintf("%c", c) + if !validTeamNameCharacter.MatchString(char) { + s = strings.Replace(s, char, "", -1) + } + } + + s = strings.Trim(s, "-") + + if !IsValidTeamName(s) { + s = NewId() + } + + return s +} diff --git a/model/team_test.go b/model/team_test.go index 071b1a2e9..0dec07559 100644 --- a/model/team_test.go +++ b/model/team_test.go @@ -74,3 +74,62 @@ func TestTeamPreUpdate(t *testing.T) { o := Team{DisplayName: "test"} o.PreUpdate() } + +var domains = []struct { + value string + expected bool +}{ + {"spin-punch", true}, + {"-spin-punch", false}, + {"spin-punch-", false}, + {"spin_punch", false}, + {"a", false}, + {"aa", false}, + {"aaa", false}, + {"aaa-999b", true}, + {"b00b", true}, + {"b))b", false}, + {"test", true}, +} + +func TestValidTeamName(t *testing.T) { + for _, v := range domains { + if IsValidTeamName(v.value) != v.expected { + t.Errorf("expect %v as %v", v.value, v.expected) + } + } +} + +var tReservedDomains = []struct { + value string + expected bool +}{ + {"test-hello", true}, + {"test", true}, + {"admin", true}, + {"Admin-punch", true}, + {"spin-punch-admin", false}, +} + +func TestReservedTeamName(t *testing.T) { + for _, v := range tReservedDomains { + if IsReservedTeamName(v.value) != v.expected { + t.Errorf("expect %v as %v", v.value, v.expected) + } + } +} + +func TestCleanTeamName(t *testing.T) { + if CleanTeamName("Jimbo's Team") != "jimbos-team" { + t.Fatal("didn't clean name properly") + } + if len(CleanTeamName("Test")) != 26 { + t.Fatal("didn't clean name properly") + } + if CleanTeamName("Team Really cool") != "really-cool" { + t.Fatal("didn't clean name properly") + } + if CleanTeamName("super-duper-guys") != "super-duper-guys" { + t.Fatal("didn't clean name properly") + } +} diff --git a/model/user.go b/model/user.go index ebefa4762..7c53593d2 100644 --- a/model/user.go +++ b/model/user.go @@ -6,6 +6,7 @@ package model import ( "code.google.com/p/go.crypto/bcrypt" "encoding/json" + "fmt" "io" "regexp" "strings" @@ -72,13 +73,7 @@ func (u *User) IsValid() *AppError { return NewAppError("User.IsValid", "Invalid team id", "") } - if len(u.Username) == 0 || len(u.Username) > 64 { - return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id) - } - - validChars, _ := regexp.Compile("^[a-z0-9\\.\\-\\_]+$") - - if !validChars.MatchString(u.Username) { + if !IsValidUsername(u.Username) { return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id) } @@ -332,17 +327,58 @@ func ComparePassword(hash string, password string) bool { func IsUsernameValid(username string) bool { - var restrictedUsernames = []string{ - BOT_USERNAME, - "all", - "channel", + return true +} + +var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) + +var restrictedUsernames = []string{ + BOT_USERNAME, + "all", + "channel", +} + +func IsValidUsername(s string) bool { + if len(s) == 0 || len(s) > 64 { + return false + } + + if !validUsernameChars.MatchString(s) { + return false } for _, restrictedUsername := range restrictedUsernames { - if username == restrictedUsername { + if s == restrictedUsername { return false } } return true } + +func CleanUsername(s string) string { + s = strings.ToLower(strings.Replace(s, " ", "-", -1)) + + for _, value := range reservedName { + if s == value { + s = strings.Replace(s, value, "", -1) + } + } + + s = strings.TrimSpace(s) + + for _, c := range s { + char := fmt.Sprintf("%c", c) + if !validUsernameChars.MatchString(char) { + s = strings.Replace(s, char, "-", -1) + } + } + + s = strings.Trim(s, "-") + + if !IsValidUsername(s) { + s = "a" + NewId() + } + + return s +} diff --git a/model/user_test.go b/model/user_test.go index a48c3f2e7..a3b4be091 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -150,3 +150,45 @@ func TestUserGetDisplayName(t *testing.T) { t.Fatal("Display name should be nickname") } } + +var usernames = []struct { + value string + expected bool +}{ + {"spin-punch", true}, + {"Spin-punch", false}, + {"spin punch-", false}, + {"spin_punch", true}, + {"spin", true}, + {"PUNCH", false}, + {"spin.punch", true}, + {"spin'punch", false}, + {"spin*punch", false}, + {"all", false}, +} + +func TestValidUsername(t *testing.T) { + for _, v := range usernames { + if IsValidUsername(v.value) != v.expected { + t.Errorf("expect %v as %v", v.value, v.expected) + } + } +} + +func TestCleanUsername(t *testing.T) { + if CleanUsername("Spin-punch") != "spin-punch" { + t.Fatal("didn't clean name properly") + } + if CleanUsername("PUNCH") != "punch" { + t.Fatal("didn't clean name properly") + } + if CleanUsername("spin'punch") != "spin-punch" { + t.Fatal("didn't clean name properly") + } + if CleanUsername("spin") != "spin" { + t.Fatal("didn't clean name properly") + } + if len(CleanUsername("all")) != 27 { + t.Fatal("didn't clean name properly") + } +} diff --git a/model/utils.go b/model/utils.go index a8257467b..17d1c6317 100644 --- a/model/utils.go +++ b/model/utils.go @@ -168,31 +168,6 @@ var reservedName = []string{ "api", } -func IsReservedTeamName(s string) bool { - s = strings.ToLower(s) - - for _, value := range reservedName { - if strings.Index(s, value) == 0 { - return true - } - } - - return false -} - -func IsValidTeamName(s string) bool { - - if !IsValidAlphaNum(s) { - return false - } - - if len(s) <= 3 { - return false - } - - return true -} - var wwwStart = regexp.MustCompile(`^www`) var betaStart = regexp.MustCompile(`^beta`) var ciStart = regexp.MustCompile(`^ci`) diff --git a/model/utils_test.go b/model/utils_test.go index dbb448882..0f26526b2 100644 --- a/model/utils_test.go +++ b/model/utils_test.go @@ -66,50 +66,6 @@ func TestValidLower(t *testing.T) { } } -var domains = []struct { - value string - expected bool -}{ - {"spin-punch", true}, - {"-spin-punch", false}, - {"spin-punch-", false}, - {"spin_punch", false}, - {"a", false}, - {"aa", false}, - {"aaa", false}, - {"aaa-999b", true}, - {"b00b", true}, - {"b))b", false}, - {"test", true}, -} - -func TestValidTeamName(t *testing.T) { - for _, v := range domains { - if IsValidTeamName(v.value) != v.expected { - t.Errorf("expect %v as %v", v.value, v.expected) - } - } -} - -var tReservedDomains = []struct { - value string - expected bool -}{ - {"test-hello", true}, - {"test", true}, - {"admin", true}, - {"Admin-punch", true}, - {"spin-punch-admin", false}, -} - -func TestReservedTeamName(t *testing.T) { - for _, v := range tReservedDomains { - if IsReservedTeamName(v.value) != v.expected { - t.Errorf("expect %v as %v", v.value, v.expected) - } - } -} - func TestEtag(t *testing.T) { etag := Etag("hello", 24) if len(etag) <= 0 { diff --git a/utils/config.go b/utils/config.go index 36301264c..36193412b 100644 --- a/utils/config.go +++ b/utils/config.go @@ -31,6 +31,7 @@ type ServiceSettings struct { UseLocalStorage bool StorageDirectory string AllowedLoginAttempts int + AllowEmailSignUp bool } type SSOSetting struct { @@ -277,5 +278,23 @@ func GetAllowedAuthServices() []string { } } + if Cfg.ServiceSettings.AllowEmailSignUp { + authServices = append(authServices, "email") + } + return authServices } + +func IsServiceAllowed(s string) bool { + if len(s) == 0 { + return false + } + + if service, ok := Cfg.SSOSettings[s]; ok { + if service.Allow { + return true + } + } + + return false +} diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 678a2ff87..489ff6960 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -10,7 +10,9 @@ var Constants = require('../utils/constants.jsx'); export default class Login extends React.Component { constructor(props) { super(props); + this.handleSubmit = this.handleSubmit.bind(this); + this.state = {}; } handleSubmit(e) { @@ -96,19 +98,28 @@ export default class Login extends React.Component { var authServices = JSON.parse(this.props.authServices); var loginMessage = []; - if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) { + if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) { loginMessage.push( -
- {'Log in with GitLab'} -
- ); + + + with GitLab + + ); } - if (authServices.indexOf(Constants.GOOGLE_SERVICE) >= 0) { + + if (authServices.indexOf(Constants.GOOGLE_SERVICE) !== -1) { loginMessage.push( -
- {'Log in with Google'} -
- ); + + + with Google Apps + + ); } var errorClass = ''; @@ -116,15 +127,10 @@ export default class Login extends React.Component { errorClass = ' has-error'; } - return ( -
-
Sign in to:
-

{teamDisplayName}

-

on {config.SiteName}

-
-
- {serverError} -
+ var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( +
+
+ ); + } + + var forgotPassword; + if (loginMessage.length > 0 && emailSignup) { + loginMessage = ( +
+ {loginMessage} +
+ or +
+
+ ); + + forgotPassword = ( +
+ I forgot my password +
+ ); + } + + return ( +
+
Sign in to:
+

{teamDisplayName}

+

on {config.SiteName}

+ +
+ {serverError} +
{loginMessage} + {emailSignup}
{'Find other ' + strings.TeamPlural}
-
- I forgot my password -
+ {forgotPassword}
{'Want to create your own ' + strings.Team + '? '} ; + } else if (this.state.page === 'service' && this.state.service !== '') { + return ; + } else { + return ( + + ); } - - client.signupTeam(team.email, - function(data) { - if (data["follow_link"]) { - window.location.href = data["follow_link"]; - } - else { - window.location.href = "/signup_team_confirm/?email=" + encodeURIComponent(team.email); - } - }.bind(this), - function(err) { - state.server_error = err.message; - this.setState(state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - - var email_error = this.state.email_error ? : null; - var server_error = this.state.server_error ?
: null; - - return ( - -
- - { email_error } -
- { server_error } -
- -
-
- - ); } -}); - - +} + +TeamSignUp.defaultProps = { + services: [] +}; +TeamSignUp.propTypes = { + services: React.PropTypes.array +}; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 0393e0413..e5c602c16 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -171,7 +171,35 @@ module.exports = React.createClass({ ); } - if (signupMessage.length > 0) { + var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( +
+
+ {email} + {yourEmailIs} +
+
Choose your username
+
+ + {nameError} +

Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'

+
+
+
+
Choose your password
+
+ + {passwordError} +
+
+
+

+
+ ); + } + + if (signupMessage.length > 0 && emailSignup) { signupMessage = (
{signupMessage} @@ -196,26 +224,7 @@ module.exports = React.createClass({

on {config.SiteName}

Let's create your account

{signupMessage} -
- {email} - {yourEmailIs} -
-
Choose your username
-
- - {nameError} -

Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'

-
-
-
-
Choose your password
-
- - {passwordError} -
-
-
-

+ {emailSignup} {serverError} {termsDisclaimer} diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx deleted file mode 100644 index 8b2800bde..000000000 --- a/web/react/components/signup_user_oauth.jsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); - -module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - - if (!this.state.user.username) { - this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""}); - return; - } - - var username_error = utils.isValidUsername(this.state.user.username); - if (username_error === "Cannot use a reserved word as a username.") { - this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); - return; - } else if (username_error) { - this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""}); - return; - } - - this.setState({name_error: "", server_error: ""}); - - this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked; - - var user = this.state.user; - client.createUser(user, "", "", - function(data) { - client.track('signup', 'signup_user_oauth_02'); - UserStore.setCurrentUser(data); - UserStore.setLastEmail(data.email); - - window.location.href = '/' + this.props.teamName + '/login/' + user.auth_service + '?login_hint=' + user.email; - }.bind(this), - function(err) { - this.state.server_error = err.message; - this.setState(this.state); - }.bind(this) - ); - }, - handleChange: function() { - var user = this.state.user; - user.username = this.refs.name.getDOMNode().value; - this.setState({ user: user }); - }, - getInitialState: function() { - var user = JSON.parse(this.props.user); - return { user: user }; - }, - render: function() { - - client.track('signup', 'signup_user_oauth_01'); - - var name_error = this.state.name_error ? : null; - var server_error = this.state.server_error ?
: null; - - var yourEmailIs = this.state.user.email == "" ? "" : Your email address is { this.state.user.email }.; - - return ( -
- -

Welcome to { config.SiteName }

-

{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}

-

Your username can be made of lowercase letters and numbers.

- -
- - { name_error } -
-

{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}

-

{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.

-
-

- { server_error } -

By proceeding to create your account and use { config.SiteName }, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {config.SiteName}.

-
- ); - } -}); - - diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx new file mode 100644 index 000000000..2d35785c2 --- /dev/null +++ b/web/react/components/team_signup_choose_auth.jsx @@ -0,0 +1,88 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../utils/constants.jsx'); + +export default class ChooseAuthPage extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + var buttons = []; + if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) { + buttons.push( + + + Create new {strings.Team} with GitLab Account + + ); + } + + if (this.props.services.indexOf(Constants.GOOGLE_SERVICE) !== -1) { + buttons.push( + + + Create new {strings.Team} with Google Apps Account + + ); + } + + if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) { + buttons.push( + + + Create new {strings.Team} with email address + + ); + } + + if (buttons.length === 0) { + buttons = No sign-up methods configured, please contact your system administrator.; + } + + return ( +
+ {buttons} + +
+ ); + } +} + +ChooseAuthPage.defaultProps = { + services: [] +}; +ChooseAuthPage.propTypes = { + services: React.PropTypes.array, + updatePage: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx new file mode 100644 index 000000000..c7204880f --- /dev/null +++ b/web/react/components/team_signup_with_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); + +export default class EmailSignUpPage extends React.Component { + constructor() { + super(); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = {}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = {serverError: ''}; + + team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); + if (!team.email || !utils.isEmail(team.email)) { + state.emailError = 'Please enter a valid email address'; + state.inValid = true; + } else { + state.emailError = ''; + } + + if (state.inValid) { + this.setState(state); + return; + } + + client.signupTeam(team.email, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(team.email); + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + render() { + return ( +
+
+ +
+
+ +
+ +
+ ); + } +} + +EmailSignUpPage.defaultProps = { +}; +EmailSignUpPage.propTypes = { +}; diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx new file mode 100644 index 000000000..57996d7cc --- /dev/null +++ b/web/react/components/team_signup_with_sso.jsx @@ -0,0 +1,137 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var Constants = require('../utils/constants.jsx'); + +export default class SSOSignUpPage extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.nameChange = this.nameChange.bind(this); + + this.state = {name: ''}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = this.state; + state.nameError = null; + state.serverError = null; + + team.display_name = this.state.name; + + if (team.display_name.length <= 3) { + return; + } + + if (!team.display_name) { + state.nameError = 'Please enter a team name'; + this.setState(state); + return; + } + + team.name = utils.cleanUpUrlable(team.display_name); + team.type = 'O'; + + client.createTeamWithSSO(team, + this.props.service, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/'; + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + nameChange() { + this.setState({name: this.refs.teamname.getDOMNode().value.trim()}); + } + render() { + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = ; + nameDivClass += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError =
; + } + + var disabled = false; + if (this.state.name.length <= 3) { + disabled = true; + } + + var button = null; + + if (this.props.service === Constants.GITLAB_SERVICE) { + button = ( + + + Create {strings.Team} with GitLab Account + + ); + } else if (this.props.service === Constants.GOOGLE_SERVICE) { + button = ( + + + Create {strings.Team} with Google Apps Account + + ); + } + + return ( +
+
+ + {nameError} +
+
+ {button} + {serverError} +
+ +
+ ); + } +} + +SSOSignUpPage.defaultProps = { + service: '' +}; +SSOSignUpPage.propTypes = { + service: React.PropTypes.string +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index 37c441d4f..4b58025ac 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -5,11 +5,13 @@ var SignupTeam = require('../components/signup_team.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -global.window.setup_signup_team_page = function() { +global.window.setup_signup_team_page = function(authServices) { AsyncClient.getConfig(); + var services = JSON.parse(authServices); + React.render( - , + , document.getElementById('signup-team') ); }; diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx deleted file mode 100644 index 6a0707702..000000000 --- a/web/react/pages/signup_user_oauth.jsx +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var SignupUserOAuth = require('../components/signup_user_oauth.jsx'); - -global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) { - React.render( - , - document.getElementById('signup-user-complete') - ); -}; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 70220c71e..082f82a08 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -70,6 +70,21 @@ module.exports.createTeamFromSignup = function(teamSignup, success, error) { }); }; +module.exports.createTeamWithSSO = function(team, service, success, error) { + $.ajax({ + url: '/api/v1/teams/create_with_sso/' + service, + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(team), + success: success, + error: function onError(xhr, status, err) { + var e = handleError('createTeamWithSSO', xhr, status, err); + error(e); + } + }); +}; + module.exports.createUser = function(user, data, emailHash, success, error) { $.ajax({ url: '/api/v1/users/create?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash), diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 82fc3da22..6678790e2 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -61,6 +61,7 @@ module.exports = { OFFTOPIC_CHANNEL: 'off-topic', GITLAB_SERVICE: 'gitlab', GOOGLE_SERVICE: 'google', + EMAIL_SERVICE: 'email', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_LOADING: 'loading', diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 733d81c2b..682809f02 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -290,6 +290,14 @@ .signup-team__name { font-size: 2em; } + .btn.btn-full { + padding-left: 10px; + } + .btn { + .icon, .fa { + margin-right: 6px; + } + } } .modal { .info__label { diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index ddf2aab88..2fb56e537 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -156,9 +156,21 @@ } .btn { + font-size: 1em; padding: em(7px) em(15px); font-weight: 600; margin-right: 5px; + .fa { + font-size: 17px; + margin-right: 8px; + } + .icon { + width: 18px; + height: 18px; + margin-right: 8px; + @include background-size(100% 100%); + display: inline-block; + } &.btn-custom-login { display: block; min-width: 200px; @@ -166,7 +178,7 @@ padding: 0 1em; margin: 1em auto; height: 40px; - line-height: 35px; + line-height: 34px; color: #fff; @include border-radius(2px); &.gitlab { @@ -178,12 +190,7 @@ vertical-align: middle; } .icon { - background: url("../images/gitlabLogo.png"); - width: 18px; - height: 18px; - margin-right: 8px; - @include background-size(100% 100%); - display: inline-block; + background-image: url("../images/gitlabLogo.png"); } } &.google { @@ -195,13 +202,22 @@ vertical-align: middle; } .icon { - background: url("../images/googleLogo.png"); - width: 18px; - height: 18px; - margin-right: 8px; - @include background-size(100% 100%); - display: inline-block; + background-image: url("../images/googleLogo.png"); + } + } + &.email { + background: #2389D7; + &:hover { + background: darken(#2389D7, 10%); } + span { + vertical-align: middle; + } + } + &.btn-full { + width: 100%; + text-align: left; + padding-left: 35px; } } &.btn-default { diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html index b896dedf5..8d9d6e0b8 100644 --- a/web/templates/signup_team.html +++ b/web/templates/signup_team.html @@ -22,7 +22,7 @@
diff --git a/web/templates/signup_user_oauth.html b/web/templates/signup_user_oauth.html deleted file mode 100644 index 2eddb50d2..000000000 --- a/web/templates/signup_user_oauth.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_user_oauth"}} - - -{{template "head" . }} - -
-
-
-
- -
- -
- -
-
- - - -{{end}} diff --git a/web/web.go b/web/web.go index dc2b5dced..03dbdde6a 100644 --- a/web/web.go +++ b/web/web.go @@ -145,6 +145,7 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if len(c.Session.UserId) == 0 { page := NewHtmlTemplatePage("signup_team", "Signup") + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } else { page := NewHtmlTemplatePage("home", "Home") @@ -160,6 +161,7 @@ func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("signup_team", "Signup") + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -529,23 +531,52 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) return } - if result := <-api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service); result.Err == nil { + 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 + } + } 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 + } + } + } + + 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 := <-api.Srv.Store.User().GetByEmail(team.Id, user.Email); result.Err == 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 } user.TeamId = team.Id - page := NewHtmlTemplatePage("signup_user_oauth", "Complete User Sign Up") - page.Props["User"] = user.ToJson() - page.Props["TeamName"] = team.Name - page.Props["TeamDisplayName"] = team.DisplayName - page.Render(c, w) + ruser := api.CreateUser(c, team, user) + if c.Err != nil { + return + } + + api.Login(c, w, r, ruser, "") + + if c.Err != nil { + return + } + + root(c, w, r) } } -- cgit v1.2.3-1-g7c22