Merge branch 'master' into mm-1420

Этот коммит содержится в:
=Corey Hulen
2015-07-23 09:22:17 -08:00
родитель c277a98b5d a9d48ff994
Коммит 8d9449c068
43 изменённых файлов: 925 добавлений и 260 удалений

Просмотреть файл

@@ -18,7 +18,7 @@
<tr> <tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2> <h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
<p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif;">{{.Props.PostMessage}}</pre></p> <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p>
<p style="margin: 20px 0 15px"> <p style="margin: 20px 0 15px">
<a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a> <a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
</p> </p>

Просмотреть файл

@@ -20,6 +20,7 @@ import (
_ "image/gif" _ "image/gif"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@@ -80,36 +81,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("h") hash := r.URL.Query().Get("h")
shouldVerifyHash := true if IsVerifyHashRequired(user, team, hash) {
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 {
domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
matched := false
for _, d := range domains {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
if matched {
shouldVerifyHash = false
} else {
c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "allowed domains failed")
return
}
}
if team.Type == model.TEAM_OPEN {
shouldVerifyHash = false
}
if len(hash) > 0 {
shouldVerifyHash = true
}
if shouldVerifyHash {
data := r.URL.Query().Get("d") data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data)) props := model.MapFromJson(strings.NewReader(data))
@@ -133,6 +105,10 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
user.EmailVerified = true user.EmailVerified = true
} }
if len(user.AuthData) > 0 && len(user.AuthService) > 0 {
user.EmailVerified = true
}
ruser := CreateUser(c, team, user) ruser := CreateUser(c, team, user)
if c.Err != nil { if c.Err != nil {
return return
@@ -142,6 +118,38 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
shouldVerifyHash := true
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil {
domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
matched := false
for _, d := range domains {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
if matched {
shouldVerifyHash = false
} else {
return true
}
}
if team.Type == model.TEAM_OPEN {
shouldVerifyHash = false
}
if len(hash) > 0 {
shouldVerifyHash = true
}
return shouldVerifyHash
}
func CreateValet(c *Context, team *model.Team) *model.User { func CreateValet(c *Context, team *model.Team) *model.User {
valet := &model.User{} valet := &model.User{}
valet.TeamId = team.Id valet.TeamId = team.Id
@@ -233,80 +241,77 @@ func FireAndForgetVerifyEmail(userId, name, email, teamDisplayName, teamURL stri
}() }()
} }
func login(c *Context, w http.ResponseWriter, r *http.Request) { func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User {
props := model.MapFromJson(r.Body) if result := <-Srv.Store.User().Get(userId); result.Err != nil {
c.Err = result.Err
extraInfo := "" return nil
var result store.StoreResult } else {
user := result.Data.(*model.User)
if len(props["id"]) != 0 { if checkUserPassword(c, user, password) {
extraInfo = props["id"] Login(c, w, r, user, deviceId)
if result = <-Srv.Store.User().Get(props["id"]); result.Err != nil { return user
c.Err = result.Err
return
} }
} }
return nil
}
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User {
var team *model.Team var team *model.Team
if result.Data == nil && len(props["email"]) != 0 && len(props["name"]) != 0 {
extraInfo = props["email"] + " in " + props["name"]
if nr := <-Srv.Store.Team().GetByName(props["name"]); nr.Err != nil { if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
c.Err = nr.Err c.Err = result.Err
return return nil
} else { } else {
team = nr.Data.(*model.Team) team = result.Data.(*model.Team)
}
if result = <-Srv.Store.User().GetByEmail(team.Id, props["email"]); result.Err != nil { if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return nil
} } else {
user := result.Data.(*model.User)
if checkUserPassword(c, user, password) {
Login(c, w, r, user, deviceId)
return user
} }
} }
if result.Data == nil { return nil
c.Err = model.NewAppError("login", "Login failed because we couldn't find a valid account", extraInfo) }
c.Err.StatusCode = http.StatusBadRequest
return func checkUserPassword(c *Context, user *model.User, password string) bool {
} if !model.ComparePassword(user.Password, password) {
c.LogAuditWithUserId(user.Id, "fail")
user := result.Data.(*model.User) c.Err = model.NewAppError("checkUserPassword", "Login failed because of invalid password", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden
if team == nil { return false
if tResult := <-Srv.Store.Team().Get(user.TeamId); tResult.Err != nil {
c.Err = tResult.Err
return
} else {
team = tResult.Data.(*model.Team)
}
} }
return true
}
// User MUST be validated before calling Login
func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
c.LogAuditWithUserId(user.Id, "attempt") c.LogAuditWithUserId(user.Id, "attempt")
if !model.ComparePassword(user.Password, props["password"]) {
c.LogAuditWithUserId(user.Id, "fail")
c.Err = model.NewAppError("login", "Login failed because of invalid password", extraInfo)
c.Err.StatusCode = http.StatusForbidden
return
}
if !user.EmailVerified && !utils.Cfg.EmailSettings.ByPassEmail { if !user.EmailVerified && !utils.Cfg.EmailSettings.ByPassEmail {
c.Err = model.NewAppError("login", "Login failed because email address has not been verified", extraInfo) c.Err = model.NewAppError("Login", "Login failed because email address has not been verified", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
return return
} }
if user.DeleteAt > 0 { if user.DeleteAt > 0 {
c.Err = model.NewAppError("login", "Login failed because your account has been set to inactive. Please contact an administrator.", extraInfo) c.Err = model.NewAppError("Login", "Login failed because your account has been set to inactive. Please contact an administrator.", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
return return
} }
session := &model.Session{UserId: user.Id, TeamId: team.Id, Roles: user.Roles, DeviceId: props["device_id"]} session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId}
maxAge := model.SESSION_TIME_WEB_IN_SECS maxAge := model.SESSION_TIME_WEB_IN_SECS
if len(props["device_id"]) > 0 { if len(deviceId) > 0 {
session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS) session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS)
maxAge = model.SESSION_TIME_MOBILE_IN_SECS maxAge = model.SESSION_TIME_MOBILE_IN_SECS
} else { } else {
@@ -357,12 +362,41 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
} }
http.SetCookie(w, sessionCookie) http.SetCookie(w, sessionCookie)
user.Sanitize(map[string]bool{})
c.Session = *session c.Session = *session
c.LogAuditWithUserId(user.Id, "success") c.LogAuditWithUserId(user.Id, "success")
}
w.Write([]byte(result.Data.(*model.User).ToJson())) func login(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
if len(props["password"]) == 0 {
c.Err = model.NewAppError("login", "Password field must not be blank", "")
c.Err.StatusCode = http.StatusForbidden
return
}
var user *model.User
if len(props["id"]) != 0 {
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
} else {
c.Err = model.NewAppError("login", "Either user id or team name and user email must be provided", "")
c.Err.StatusCode = http.StatusForbidden
return
}
if c.Err != nil {
return
}
if user != nil {
user.Sanitize(map[string]bool{})
} else {
user = &model.User{}
}
w.Write([]byte(user.ToJson()))
} }
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -793,6 +827,13 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
tchan := Srv.Store.Team().Get(user.TeamId) tchan := Srv.Store.Team().Get(user.TeamId)
if user.AuthData != "" {
c.LogAudit("failed - tried to update user password who was logged in through oauth")
c.Err = model.NewAppError("updatePassword", "Update password failed because the user is logged in through an OAuth service", "auth_service="+user.AuthService)
c.Err.StatusCode = http.StatusForbidden
return
}
if !model.ComparePassword(user.Password, currentPassword) { if !model.ComparePassword(user.Password, currentPassword) {
c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "") c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "")
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
@@ -1212,3 +1253,72 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
} }
func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri string) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
c.Err.StatusCode = http.StatusBadRequest
return
}
clientId := utils.Cfg.SSOSettings[service].Id
endpoint := utils.Cfg.SSOSettings[service].AuthEndpoint
state := model.HashPassword(clientId)
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri+"?team="+teamName) + "&state=" + url.QueryEscape(state)
http.Redirect(w, r, authUrl, http.StatusFound)
}
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.AppError) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
return nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
}
if !model.ComparePassword(state, utils.Cfg.SSOSettings[service].Id) {
return nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
}
p := url.Values{}
p.Set("client_id", utils.Cfg.SSOSettings[service].Id)
p.Set("client_secret", utils.Cfg.SSOSettings[service].Secret)
p.Set("code", code)
p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
p.Set("redirect_uri", redirectUri)
client := &http.Client{}
req, _ := http.NewRequest("POST", utils.Cfg.SSOSettings[service].TokenEndpoint, strings.NewReader(p.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
var ar *model.AccessResponse
if resp, err := client.Do(req); err != nil {
return nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error())
} else {
ar = model.AccessResponseFromJson(resp.Body)
}
if ar.TokenType != model.ACCESS_TOKEN_TYPE {
return nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType)
}
if len(ar.AccessToken) == 0 {
return nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "")
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
req, _ = http.NewRequest("GET", utils.Cfg.SSOSettings[service].UserApiEndpoint, strings.NewReader(""))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
if resp, err := client.Do(req); err != nil {
return nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error())
} else {
return resp.Body, nil
}
}

Просмотреть файл

@@ -23,6 +23,16 @@
"UseLocalStorage": true, "UseLocalStorage": true,
"StorageDirectory": "./data/" "StorageDirectory": "./data/"
}, },
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test", "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test",

Просмотреть файл

@@ -23,6 +23,16 @@
"UseLocalStorage": true, "UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/" "StorageDirectory": "/mattermost/data/"
}, },
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test", "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test",

41
model/access.go Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
ACCESS_TOKEN_GRANT_TYPE = "authorization_code"
ACCESS_TOKEN_TYPE = "bearer"
)
type AccessResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
func (ar *AccessResponse) ToJson() string {
b, err := json.Marshal(ar)
if err != nil {
return ""
} else {
return string(b)
}
}
func AccessResponseFromJson(data io.Reader) *AccessResponse {
decoder := json.NewDecoder(data)
var ar AccessResponse
err := decoder.Decode(&ar)
if err == nil {
return &ar
} else {
return nil
}
}

Просмотреть файл

@@ -8,22 +8,24 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"regexp" "regexp"
"strconv"
"strings" "strings"
) )
const ( const (
ROLE_ADMIN = "admin" ROLE_ADMIN = "admin"
ROLE_SYSTEM_ADMIN = "system_admin" ROLE_SYSTEM_ADMIN = "system_admin"
ROLE_SYSTEM_SUPPORT = "system_support" ROLE_SYSTEM_SUPPORT = "system_support"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline" USER_OFFLINE = "offline"
USER_AWAY = "away" USER_AWAY = "away"
USER_ONLINE = "online" USER_ONLINE = "online"
USER_NOTIFY_ALL = "all" USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention" USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none" USER_NOTIFY_NONE = "none"
BOT_USERNAME = "valet" BOT_USERNAME = "valet"
USER_AUTH_SERVICE_GITLAB = "gitlab"
) )
type User struct { type User struct {
@@ -35,6 +37,7 @@ type User struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
AuthData string `json:"auth_data"` AuthData string `json:"auth_data"`
AuthService string `json:"auth_service"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
@@ -50,6 +53,13 @@ type User struct {
LastPictureUpdate int64 `json:"last_picture_update"` LastPictureUpdate int64 `json:"last_picture_update"`
} }
type GitLabUser struct {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
}
// IsValid validates the user and returns an error if it isn't configured // IsValid validates the user and returns an error if it isn't configured
// correctly. // correctly.
func (u *User) IsValid() *AppError { func (u *User) IsValid() *AppError {
@@ -96,6 +106,22 @@ func (u *User) IsValid() *AppError {
return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id) return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id)
} }
if len(u.Password) > 128 {
return NewAppError("User.IsValid", "Invalid password", "user_id="+u.Id)
}
if len(u.AuthData) > 128 {
return NewAppError("User.IsValid", "Invalid auth data", "user_id="+u.Id)
}
if len(u.AuthData) > 0 && len(u.AuthService) == 0 {
return NewAppError("User.IsValid", "Invalid user, auth data must be set with auth type", "user_id="+u.Id)
}
if len(u.Password) > 0 && len(u.AuthData) > 0 {
return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id)
}
return nil return nil
} }
@@ -328,3 +354,38 @@ func IsUsernameValid(username string) bool {
return true return true
} }
func UserFromGitLabUser(glu *GitLabUser) *User {
user := &User{}
user.Username = glu.Username
splitName := strings.Split(glu.Name, " ")
if len(splitName) == 2 {
user.FirstName = splitName[0]
user.LastName = splitName[1]
} else if len(splitName) >= 2 {
user.FirstName = splitName[0]
user.LastName = strings.Join(splitName[1:], " ")
} else {
user.FirstName = glu.Name
}
user.Email = glu.Email
user.AuthData = strconv.FormatInt(glu.Id, 10)
user.AuthService = USER_AUTH_SERVICE_GITLAB
return user
}
func GitLabUserFromJson(data io.Reader) *GitLabUser {
decoder := json.NewDecoder(data)
var glu GitLabUser
err := decoder.Decode(&glu)
if err == nil {
return &glu
} else {
return nil
}
}
func (glu *GitLabUser) GetAuthData() string {
return strconv.FormatInt(glu.Id, 10)
}

Просмотреть файл

@@ -23,6 +23,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("Username").SetMaxSize(64) table.ColMap("Username").SetMaxSize(64)
table.ColMap("Password").SetMaxSize(128) table.ColMap("Password").SetMaxSize(128)
table.ColMap("AuthData").SetMaxSize(128) table.ColMap("AuthData").SetMaxSize(128)
table.ColMap("AuthService").SetMaxSize(32)
table.ColMap("Email").SetMaxSize(128) table.ColMap("Email").SetMaxSize(128)
table.ColMap("Nickname").SetMaxSize(64) table.ColMap("Nickname").SetMaxSize(64)
table.ColMap("FirstName").SetMaxSize(64) table.ColMap("FirstName").SetMaxSize(64)
@@ -56,6 +57,8 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
panic("Failed to set last name from nickname " + err.Error()) panic("Failed to set last name from nickname " + err.Error())
} }
} }
us.CreateColumnIfNotExists("Users", "AuthService", "AuthData", "varchar(32)", "") // for OAuth Client
} }
//func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool { //func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool {
@@ -371,6 +374,28 @@ func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel {
return storeChannel return storeChannel
} }
func (us SqlUserStore) GetByAuth(teamId string, authData string, authService string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
user := model.User{}
if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND AuthData=? AND AuthService=?", teamId, authData, authService); err != nil {
result.Err = model.NewAppError("SqlUserStore.GetByAuth", "We couldn't find the existing account", "teamId="+teamId+", authData="+authData+", authService="+authService+", "+err.Error())
}
result.Data = &user
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel { func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

Просмотреть файл

@@ -236,6 +236,25 @@ func TestUserStoreGetByEmail(t *testing.T) {
} }
} }
func TestUserStoreGetByAuthData(t *testing.T) {
Setup()
u1 := model.User{}
u1.TeamId = model.NewId()
u1.Email = model.NewId()
u1.AuthData = "123"
u1.AuthService = "service"
Must(store.User().Save(&u1))
if err := (<-store.User().GetByAuth(u1.TeamId, u1.AuthData, u1.AuthService)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.User().GetByAuth("", "", "")).Err; err == nil {
t.Fatal("Should have failed because of missing auth data")
}
}
func TestUserStoreGetByUsername(t *testing.T) { func TestUserStoreGetByUsername(t *testing.T) {
Setup() Setup()

Просмотреть файл

@@ -85,6 +85,7 @@ type UserStore interface {
Get(id string) StoreChannel Get(id string) StoreChannel
GetProfiles(teamId string) StoreChannel GetProfiles(teamId string) StoreChannel
GetByEmail(teamId string, email string) StoreChannel GetByEmail(teamId string, email string) StoreChannel
GetByAuth(teamId string, authData string, authService string) StoreChannel
GetByUsername(teamId string, username string) StoreChannel GetByUsername(teamId string, username string) StoreChannel
VerifyEmail(userId string) StoreChannel VerifyEmail(userId string) StoreChannel
GetEtagForProfiles(teamId string) StoreChannel GetEtagForProfiles(teamId string) StoreChannel

Просмотреть файл

@@ -32,6 +32,15 @@ type ServiceSettings struct {
StorageDirectory string StorageDirectory string
} }
type SSOSetting struct {
Allow bool
Secret string
Id string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct { type SqlSettings struct {
DriverName string DriverName string
DataSource string DataSource string
@@ -109,6 +118,7 @@ type Config struct {
EmailSettings EmailSettings EmailSettings EmailSettings
PrivacySettings PrivacySettings PrivacySettings PrivacySettings
TeamSettings TeamSettings TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
} }
func (o *Config) ToJson() string { func (o *Config) ToJson() string {
@@ -243,3 +253,14 @@ func IsS3Configured() bool {
return true return true
} }
func GetAllowedAuthServices() []string {
authServices := []string{}
for name, service := range Cfg.SSOSettings {
if service.Allow {
authServices = append(authServices, name)
}
}
return authServices
}

Просмотреть файл

@@ -64,7 +64,7 @@ module.exports = React.createClass({
<div>{"URL: " + currentAudit.action.replace("/api/v1", "")}</div> <div>{"URL: " + currentAudit.action.replace("/api/v1", "")}</div>
</div> </div>
: :
<a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> <a href="#" className="theme" onClick={this.handleMoreInfo.bind(this, i)}>More info</a>
} }
</div> </div>
{i < this.state.audits.length - 1 ? {i < this.state.audits.length - 1 ?

Просмотреть файл

@@ -68,6 +68,9 @@ module.exports = React.createClass({
else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") {
devicePicture = "fa fa-apple"; devicePicture = "fa fa-apple";
} }
else if (currentSession.props.platform === "Linux") {
devicePicture = "fa fa-linux";
}
activityList[i] = ( activityList[i] = (
<div className="activity-log__table"> <div className="activity-log__table">
@@ -83,7 +86,7 @@ module.exports = React.createClass({
<div>{"Session ID: " + currentSession.alt_id}</div> <div>{"Session ID: " + currentSession.alt_id}</div>
</div> </div>
: :
<a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> <a className="theme" href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a>
} }
</div> </div>
</div> </div>

Просмотреть файл

@@ -20,12 +20,7 @@ module.exports = React.createClass({
}, },
getSuggestedCommands: function(cmd) { getSuggestedCommands: function(cmd) {
if (cmd == "") { if (!cmd || cmd.charAt(0) != '/') {
this.setState({ suggestions: [ ], cmd: "" });
return;
}
if (cmd.indexOf("/") != 0) {
this.setState({ suggestions: [ ], cmd: "" }); this.setState({ suggestions: [ ], cmd: "" });
return; return;
} }
@@ -35,17 +30,19 @@ module.exports = React.createClass({
cmd, cmd,
true, true,
function(data) { function(data) {
if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) data.suggestions = []; if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) {
data.suggestions = [];
}
this.setState({ suggestions: data.suggestions, cmd: cmd }); this.setState({ suggestions: data.suggestions, cmd: cmd });
}.bind(this), }.bind(this),
function(err){ function(err){
}.bind(this) }
); );
}, },
render: function() { render: function() {
if (this.state.suggestions.length == 0) return (<div/>); if (this.state.suggestions.length == 0) return (<div/>);
var suggestions = [] var suggestions = [];
for (var i = 0; i < this.state.suggestions.length; i++) { for (var i = 0; i < this.state.suggestions.length; i++) {
if (this.state.suggestions[i].suggestion != this.state.cmd) { if (this.state.suggestions[i].suggestion != this.state.cmd) {
@@ -59,7 +56,7 @@ module.exports = React.createClass({
} }
return ( return (
<div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions*37)+2}}> <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*37)+2}}>
{ suggestions } { suggestions }
</div> </div>
); );

Просмотреть файл

@@ -8,21 +8,25 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes; var ActionTypes = Constants.ActionTypes;
function getStateFromStores() { function getStateFromStores() {
var error = ErrorStore.getLastError(); var error = ErrorStore.getLastError();
if (error && error.message !== "There appears to be a problem with your internet connection") { if (error && error.message !== "There appears to be a problem with your internet connection") {
return { message: error.message }; return { message: error.message };
} else { } else {
return { message: null }; return { message: null };
} }
} }
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ErrorBar',
componentDidMount: function() { componentDidMount: function() {
ErrorStore.addChangeListener(this._onChange); ErrorStore.addChangeListener(this._onChange);
$('body').css('padding-top', $('#error_bar').outerHeight()); $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
$(window).resize(function(){ $(window).resize(function() {
$('body').css('padding-top', $('#error_bar').outerHeight()); if (this.state.message) {
}); $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
}
}.bind(this));
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
ErrorStore.removeChangeListener(this._onChange); ErrorStore.removeChangeListener(this._onChange);
@@ -31,39 +35,39 @@ module.exports = React.createClass({
var newState = getStateFromStores(); var newState = getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) { if (!utils.areStatesEqual(newState, this.state)) {
if (newState.message) { if (newState.message) {
var self = this; setTimeout(this.handleClose, 10000);
setTimeout(function(){self.handleClose();}, 10000);
} }
this.setState(newState); this.setState(newState);
} }
}, },
handleClose: function(e) { handleClose: function(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
AppDispatcher.handleServerAction({ AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ERROR, type: ActionTypes.RECIEVED_ERROR,
err: null err: null
}); });
$('body').css('padding-top', '0'); $('body').css('padding-top', '0');
}, },
getInitialState: function() { getInitialState: function() {
var state = getStateFromStores(); var state = getStateFromStores();
if (state.message) { if (state.message) {
var self = this; setTimeout(this.handleClose, 10000);
setTimeout(function(){self.handleClose();}, 10000);
} }
return state; return state;
}, },
render: function() { render: function() {
var message = this.state.message; if (this.state.message) {
if (message) {
return ( return (
<div className="error-bar"> <div className="error-bar">
<span className="error-text">{message}</span> <span>{this.state.message}</span>
<a href="#" className="error-close pull-right" onClick={this.handleClose}>×</a> <a href="#" className="error-bar__close" onClick={this.handleClose}>&times;</a>
</div> </div>
); );
} else { } else {
return <div/>; return <div/>;
} }
} }
}); });

Просмотреть файл

@@ -90,6 +90,17 @@ module.exports = React.createClass({
focusEmail = true; focusEmail = true;
} }
var auth_services = JSON.parse(this.props.authServices);
var login_message;
if (auth_services.indexOf("gitlab") >= 0) {
login_message = (
<div className="form-group form-group--small">
<span><a href={"/"+teamName+"/login/gitlab"}>{"Log in with GitLab"}</a></span>
</div>
);
}
return ( return (
<div className="signup-team__container"> <div className="signup-team__container">
<div> <div>
@@ -112,6 +123,7 @@ module.exports = React.createClass({
<div className="form-group"> <div className="form-group">
<button type="submit" className="btn btn-primary">Sign in</button> <button type="submit" className="btn btn-primary">Sign in</button>
</div> </div>
{ login_message }
<div className="form-group form-group--small"> <div className="form-group form-group--small">
<span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span> <span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span>
</div> </div>

Просмотреть файл

@@ -20,7 +20,7 @@ module.exports = React.createClass({
<hr /> <hr />
{ server_error } { server_error }
{ client_error } { client_error }
<a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> { this.props.submit ? <a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> : "" }
<a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a> <a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a>
</li> </li>
</ul> </ul>

Просмотреть файл

@@ -25,9 +25,9 @@ module.exports = React.createClass({
var img = null; var img = null;
if (this.props.picture) { if (this.props.picture) {
img = (<img ref="image" className="col-xs-5 profile-img" src=""/>); img = (<img ref="image" className="profile-img" src=""/>);
} else { } else {
img = (<img ref="image" className="col-xs-5 profile-img" src={this.props.src}/>); img = (<img ref="image" className="profile-img" src={this.props.src}/>);
} }
var self = this; var self = this;
@@ -37,7 +37,7 @@ module.exports = React.createClass({
<li className="col-xs-12 section-title">{this.props.title}</li> <li className="col-xs-12 section-title">{this.props.title}</li>
<li className="col-xs-offset-3 col-xs-8"> <li className="col-xs-offset-3 col-xs-8">
<ul className="setting-list"> <ul className="setting-list">
<li className="row setting-list-item"> <li className="setting-list-item">
{img} {img}
</li> </li>
<li className="setting-list-item"> <li className="setting-list-item">

Просмотреть файл

@@ -101,13 +101,13 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
teamName: config.SiteName teamDisplayName: config.SiteName
}; };
}, },
render: function() { render: function() {
var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : config.SiteName; var me = UserStore.getCurrentUser();
var me = UserStore.getCurrentUser()
if (!me) { if (!me) {
return null; return null;
} }
@@ -118,11 +118,11 @@ module.exports = React.createClass({
{ me.last_picture_update ? { me.last_picture_update ?
<img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} />
: :
<div /> null
} }
<div className="header__info"> <div className="header__info">
<div className="user__name">{ '@' + me.username}</div> <div className="user__name">{ '@' + me.username}</div>
<div className="team__name">{ teamDisplayName }</div> <div className="team__name">{ this.props.teamDisplayName }</div>
</div> </div>
</a> </a>
<NavbarDropdown teamType={this.props.teamType} /> <NavbarDropdown teamType={this.props.teamType} />

Просмотреть файл

@@ -69,7 +69,9 @@ module.exports = React.createClass({
{ name_error } { name_error }
</div> </div>
{ server_error } { server_error }
<button className="btn btn-md btn-primary" type="submit">Sign up for Free</button> <div className="form-group">
<button className="btn btn-md btn-primary" type="submit">Sign up for Free</button>
</div>
<div className="form-group form-group--small"> <div className="form-group form-group--small">
<span><a href="/find_team">{"Find my " + strings.Team}</a></span> <span><a href="/find_team">{"Find my " + strings.Team}</a></span>
</div> </div>

Просмотреть файл

@@ -246,7 +246,7 @@ TeamURLPage = React.createClass({
<h2>{utils.toTitleCase(strings.Team) + " URL"}</h2> <h2>{utils.toTitleCase(strings.Team) + " URL"}</h2>
<div className={ name_error ? "form-group has-error" : "form-group" }> <div className={ name_error ? "form-group has-error" : "form-group" }>
<div className="row"> <div className="row">
<div className="col-sm-9"> <div className="col-sm-11">
<div className="input-group"> <div className="input-group">
<span className="input-group-addon">{ window.location.origin + "/" }</span> <span className="input-group-addon">{ window.location.origin + "/" }</span>
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/>

Просмотреть файл

@@ -46,7 +46,7 @@ module.exports = React.createClass({
function(data) { function(data) {
client.track('signup', 'signup_user_02_complete'); client.track('signup', 'signup_user_02_complete');
client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, client.loginByEmail(this.props.teamName, this.state.user.email, this.state.user.password,
function(data) { function(data) {
UserStore.setLastEmail(this.state.user.email); UserStore.setLastEmail(this.state.user.email);
UserStore.setCurrentUser(data); UserStore.setCurrentUser(data);
@@ -58,7 +58,7 @@ module.exports = React.createClass({
}.bind(this), }.bind(this),
function(err) { function(err) {
if (err.message == "Login failed because email address has not been verified") { if (err.message == "Login failed because email address has not been verified") {
window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.teamName);
} else { } else {
this.state.server_error = err.message; this.state.server_error = err.message;
this.setState(this.state); this.setState(this.state);
@@ -79,7 +79,7 @@ module.exports = React.createClass({
props = {}; props = {};
props.wizard = "welcome"; props.wizard = "welcome";
props.user = {}; props.user = {};
props.user.team_id = this.props.team_id; props.user.team_id = this.props.teamId;
props.user.email = this.props.email; props.user.email = this.props.email;
props.hash = this.props.hash; props.hash = this.props.hash;
props.data = this.props.data; props.data = this.props.data;
@@ -103,7 +103,7 @@ module.exports = React.createClass({
var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span> var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span>
var email = var email = (
<div className={ this.state.original_email == "" ? "" : "hidden"} > <div className={ this.state.original_email == "" ? "" : "hidden"} >
<label className="control-label">Email</label> <label className="control-label">Email</label>
<div className={ email_error ? "form-group has-error" : "form-group" }> <div className={ email_error ? "form-group has-error" : "form-group" }>
@@ -111,12 +111,25 @@ module.exports = React.createClass({
{ email_error } { email_error }
</div> </div>
</div> </div>
);
var auth_services = JSON.parse(this.props.authServices);
var signup_message;
if (auth_services.indexOf("gitlab") >= 0) {
signup_message = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} <a href={"/"+this.props.teamName+"/signup/gitlab"+window.location.search}>{"or sign up with GitLab."}</a></p>;
} else {
signup_message = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team + "."}</p>;
}
return ( return (
<div> <div>
<img className="signup-team-logo" src="/static/images/logo.png" /> <img className="signup-team-logo" src="/static/images/logo.png" />
<h4>Welcome to { config.SiteName }</h4> <h4>Welcome to { config.SiteName }</h4>
<p>{"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}</p> <div className="form-group form-group--small">
<span></span>
</div>
{ signup_message }
<p>Your username can be made of lowercase letters and numbers.</p> <p>Your username can be made of lowercase letters and numbers.</p>
<label className="control-label">Username</label> <label className="control-label">Username</label>
<div className={ name_error ? "form-group has-error" : "form-group" }> <div className={ name_error ? "form-group has-error" : "form-group" }>

84
web/react/components/signup_user_oauth.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
// 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');
window.location.href = '/' + this.props.teamName + '/login/'+user.auth_service;
}.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 ? <label className='control-label'>{ this.state.name_error }</label> : null;
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null;
var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is <b>{ this.state.user.email }.</b></span>;
return (
<div>
<img className="signup-team-logo" src="/static/images/logo.png" />
<h4>Welcome to { config.SiteName }</h4>
<p>{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}</p>
<p>Your username can be made of lowercase letters and numbers.</p>
<label className="control-label">Username</label>
<div className={ name_error ? "form-group has-error" : "form-group" }>
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" value={this.state.user.username} onChange={this.handleChange} />
{ name_error }
</div>
<p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p>
<p>{ yourEmailIs } Youll use this address to sign in to {config.SiteName}.</p>
<div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div>
<p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p>
{ server_error }
<p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
</div>
);
}
});

Просмотреть файл

@@ -73,13 +73,12 @@ var FeatureTab = React.createClass({
var inputs = []; var inputs = [];
inputs.push( inputs.push(
<div className="col-sm-12"> <div>
<div className="btn-group" data-toggle="buttons-radio"> <div className="btn-group" data-toggle="buttons-radio">
<button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button> <button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button>
<button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button> <button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
</div> </div>
<div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div> <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
<br></br>
</div> </div>
); );

Просмотреть файл

@@ -449,7 +449,7 @@ var SecurityTab = React.createClass({
submitPassword: function(e) { submitPassword: function(e) {
e.preventDefault(); e.preventDefault();
var user = UserStore.getCurrentUser(); var user = this.props.user;
var currentPassword = this.state.current_password; var currentPassword = this.state.current_password;
var newPassword = this.state.new_password; var newPassword = this.state.new_password;
var confirmPassword = this.state.confirm_password; var confirmPassword = this.state.confirm_password;
@@ -513,53 +513,69 @@ var SecurityTab = React.createClass({
var self = this; var self = this;
if (this.props.activeSection === 'password') { if (this.props.activeSection === 'password') {
var inputs = []; var inputs = [];
var submit = null;
inputs.push( if (this.props.user.auth_service === "") {
<div className="form-group"> inputs.push(
<label className="col-sm-5 control-label">Current Password</label> <div className="form-group">
<div className="col-sm-7"> <label className="col-sm-5 control-label">Current Password</label>
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/> <div className="col-sm-7">
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
</div>
</div> </div>
</div> );
); inputs.push(
inputs.push( <div className="form-group">
<div className="form-group"> <label className="col-sm-5 control-label">New Password</label>
<label className="col-sm-5 control-label">New Password</label> <div className="col-sm-7">
<div className="col-sm-7"> <input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/> </div>
</div> </div>
</div> );
); inputs.push(
inputs.push( <div className="form-group">
<div className="form-group"> <label className="col-sm-5 control-label">Retype New Password</label>
<label className="col-sm-5 control-label">Retype New Password</label> <div className="col-sm-7">
<div className="col-sm-7"> <input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/>
<input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/> </div>
</div> </div>
</div> );
);
submit = this.submitPassword;
} else {
inputs.push(
<div 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>
);
}
passwordSection = ( passwordSection = (
<SettingItemMax <SettingItemMax
title="Password" title="Password"
inputs={inputs} inputs={inputs}
submit={this.submitPassword} submit={submit}
server_error={server_error} server_error={server_error}
client_error={password_error} client_error={password_error}
updateSection={function(e){self.props.updateSection("");e.preventDefault();}} updateSection={function(e){self.props.updateSection("");e.preventDefault();}}
/> />
); );
} else { } else {
var d = new Date(this.props.user.last_password_update); var describe;
var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; if (this.props.user.auth_service === "") {
var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); var d = new Date(this.props.user.last_password_update);
var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12";
var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes());
var timeOfDay = d.getHours() >= 12 ? " pm" : " am";
describe = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay;
} else {
describe = "Log in done through GitLab"
}
passwordSection = ( passwordSection = (
<SettingItemMin <SettingItemMin
title="Password" title="Password"
describe={dateStr} describe={describe}
updateSection={function(){self.props.updateSection("password");}} updateSection={function(){self.props.updateSection("password");}}
/> />
); );
@@ -577,9 +593,9 @@ var SecurityTab = React.createClass({
{ passwordSection } { passwordSection }
<div className="divider-dark"/> <div className="divider-dark"/>
<br></br> <br></br>
<a data-toggle="modal" className="security-links" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a> <a data-toggle="modal" className="security-links theme" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a>
<b> </b> <b> </b>
<a data-toggle="modal" className="security-links" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Devices</a> <a data-toggle="modal" className="security-links theme" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Devices</a>
</div> </div>
</div> </div>
); );

Просмотреть файл

@@ -3,9 +3,9 @@
var Login = require('../components/login.jsx'); var Login = require('../components/login.jsx');
global.window.setup_login_page = function(teamDisplayName, teamName) { global.window.setup_login_page = function(team_display_name, team_name, auth_services) {
React.render( React.render(
<Login teamDisplayName={teamDisplayName} teamName={teamName}/>, <Login teamDisplayName={team_display_name} teamName={team_name} authServices={auth_services} />,
document.getElementById('login') document.getElementById('login')
); );
}; };

Просмотреть файл

@@ -3,9 +3,9 @@
var SignupUserComplete =require('../components/signup_user_complete.jsx'); var SignupUserComplete =require('../components/signup_user_complete.jsx');
global.window.setup_signup_user_complete_page = function(email, domain, name, id, data, hash) { global.window.setup_signup_user_complete_page = function(email, name, ui_name, id, data, hash, auth_services) {
React.render( React.render(
<SignupUserComplete team_id={id} domain={domain} team_name={name} email={email} hash={hash} data={data} />, <SignupUserComplete teamId={id} teamName={name} teamDisplayName={ui_name} email={email} hash={hash} data={data} authServices={auth_services} />,
document.getElementById('signup-user-complete') document.getElementById('signup-user-complete')
); );
}; };

11
web/react/pages/signup_user_oauth.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// 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(
<SignupUserOAuth user={user} teamName={team_name} teamDisplayName={team_display_name} />,
document.getElementById('signup-user-complete')
);
};

Просмотреть файл

@@ -183,6 +183,9 @@ var UserStore = assign({}, EventEmitter.prototype, {
var keys = []; var keys = [];
if (!user)
return keys;
if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(',')); if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(','));
if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name); if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name);
if (user.notify_props.all === "true") keys.push('@all'); if (user.notify_props.all === "true") keys.push('@all');
@@ -258,4 +261,3 @@ UserStore.dispatchToken = AppDispatcher.register(function(payload) {
UserStore.setMaxListeners(0); UserStore.setMaxListeners(0);
global.window.UserStore = UserStore; global.window.UserStore = UserStore;
module.exports = UserStore; module.exports = UserStore;

Просмотреть файл

@@ -12,7 +12,7 @@
} }
.access__date { .access__date {
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 15px;
width: 190px; width: 190px;
} }
.access__report { .access__report {
@@ -21,7 +21,7 @@
} }
.report__time { .report__time {
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 15px;
} }
.report__info { .report__info {
color: #999; color: #999;

Просмотреть файл

@@ -5,7 +5,7 @@ html, body {
body { body {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
background: #e9e9e9; background: $body-bg;
position: relative; position: relative;
height: 100%; height: 100%;
&.white { &.white {
@@ -96,32 +96,6 @@ div.theme {
position:relative; position:relative;
} }
.command-box {
position:absolute;
background-color:#fff;
width:100%;
border:1px solid #ddd;
bottom: 38;
}
.command-name {
position: relative;
width: 100%;
background-color: #fff;
height: 37px;
line-height: 37px;
padding: 2px 10px 2px 5px;
z-index: 101;
}
.command-name:hover {
background-color:#e8eaed;
}
.command-desc {
color: #a7a8ab;
}
@-webkit-keyframes spin2 { @-webkit-keyframes spin2 {
from { -webkit-transform: rotate(0deg);} from { -webkit-transform: rotate(0deg);}
to { -webkit-transform: rotate(360deg);} to { -webkit-transform: rotate(360deg);}
@@ -139,29 +113,3 @@ div.theme {
.black-bg { .black-bg {
background-color: black !important; background-color: black !important;
} }
#error_bar {
background-color: #0099FF;
text-align:center;
position: relative;
color: #fff;
position: fixed;
top: 0;
width: 100%;
z-index: 9999;
.error-bar {
padding: 5px 30px;
}
.error-close {
position: absolute;
right: 0;
top: 0;
color: #FFF;
font-size: 20px;
font-weight: 600;
text-decoration: none;
padding: 0 10px;
}
}

Просмотреть файл

@@ -0,0 +1,25 @@
.command-box {
position: absolute;
background-color: #fff;
width: 100%;
border: $border-gray;
bottom: 38px;
@extend %popover-box-shadow;
}
.command-name {
position: relative;
width: 100%;
background-color: #fff;
height: 37px;
line-height: 37px;
padding: 2px 10px 2px 5px;
z-index: 101;
&:hover {
background-color: #e8eaed;
}
}
.command-desc {
color: #a7a8ab;
}

Просмотреть файл

@@ -0,0 +1,25 @@
.error-bar {
background-color: #0099FF;
text-align:center;
position: relative;
color: #fff;
position: fixed;
top: 0;
width: 100%;
z-index: 9999;
padding: 5px 30px;
&__close {
position: absolute;
right: 0;
top: 0;
color: #FFF;
font-size: 20px;
font-weight: 600;
text-decoration: none;
padding: 0 10px;
&:hover {
color: #FFF;
text-decoration: none;
}
}
}

Просмотреть файл

@@ -92,10 +92,10 @@
.navbar-right { .navbar-right {
font-size: 0.85em; font-size: 0.85em;
position: absolute; position: absolute;
top: 20px; top: 10px;
right: 22px; right: 22px;
.dropdown-toggle { .dropdown-toggle {
padding: 0 10px; padding: 10px;
} }
.dropdown-menu { .dropdown-menu {
li a { li a {
@@ -119,6 +119,7 @@
} }
.header__info { .header__info {
color: #fff; color: #fff;
padding-left: 3px
} }
.team__name, .user__name { .team__name, .user__name {
display: block; display: block;

Просмотреть файл

@@ -10,6 +10,7 @@
.mentions--top { .mentions--top {
position: absolute; position: absolute;
z-index: 1060; z-index: 1060;
@extend %popover-box-shadow;
.mentions-box { .mentions-box {
width: 100%; width: 100%;
height: 100%; height: 100%;

Просмотреть файл

@@ -451,31 +451,23 @@
color: #fff; color: #fff;
.search__form { .search__form {
border: none; border: none;
padding: 0 60px 0 25px; padding: 0 60px 0 35px;
.form-control { .form-control {
line-height: 31px; line-height: normal;
background: none; background: none;
color: #fff; color: #fff;
border-radius: 0; border-radius: 0;
padding: 0 10px 0; padding: 0;
border-bottom: 1px solid #FFF;
border-bottom: 1px solid rgba(#fff, 0.6);
@include input-placeholder { @include input-placeholder {
color: rgba(#fff, 0.6); color: rgba(#fff, 0.6);
} }
} }
::-webkit-input-placeholder { input[type=text] {
color: #fff; @include input-placeholder {
} color: #fff;
}
:-moz-placeholder { /* Firefox 18- */
color: #fff;
}
::-moz-placeholder { /* Firefox 19+ */
color: #fff;
}
:-ms-input-placeholder {
color: #fff;
} }
} }
} }

Просмотреть файл

@@ -163,8 +163,10 @@
} }
.profile-img { .profile-img {
width:158px; width:128px;
max-height:128px; height:128px;
margin-bottom: 10px;
@include border-radius(128px);
} }
.sel-btn { .sel-btn {
@@ -232,4 +234,4 @@
.color-btn { .color-btn {
margin:4px; margin:4px;
} }

Просмотреть файл

@@ -6,7 +6,7 @@
} }
.signup-team__container { .signup-team__container {
padding: 100px 0px 50px 0px; padding: 100px 0px 50px 0px;
max-width: 600px; max-width: 380px;
margin: 0 auto; margin: 0 auto;
font-size: 1.1em; font-size: 1.1em;
position: relative; position: relative;

Просмотреть файл

@@ -7,4 +7,8 @@ $primary-color: #2389D7;
$primary-color--hover: darken(#2389D7, 5%); $primary-color--hover: darken(#2389D7, 5%);
$body-bg: #e9e9e9; $body-bg: #e9e9e9;
$header-bg: #f9f9f9; $header-bg: #f9f9f9;
$border-gray: 1px solid #ddd; $border-gray: 1px solid #ddd;
%popover-box-shadow {
@include box-shadow(rgba(black, 0.175) 1px -3px 12px);
}

Просмотреть файл

@@ -29,7 +29,9 @@
@import "partials/settings"; @import "partials/settings";
@import "partials/modal"; @import "partials/modal";
@import "partials/mentions"; @import "partials/mentions";
@import "partials/command-box";
@import "partials/error"; @import "partials/error";
@import "partials/error-bar";
@import "partials/loading"; @import "partials/loading";
// Responsive Css // Responsive Css

Просмотреть файл

@@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<script> <script>
window.setup_login_page({{.Props.TeamDisplayName}}, {{.Props.TeamName}}); window.setup_login_page('{{.Props.TeamDisplayName}}', '{{.Props.TeamName}}', '{{.Props.AuthServices}}');
</script> </script>
</body> </body>
</html> </html>

Просмотреть файл

@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<script> <script>
window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}'); window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}', '{{.Props.AuthServices}}');
</script> </script>
</body> </body>
</html> </html>

26
web/templates/signup_user_oauth.html Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
{{define "signup_user_oauth"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">
<div class="container-fluid">
<div class="inner__wrap">
<div class="row content">
<div class="col-sm-12">
<div class="signup-team__container">
<div id="signup-user-complete"></div>
</div>
</div>
<div class="footer-push"></div>
</div>
<div class="row footer">
{{template "footer" . }}
</div>
</div>
</div>
<script>
window.setup_signup_user_oauth_page('{{.Props.User}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}');
</script>
</body>
</html>
{{end}}

Просмотреть файл

@@ -52,6 +52,11 @@ func InitWeb() {
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET")
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-]+}/login", api.AppHandler(login)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here.
mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET")
mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).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-]+}/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-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here. // Bug in gorilla.mux pervents us from using regex here.
@@ -61,6 +66,11 @@ func InitWeb() {
mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here.
mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET")
mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET")
mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
@@ -178,6 +188,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("login", "Login") page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = teamName page.Props["TeamName"] = teamName
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -264,6 +275,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
page.Props["TeamId"] = props["id"] page.Props["TeamId"] = props["id"]
page.Props["Data"] = data page.Props["Data"] = data
page.Props["Hash"] = hash page.Props["Hash"] = hash
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -439,3 +451,189 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Props["IsReset"] = strconv.FormatBool(isResetLink)
page.Render(c, w) page.Render(c, w)
} }
func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
teamName := params["team"]
if len(teamName) == 0 {
c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
hash := r.URL.Query().Get("h")
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if api.IsVerifyHashRequired(nil, team, hash) {
data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "")
return
}
t, err := strconv.ParseInt(props["time"], 10, 64)
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
c.Err = model.NewAppError("signupWithOAuth", "The signup link has expired", "")
return
}
if team.Id != props["id"] {
c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", data)
return
}
}
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete"
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri)
}
func signupCompleteOAuth(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")
teamName := r.FormValue("team")
uri := c.GetSiteURL() + "/signup/" + service + "/complete?team=" + teamName
if len(teamName) == 0 {
c.Err = model.NewAppError("signupCompleteOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
var user *model.User
if service == model.USER_AUTH_SERVICE_GITLAB {
glu := model.GitLabUserFromJson(body)
user = model.UserFromGitLabUser(glu)
}
if user == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "")
return
}
if result := <-api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service); 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 {
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)
}
}
func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
teamName := params["team"]
if len(teamName) == 0 {
c.Err = model.NewAppError("loginWithOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
}
redirectUri := c.GetSiteURL() + "/login/" + service + "/complete"
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri)
}
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")
teamName := r.FormValue("team")
uri := c.GetSiteURL() + "/login/" + service + "/complete?team=" + teamName
if len(teamName) == 0 {
c.Err = model.NewAppError("loginCompleteOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
authData := ""
if service == model.USER_AUTH_SERVICE_GITLAB {
glu := model.GitLabUserFromJson(body)
authData = glu.GetAuthData()
}
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
}
root(c, w, r)
}
}
}