diff options
123 files changed, 2928 insertions, 898 deletions
diff --git a/api/admin.go b/api/admin.go index d4af1d247..6d7a9028f 100644 --- a/api/admin.go +++ b/api/admin.go @@ -20,6 +20,7 @@ func InitAdmin(r *mux.Router) { sr := r.PathPrefix("/admin").Subrouter() sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET") + sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET") } func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { @@ -49,3 +50,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.ArrayToJson(lines))) } + +func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) { + w.Write([]byte(model.MapToJson(utils.ClientProperties))) +} diff --git a/api/admin_test.go b/api/admin_test.go index 460ac1208..e67077c55 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -33,3 +33,12 @@ func TestGetLogs(t *testing.T) { t.Fatal() } } + +func TestGetClientProperties(t *testing.T) { + Setup() + + if _, err := Client.GetClientProperties(); err != nil { + + t.Fatal(err) + } +} diff --git a/api/api.go b/api/api.go index 35ac0bdc0..c8f97c5af 100644 --- a/api/api.go +++ b/api/api.go @@ -16,10 +16,12 @@ var ServerTemplates *template.Template type ServerTemplatePage Page -func NewServerTemplatePage(templateName, siteURL string) *ServerTemplatePage { - props := make(map[string]string) - props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl - return &ServerTemplatePage{TemplateName: templateName, SiteName: utils.Cfg.ServiceSettings.SiteName, FeedbackEmail: utils.Cfg.EmailSettings.FeedbackEmail, SiteURL: siteURL, Props: props} +func NewServerTemplatePage(templateName string) *ServerTemplatePage { + return &ServerTemplatePage{ + TemplateName: templateName, + Props: make(map[string]string), + ClientProps: utils.ClientProperties, + } } func (me *ServerTemplatePage) Render() string { @@ -40,8 +42,8 @@ func InitApi() { InitWebSocket(r) InitFile(r) InitCommand(r) - InitConfig(r) InitAdmin(r) + InitOAuth(r) templatesDir := utils.FindDir("api/templates") l4g.Debug("Parsing server templates at %v", templatesDir) diff --git a/api/api_test.go b/api/api_test.go index 0c2e57891..642db581e 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -17,7 +17,7 @@ func Setup() { NewServer() StartServer() InitApi() - Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) } } diff --git a/api/channel_test.go b/api/channel_test.go index d65aff66c..7e9267192 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -62,7 +62,7 @@ func TestCreateChannel(t *testing.T) { } } - if _, err := Client.DoPost("/channels/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/channels/create", "garbage"); err == nil { t.Fatal("should have been an error") } @@ -627,7 +627,7 @@ func TestGetChannelExtraInfo(t *testing.T) { currentEtag = cache_result.Etag } - Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "tester2@test.com", Nickname: "Tester 2", Password: "pwd"} user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User) diff --git a/api/command.go b/api/command.go index 2919e93a0..be1d3229b 100644 --- a/api/command.go +++ b/api/command.go @@ -315,7 +315,7 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool { numPosts, _ = strconv.Atoi(tokens[numArgs+2]) } } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) if doTeams { if err := CreateBasicUser(client); err != nil { @@ -375,7 +375,7 @@ func loadTestUsersCommand(c *Context, command *model.Command) bool { if err == false { usersr = utils.Range{10, 15} } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) userCreator := NewAutoUserCreator(client, c.Session.TeamId) userCreator.Fuzzy = doFuzz userCreator.CreateTestUsers(usersr) @@ -405,7 +405,7 @@ func loadTestChannelsCommand(c *Context, command *model.Command) bool { if err == false { channelsr = utils.Range{20, 30} } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) client.MockSession(c.Session.Id) channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) channelCreator.Fuzzy = doFuzz @@ -457,7 +457,7 @@ func loadTestPostsCommand(c *Context, command *model.Command) bool { } } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) client.MockSession(c.Session.Id) testPoster := NewAutoPostCreator(client, command.ChannelId) testPoster.Fuzzy = doFuzz diff --git a/api/config.go b/api/config.go deleted file mode 100644 index 142d1ca66..000000000 --- a/api/config.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - l4g "code.google.com/p/log4go" - "encoding/json" - "github.com/gorilla/mux" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" - "net/http" - "strconv" -) - -func InitConfig(r *mux.Router) { - l4g.Debug("Initializing config api routes") - - sr := r.PathPrefix("/config").Subrouter() - sr.Handle("/get_all", ApiAppHandler(getConfig)).Methods("GET") -} - -func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { - settings := make(map[string]string) - - settings["ByPassEmail"] = strconv.FormatBool(utils.Cfg.EmailSettings.ByPassEmail) - - if bytes, err := json.Marshal(settings); err != nil { - c.Err = model.NewAppError("getConfig", "Unable to marshall configuration data", err.Error()) - return - } else { - w.Write(bytes) - } -} diff --git a/api/context.go b/api/context.go index fc7d8f23d..b1b4d2d10 100644 --- a/api/context.go +++ b/api/context.go @@ -4,6 +4,7 @@ package api import ( + "fmt" "net" "net/http" "net/url" @@ -29,12 +30,9 @@ type Context struct { } type Page struct { - TemplateName string - Title string - SiteName string - FeedbackEmail string - SiteURL string - Props map[string]string + TemplateName string + Props map[string]string + ClientProps map[string]string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -82,9 +80,36 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.RequestId = model.NewId() c.IpAddress = GetIpAddress(r) + token := "" + isTokenFromQueryString := false + + // Attempt to parse token out of the header + authHeader := r.Header.Get(model.HEADER_AUTH) + if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER { + // Default session token + token = authHeader[7:] + + } else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN { + // OAuth token + token = authHeader[6:] + } + + // Attempt to parse the token from the cookie + if len(token) == 0 { + if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { + token = cookie.Value + } + } + + // Attempt to parse token out of the query string + if len(token) == 0 { + token = r.URL.Query().Get("access_token") + isTokenFromQueryString = true + } + protocol := "http" - // if the request came from the ELB then assume this is produciton + // If the request came from the ELB then assume this is produciton // and redirect all http requests to https if utils.Cfg.ServiceSettings.UseSSL { forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO) @@ -100,43 +125,26 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.setSiteURL(protocol + "://" + r.Host) w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) - w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version) + w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version+fmt.Sprintf(".%v", utils.CfgLastModified)) // Instruct the browser not to display us in an iframe for anti-clickjacking if !h.isApi { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Content-Security-Policy", "frame-ancestors none") } else { - // All api response bodies will be JSON formatted + // All api response bodies will be JSON formatted by default w.Header().Set("Content-Type", "application/json") } - sessionId := "" - - // attempt to parse the session token from the header - if ah := r.Header.Get(model.HEADER_AUTH); ah != "" { - if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { - sessionId = ah[7:] - } - } - - // attempt to parse the session token from the cookie - if sessionId == "" { - if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { - sessionId = cookie.Value - } - } - - if sessionId != "" { - + if len(token) != 0 { var session *model.Session - if ts, ok := sessionCache.Get(sessionId); ok { + if ts, ok := sessionCache.Get(token); ok { session = ts.(*model.Session) } if session == nil { - if sessionResult := <-Srv.Store.Session().Get(sessionId); sessionResult.Err != nil { - c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "id="+sessionId+", err="+sessionResult.Err.DetailedError)) + if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { + c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "token="+token+", err="+sessionResult.Err.DetailedError)) } else { session = sessionResult.Data.(*model.Session) } @@ -144,7 +152,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w) - c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "id="+sessionId) + c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } else if !session.IsOAuth && isTokenFromQueryString { + c.Err = model.NewAppError("ServeHTTP", "Session is not OAuth but token was provided in the query string", "token="+token) c.Err.StatusCode = http.StatusUnauthorized } else { c.Session = *session @@ -168,10 +179,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SystemAdminRequired() } - if c.Err == nil && h.isUserActivity && sessionId != "" && len(c.Session.UserId) > 0 { + if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 { go func() { - if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, sessionId, model.GetMillis())).Err; err != nil { - l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, sessionId, err) + if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil { + l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, c.Session.Id, err) } }() } @@ -199,7 +210,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (c *Context) LogAudit(extraInfo string) { - audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } @@ -211,7 +222,7 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) { extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId) } - audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } @@ -317,7 +328,7 @@ func (c *Context) IsTeamAdmin(userId string) bool { func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { - sessionCache.Remove(c.Session.Id) + sessionCache.Remove(c.Session.Token) cookie := &http.Cookie{ Name: model.SESSION_TOKEN, @@ -473,3 +484,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) { l4g.Error("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r)) RenderWebError(err, w, r) } + +func AddSessionToCache(session *model.Session) { + sessionCache.Add(session.Token, session) +} diff --git a/api/oauth.go b/api/oauth.go new file mode 100644 index 000000000..26c3c5da8 --- /dev/null +++ b/api/oauth.go @@ -0,0 +1,165 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "fmt" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "net/url" +) + +func InitOAuth(r *mux.Router) { + l4g.Debug("Initializing oauth api routes") + + sr := r.PathPrefix("/oauth").Subrouter() + + sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") + sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") +} + +func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("registerOAuthApp", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + app := model.OAuthAppFromJson(r.Body) + + if app == nil { + c.SetInvalidParam("registerOAuthApp", "app") + return + } + + secret := model.NewId() + + app.ClientSecret = secret + app.CreatorId = c.Session.UserId + + if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + app.ClientSecret = secret + + c.LogAudit("client_id=" + app.Id) + + w.Write([]byte(app.ToJson())) + return + } + +} + +func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("allowOAuth", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + responseData := map[string]string{} + + responseType := r.URL.Query().Get("response_type") + if len(responseType) == 0 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad response_type", "") + return + } + + clientId := r.URL.Query().Get("client_id") + if len(clientId) != 26 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad client_id", "") + return + } + + redirectUri := r.URL.Query().Get("redirect_uri") + if len(redirectUri) == 0 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Missing or bad redirect_uri", "") + return + } + + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = model.NewAppError("allowOAuth", "server_error: Error accessing the database", "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !app.IsValidRedirectURL(redirectUri) { + c.LogAudit("fail - redirect_uri did not match registered callback") + c.Err = model.NewAppError("allowOAuth", "invalid_request: Supplied redirect_uri did not match registered callback_url", "") + return + } + + if responseType != model.AUTHCODE_RESPONSE_TYPE { + responseData["redirect"] = redirectUri + "?error=unsupported_response_type&state=" + state + w.Write([]byte(model.MapToJson(responseData))) + return + } + + authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope} + authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId)) + + if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { + responseData["redirect"] = redirectUri + "?error=server_error&state=" + state + w.Write([]byte(model.MapToJson(responseData))) + return + } + + c.LogAudit("success") + + responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State) + + w.Write([]byte(model.MapToJson(responseData))) +} + +func RevokeAccessToken(token string) *model.AppError { + + schan := Srv.Store.Session().Remove(token) + sessionCache.Remove(token) + + var accessData *model.AccessData + if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error getting access token from DB before deletion", "") + } else { + accessData = result.Data.(*model.AccessData) + } + + tchan := Srv.Store.OAuth().RemoveAccessData(token) + cchan := Srv.Store.OAuth().RemoveAuthData(accessData.AuthCode) + + if result := <-tchan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting access token from DB", "") + } + + if result := <-cchan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting authorization code from DB", "") + } + + if result := <-schan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting session from DB", "") + } + + return nil +} + +func GetAuthData(code string) *model.AuthData { + if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil { + l4g.Error("Couldn't find auth code for code=%s", code) + return nil + } else { + return result.Data.(*model.AuthData) + } +} diff --git a/api/oauth_test.go b/api/oauth_test.go new file mode 100644 index 000000000..18db49bc5 --- /dev/null +++ b/api/oauth_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "net/url" + "strings" + "testing" +) + +func TestRegisterApp(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("should have failed - oauth providing turned off") + } + + } else { + + Client.Logout() + + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("not logged in - should have failed") + } + + Client.Must(Client.LoginById(ruser.Id, "pwd")) + + if result, err := Client.RegisterApp(app); err != nil { + t.Fatal(err) + } else { + rapp := result.Data.(*model.OAuthApp) + if len(rapp.Id) != 26 { + t.Fatal("clientid didn't return properly") + } + if len(rapp.ClientSecret) != 26 { + t.Fatal("client secret didn't return properly") + } + } + + app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing name - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing homepage - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing callback url - should have failed") + } + } +} + +func TestAllowOAuth(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + Client.Must(Client.LoginById(ruser.Id, "pwd")) + + state := "123" + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "12345678901234567890123456", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - oauth service providing turned off") + } + } else { + app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) + } else { + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") + } + + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") + } else { + if len(ru.Query().Get("code")) == 0 { + t.Fatal("authorization code not returned") + } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") + } + } + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } + + if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) + } else { + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") + } + + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") + } else { + if ru.Query().Get("error") != "unsupported_response_type" { + t.Fatal("wrong error returned") + } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") + } + } + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - empty client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - bad client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil { + t.Fatal("should have failed - redirect uri host does not match app host") + } + } +} diff --git a/api/post.go b/api/post.go index bd31e0210..005f3f884 100644 --- a/api/post.go +++ b/api/post.go @@ -378,7 +378,8 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { location, _ := time.LoadLocation("UTC") tm := time.Unix(post.CreateAt/1000, 0).In(location) - subjectPage := NewServerTemplatePage("post_subject", siteURL) + subjectPage := NewServerTemplatePage("post_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["Month"] = tm.Month().String()[:3] @@ -396,7 +397,8 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { continue } - bodyPage := NewServerTemplatePage("post_body", siteURL) + bodyPage := NewServerTemplatePage("post_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Nickname"] = profileMap[id].FirstName bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["ChannelName"] = channelName diff --git a/api/post_test.go b/api/post_test.go index 85d92de3a..4cccfd62a 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -118,7 +118,7 @@ func TestCreatePost(t *testing.T) { t.Fatal("Should have been forbidden") } - if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { t.Fatal("should have been an error") } } @@ -203,7 +203,7 @@ func TestCreateValetPost(t *testing.T) { t.Fatal("Should have been forbidden") } - if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { t.Fatal("should have been an error") } } else { diff --git a/api/team.go b/api/team.go index 8258fa929..44f86b160 100644 --- a/api/team.go +++ b/api/team.go @@ -56,8 +56,10 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("signup_team_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("signup_team_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("signup_team_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink props := make(map[string]string) @@ -401,8 +403,10 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("find_teams_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("find_teams_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("find_teams_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("find_teams_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { c.Err = result.Err @@ -483,16 +487,17 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = "member" } - subjectPage := NewServerTemplatePage("invite_subject", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("invite_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() subjectPage.Props["SenderName"] = sender subjectPage.Props["TeamDisplayName"] = team.DisplayName - bodyPage := NewServerTemplatePage("invite_body", c.GetSiteURL()) + + bodyPage := NewServerTemplatePage("invite_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["TeamDisplayName"] = team.DisplayName bodyPage.Props["SenderName"] = sender bodyPage.Props["SenderStatus"] = senderRole - bodyPage.Props["Email"] = invite - props := make(map[string]string) props["email"] = invite props["id"] = team.Id diff --git a/api/team_test.go b/api/team_test.go index 2723eff57..4f1b9e5f0 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -103,7 +103,7 @@ func TestCreateTeam(t *testing.T) { } } - if _, err := Client.DoPost("/teams/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/teams/create", "garbage"); err == nil { t.Fatal("should have been an error") } } diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index c4e1cf39d..3f041d09d 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -25,7 +25,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,7 +34,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/error.html b/api/templates/error.html index adb8f9f7d..cac46e223 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -1,7 +1,7 @@ <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <title>{{ .SiteName }} - Error</title> + <title>{{ .ClientProps.SiteName }} - Error</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script> @@ -12,9 +12,9 @@ <div class="container-fluid"> <div class="error__container"> <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> - <h2>{{ .SiteName }} needs your help:</h2> + <h2>{{ .ClientProps.SiteName }} needs your help:</h2> <p>{{.Message}}</p> - <a href="{{.SiteURL}}">Go back to team site</a> + <a href="{{.Props.SiteURL}}">Go back to team site</a> </div> </div> </body> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 00c5628dd..fe134811a 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -33,7 +33,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -42,7 +42,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html index e5ba2d23f..3c2bef589 100644 --- a/api/templates/find_teams_subject.html +++ b/api/templates/find_teams_subject.html @@ -1 +1 @@ -{{define "find_teams_subject"}}Your {{ .SiteName }} Teams{{end}} +{{define "find_teams_subject"}}Your {{ .ClientProps.SiteName }} Teams{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index 568a0d893..401111b75 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -18,7 +18,7 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> - <p>{{.Props.TeamDisplayName}} started using {{.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> + <p>{{.Props.TeamDisplayName}} started using {{.ClientProps.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> <p style="margin: 20px 0 15px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a> </p> @@ -28,7 +28,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,7 +37,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html index 6a1e57dcc..f46bdcfaf 100644 --- a/api/templates/invite_subject.html +++ b/api/templates/invite_subject.html @@ -1 +1 @@ -{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.SiteName}}{{end}} +{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientProps.SiteName}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html index 6fc9f2822..6a32e89db 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -25,7 +25,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,7 +34,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html index 55daefdb1..283fda1af 100644 --- a/api/templates/password_change_subject.html +++ b/api/templates/password_change_subject.html @@ -1 +1 @@ -{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .SiteName }}{{end}} +{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .ClientProps.SiteName }}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html index a1df5b4c9..0c906807a 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -28,7 +28,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,7 +37,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html index 7d8941549..944cd5a42 100644 --- a/api/templates/post_subject.html +++ b/api/templates/post_subject.html @@ -1 +1 @@ -{{define "post_subject"}}[{{.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} +{{define "post_subject"}}[{{.ClientProps.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index a6e6269c0..3e4938a09 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -28,7 +28,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,7 +37,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index b49cf5f36..ef58aa92c 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -21,7 +21,7 @@ <p style="margin: 20px 0 25px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a> </p> - {{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p> + {{ .ClientProps.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientProps.SiteName }} when your team is in constant communication--let's get them on board.<br></p> <p> Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a> </p> @@ -31,7 +31,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -40,7 +40,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html index 1cd3427d2..7bc0cc640 100644 --- a/api/templates/signup_team_subject.html +++ b/api/templates/signup_team_subject.html @@ -1 +1 @@ -{{define "signup_team_subject"}}Invitation to {{ .SiteName }}{{end}}
\ No newline at end of file +{{define "signup_team_subject"}}Invitation to {{ .ClientProps.SiteName }}{{end}}
\ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html index 6ba11d845..ac60e4fad 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -28,7 +28,7 @@ <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,7 +37,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html index a66150735..7990df84a 100644 --- a/api/templates/verify_subject.html +++ b/api/templates/verify_subject.html @@ -1 +1 @@ -{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .SiteName }}] Email Verification{{end}} +{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientProps.SiteName }}] Email Verification{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html index f16f50e14..4d4f03a2d 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -17,15 +17,15 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamDisplayName}} team at {{.SiteName}}!</h2> - <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.Props.TeamURL}}">{{.SiteName}}</a>.</p> + <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamDisplayName}} team at {{.ClientProps.SiteName}}!</h2> + <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.Props.TeamURL}}">{{.ClientProps.SiteName}}</a>.</p> </td> </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,7 +34,7 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html index 106cc3ae6..2214f7a38 100644 --- a/api/templates/welcome_subject.html +++ b/api/templates/welcome_subject.html @@ -1 +1 @@ -{{define "welcome_subject"}}Welcome to {{ .SiteName }}{{end}}
\ No newline at end of file +{{define "welcome_subject"}}Welcome to {{ .ClientProps.SiteName }}{{end}}
\ No newline at end of file diff --git a/api/user.go b/api/user.go index c87b89c7a..b42d156ae 100644 --- a/api/user.go +++ b/api/user.go @@ -216,8 +216,10 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { func fireAndForgetWelcomeEmail(name, email, teamDisplayName, link, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", siteURL) - bodyPage := NewServerTemplatePage("welcome_body", siteURL) + subjectPage := NewServerTemplatePage("welcome_subject") + subjectPage.Props["SiteURL"] = siteURL + bodyPage := NewServerTemplatePage("welcome_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Nickname"] = name bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["FeedbackName"] = utils.Cfg.EmailSettings.FeedbackName @@ -235,9 +237,11 @@ func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, site link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", siteURL) + subjectPage := NewServerTemplatePage("verify_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("verify_body", siteURL) + bodyPage := NewServerTemplatePage("verify_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["VerifyUrl"] = link @@ -332,7 +336,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, return } - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId} + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} maxAge := model.SESSION_TIME_WEB_IN_SECS @@ -374,13 +378,13 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, return } else { session = result.Data.(*model.Session) - sessionCache.Add(session.Id, session) + AddSessionToCache(session) } - w.Header().Set(model.HEADER_TOKEN, session.Id) + w.Header().Set(model.HEADER_TOKEN, session.Token) sessionCookie := &http.Cookie{ Name: model.SESSION_TOKEN, - Value: session.Id, + Value: session.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, @@ -426,25 +430,27 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - altId := props["id"] + id := props["id"] - if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Session().Get(id); result.Err != nil { c.Err = result.Err return } else { - sessions := result.Data.([]*model.Session) + session := result.Data.(*model.Session) - for _, session := range sessions { - if session.AltId == altId { - c.LogAudit("session_id=" + session.AltId) - sessionCache.Remove(session.Id) - if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { - c.Err = result.Err - return - } else { - w.Write([]byte(model.MapToJson(props))) - return - } + c.LogAudit("session_id=" + session.Id) + + if session.IsOAuth { + RevokeAccessToken(session.Token) + } else { + sessionCache.Remove(session.Token) + + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(model.MapToJson(props))) + return } } } @@ -458,8 +464,8 @@ func RevokeAllSession(c *Context, userId string) { sessions := result.Data.([]*model.Session) for _, session := range sessions { - c.LogAuditWithUserId(userId, "session_id="+session.AltId) - sessionCache.Remove(session.Id) + c.LogAuditWithUserId(userId, "session_id="+session.Id) + sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return @@ -1133,8 +1139,10 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("reset_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("reset_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("reset_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["ResetUrl"] = link if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { @@ -1233,9 +1241,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", siteURL) + subjectPage := NewServerTemplatePage("password_change_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("password_change_body", siteURL) + bodyPage := NewServerTemplatePage("password_change_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL bodyPage.Props["Method"] = method @@ -1250,9 +1260,11 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, func fireAndForgetEmailChangeEmail(email, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", siteURL) + subjectPage := NewServerTemplatePage("email_change_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("email_change_body", siteURL) + bodyPage := NewServerTemplatePage("email_change_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL diff --git a/api/user_test.go b/api/user_test.go index fe5a4a27f..986365bd0 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -68,7 +68,7 @@ func TestCreateUser(t *testing.T) { } } - if _, err := Client.DoPost("/users/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/users/create", "garbage"); err == nil { t.Fatal("should have been an error") } } @@ -190,11 +190,11 @@ func TestSessions(t *testing.T) { for _, session := range sessions { if session.DeviceId == deviceId { - otherSession = session.AltId + otherSession = session.Id } - if len(session.Id) != 0 { - t.Fatal("shouldn't return sessions") + if len(session.Token) != 0 { + t.Fatal("shouldn't return session tokens") } } @@ -212,11 +212,6 @@ func TestSessions(t *testing.T) { if len(sessions2) != 1 { t.Fatal("invalid number of sessions") } - - if _, err := Client.RevokeSession(otherSession); err != nil { - t.Fatal(err) - } - } func TestGetUser(t *testing.T) { @@ -355,7 +350,7 @@ func TestUserCreateImage(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - Client.DoGet("/users/"+user.Id+"/image", "", "") + Client.DoApiGet("/users/"+user.Id+"/image", "", "") if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth @@ -453,7 +448,7 @@ func TestUserUploadProfileImage(t *testing.T) { t.Fatal(upErr) } - Client.DoGet("/users/"+user.Id+"/image", "", "") + Client.DoApiGet("/users/"+user.Id+"/image", "", "") if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth diff --git a/config/config.json b/config/config.json index cd7e221e7..4c4fbb255 100644 --- a/config/config.json +++ b/config/config.json @@ -23,7 +23,8 @@ "UseLocalStorage": true, "StorageDirectory": "./data/", "AllowedLoginAttempts": 10, - "DisableEmailSignUp": false + "DisableEmailSignUp": false, + "EnableOAuthServiceProvider": false }, "SSOSettings": { "gitlab": { @@ -86,16 +87,14 @@ "ShowSkypeId": true, "ShowFullName": true }, + "ClientSettings": { + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "" + }, "TeamSettings": { "MaxUsersPerTeam": 150, "AllowPublicLink": true, "AllowValetDefault": false, - "TermsLink": "/static/help/configure_links.html", - "PrivacyLink": "/static/help/configure_links.html", - "AboutLink": "/static/help/configure_links.html", - "HelpLink": "/static/help/configure_links.html", - "ReportProblemLink": "/static/help/configure_links.html", - "TourLink": "/static/help/configure_links.html", "DefaultThemeColor": "#2389D7", "DisableTeamCreation": false, "RestrictCreationToDomains": "" diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 794ac95ae..bc42951b8 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -23,7 +23,8 @@ "UseLocalStorage": true, "StorageDirectory": "/mattermost/data/", "AllowedLoginAttempts": 10, - "DisableEmailSignUp": false + "DisableEmailSignUp": false, + "EnableOAuthServiceProvider": false }, "SSOSettings": { "gitlab": { diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 794ac95ae..bc42951b8 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -23,7 +23,8 @@ "UseLocalStorage": true, "StorageDirectory": "/mattermost/data/", "AllowedLoginAttempts": 10, - "DisableEmailSignUp": false + "DisableEmailSignUp": false, + "EnableOAuthServiceProvider": false }, "SSOSettings": { "gitlab": { diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index f7408b814..86b173c6a 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -53,7 +53,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { } // Create a client for tests to use - client := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + client := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) // Check for username parameter and create a user if present username, ok1 := params["username"] @@ -65,7 +65,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { // Create team for testing team := &model.Team{ DisplayName: teamDisplayName[0], - Name: utils.RandomName(utils.Range{20, 20}, utils.LOWERCASE), + Name: utils.RandomName(utils.Range{20, 20}, utils.LOWERCASE), Email: utils.RandomEmail(utils.Range{20, 20}, utils.LOWERCASE), Type: model.TEAM_OPEN, } diff --git a/model/access.go b/model/access.go index f9e36ce07..44a0463ac 100644 --- a/model/access.go +++ b/model/access.go @@ -9,17 +9,69 @@ import ( ) const ( - ACCESS_TOKEN_GRANT_TYPE = "authorization_code" - ACCESS_TOKEN_TYPE = "bearer" + ACCESS_TOKEN_GRANT_TYPE = "authorization_code" + ACCESS_TOKEN_TYPE = "bearer" + REFRESH_TOKEN_GRANT_TYPE = "refresh_token" ) +type AccessData struct { + AuthCode string `json:"auth_code"` + Token string `json"token"` + RefreshToken string `json:"refresh_token"` + RedirectUri string `json:"redirect_uri"` +} + type AccessResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int32 `json:"expires_in"` + Scope string `json:"scope"` RefreshToken string `json:"refresh_token"` } +// IsValid validates the AccessData and returns an error if it isn't configured +// correctly. +func (ad *AccessData) IsValid() *AppError { + + if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 { + return NewAppError("AccessData.IsValid", "Invalid auth code", "") + } + + if len(ad.Token) != 26 { + return NewAppError("AccessData.IsValid", "Invalid access token", "") + } + + if len(ad.RefreshToken) > 26 { + return NewAppError("AccessData.IsValid", "Invalid refresh token", "") + } + + if len(ad.RedirectUri) > 256 { + return NewAppError("AccessData.IsValid", "Invalid redirect uri", "") + } + + return nil +} + +func (ad *AccessData) ToJson() string { + b, err := json.Marshal(ad) + if err != nil { + return "" + } else { + return string(b) + } +} + +func AccessDataFromJson(data io.Reader) *AccessData { + decoder := json.NewDecoder(data) + var ad AccessData + err := decoder.Decode(&ad) + if err == nil { + return &ad + } else { + return nil + } +} + func (ar *AccessResponse) ToJson() string { b, err := json.Marshal(ar) if err != nil { diff --git a/model/access_test.go b/model/access_test.go new file mode 100644 index 000000000..e385c0586 --- /dev/null +++ b/model/access_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAccessJson(t *testing.T) { + a1 := AccessData{} + a1.AuthCode = NewId() + a1.Token = NewId() + a1.RefreshToken = NewId() + + json := a1.ToJson() + ra1 := AccessDataFromJson(strings.NewReader(json)) + + if a1.Token != ra1.Token { + t.Fatal("tokens didn't match") + } +} + +func TestAccessIsValid(t *testing.T) { + ad := AccessData{} + + if err := ad.IsValid(); err == nil { + t.Fatal("should have failed") + } + + ad.AuthCode = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal("should have failed") + } + + ad.Token = NewId() + if err := ad.IsValid(); err != nil { + t.Fatal(err) + } +} diff --git a/model/authorize.go b/model/authorize.go new file mode 100644 index 000000000..6eaac97f1 --- /dev/null +++ b/model/authorize.go @@ -0,0 +1,103 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes + AUTHCODE_RESPONSE_TYPE = "code" +) + +type AuthData struct { + ClientId string `json:"client_id"` + UserId string `json:"user_id"` + Code string `json:"code"` + ExpiresIn int32 `json:"expires_in"` + CreateAt int64 `json:"create_at"` + RedirectUri string `json:"redirect_uri"` + State string `json:"state"` + Scope string `json:"scope"` +} + +// IsValid validates the AuthData and returns an error if it isn't configured +// correctly. +func (ad *AuthData) IsValid() *AppError { + + if len(ad.ClientId) != 26 { + return NewAppError("AuthData.IsValid", "Invalid client id", "") + } + + if len(ad.UserId) != 26 { + return NewAppError("AuthData.IsValid", "Invalid user id", "") + } + + if len(ad.Code) == 0 || len(ad.Code) > 128 { + return NewAppError("AuthData.IsValid", "Invalid authorization code", "client_id="+ad.ClientId) + } + + if ad.ExpiresIn == 0 { + return NewAppError("AuthData.IsValid", "Expires in must be set", "") + } + + if ad.CreateAt <= 0 { + return NewAppError("AuthData.IsValid", "Create at must be a valid time", "client_id="+ad.ClientId) + } + + if len(ad.RedirectUri) > 256 { + return NewAppError("AuthData.IsValid", "Invalid redirect uri", "client_id="+ad.ClientId) + } + + if len(ad.State) > 128 { + return NewAppError("AuthData.IsValid", "Invalid state", "client_id="+ad.ClientId) + } + + if len(ad.Scope) > 128 { + return NewAppError("AuthData.IsValid", "Invalid scope", "client_id="+ad.ClientId) + } + + return nil +} + +func (ad *AuthData) PreSave() { + if ad.ExpiresIn == 0 { + ad.ExpiresIn = AUTHCODE_EXPIRE_TIME + } + + if ad.CreateAt == 0 { + ad.CreateAt = GetMillis() + } +} + +func (ad *AuthData) ToJson() string { + b, err := json.Marshal(ad) + if err != nil { + return "" + } else { + return string(b) + } +} + +func AuthDataFromJson(data io.Reader) *AuthData { + decoder := json.NewDecoder(data) + var ad AuthData + err := decoder.Decode(&ad) + if err == nil { + return &ad + } else { + return nil + } +} + +func (ad *AuthData) IsExpired() bool { + + if GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) { + return true + } + + return false +} diff --git a/model/authorize_test.go b/model/authorize_test.go new file mode 100644 index 000000000..14524ad84 --- /dev/null +++ b/model/authorize_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAuthJson(t *testing.T) { + a1 := AuthData{} + a1.ClientId = NewId() + a1.UserId = NewId() + a1.Code = NewId() + + json := a1.ToJson() + ra1 := AuthDataFromJson(strings.NewReader(json)) + + if a1.Code != ra1.Code { + t.Fatal("codes didn't match") + } +} + +func TestAuthPreSave(t *testing.T) { + a1 := AuthData{} + a1.ClientId = NewId() + a1.UserId = NewId() + a1.Code = NewId() + a1.PreSave() + a1.IsExpired() +} + +func TestAuthIsValid(t *testing.T) { + + ad := AuthData{} + + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.ClientId = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.UserId = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.Code = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.ExpiresIn = 1 + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.CreateAt = 1 + if err := ad.IsValid(); err != nil { + t.Fatal() + } +} diff --git a/model/client.go b/model/client.go index 5aac09289..9a89e8208 100644 --- a/model/client.go +++ b/model/client.go @@ -23,7 +23,9 @@ const ( HEADER_FORWARDED = "X-Forwarded-For" HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" HEADER_AUTH = "Authorization" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -33,22 +35,37 @@ type Result struct { } type Client struct { - Url string // The location of the server like "http://localhost/api/v1" + Url string // The location of the server like "http://localhost:8065" + ApiUrl string // The api location of the server like "http://localhost:8065/api/v1" HttpClient *http.Client // The http client AuthToken string + AuthType string } // NewClient constructs a new client with convienence methods for talking to // the server. func NewClient(url string) *Client { - return &Client{url, &http.Client{}, ""} + return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", ""} } -func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) { +func (c *Client) DoPost(url string, data, contentType string) (*http.Response, *AppError) { rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) + rq.Header.Set("Content-Type", contentType) + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data)) if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) } if rp, err := c.HttpClient.Do(rq); err != nil { @@ -60,15 +77,15 @@ func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) { } } -func (c *Client) DoGet(url string, data string, etag string) (*http.Response, *AppError) { - rq, _ := http.NewRequest("GET", c.Url+url, strings.NewReader(data)) +func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data)) if len(etag) > 0 { rq.Header.Set(HEADER_ETAG_CLIENT, etag) } if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) } if rp, err := c.HttpClient.Do(rq); err != nil { @@ -106,7 +123,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro m := make(map[string]string) m["email"] = email m["display_name"] = displayName - if r, err := c.DoPost("/teams/signup", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/signup", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -115,7 +132,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro } func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) { - if r, err := c.DoPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -124,7 +141,7 @@ func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppErro } func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { - if r, err := c.DoPost("/teams/create", team.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/create", team.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -136,7 +153,7 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro m := make(map[string]string) m["name"] = name m["all"] = fmt.Sprintf("%v", allServers) - if r, err := c.DoPost("/teams/find_team_by_name", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/find_team_by_name", MapToJson(m)); err != nil { return nil, err } else { val := false @@ -152,7 +169,7 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro func (c *Client) FindTeams(email string) (*Result, *AppError) { m := make(map[string]string) m["email"] = email - if r, err := c.DoPost("/teams/find_teams", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { return nil, err } else { @@ -164,7 +181,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) { func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { m := make(map[string]string) m["email"] = email - if r, err := c.DoPost("/teams/email_teams", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -173,7 +190,7 @@ func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { } func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { - if r, err := c.DoPost("/teams/invite_members", invites.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -182,7 +199,7 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { } func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/teams/update_name", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/teams/update_name", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -191,7 +208,7 @@ func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppErr } func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/teams/update_valet_feature", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/teams/update_valet_feature", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -200,7 +217,7 @@ func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) } func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { - if r, err := c.DoPost("/users/create", user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -209,7 +226,7 @@ func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { } func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) { - if r, err := c.DoPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -218,7 +235,7 @@ func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Re } func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id, "", etag); err != nil { + if r, err := c.DoApiGet("/users/"+id, "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -227,7 +244,7 @@ func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { } func (c *Client) GetMe(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/me", "", etag); err != nil { + if r, err := c.DoApiGet("/users/me", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -236,7 +253,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) { } func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/profiles", "", etag); err != nil { + if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -269,13 +286,14 @@ func (c *Client) LoginByEmailWithDevice(name string, email string, password stri } func (c *Client) login(m map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/login", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { return nil, err } else { c.AuthToken = r.Header.Get(HEADER_TOKEN) - sessionId := getCookie(SESSION_TOKEN, r) + c.AuthType = HEADER_BEARER + sessionToken := getCookie(SESSION_TOKEN, r) - if c.AuthToken != sessionId.Value { + if c.AuthToken != sessionToken.Value { NewAppError("/users/login", "Authentication tokens didn't match", "") } @@ -285,21 +303,32 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) { } func (c *Client) Logout() (*Result, *AppError) { - if r, err := c.DoPost("/users/logout", ""); err != nil { + if r, err := c.DoApiPost("/users/logout", ""); err != nil { return nil, err } else { c.AuthToken = "" + c.AuthType = HEADER_BEARER return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil } } +func (c *Client) SetOAuthToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_TOKEN +} + +func (c *Client) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HEADER_BEARER +} + func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { m := make(map[string]string) m["id"] = sessionAltId - if r, err := c.DoPost("/users/revoke_session", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/users/revoke_session", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -308,7 +337,7 @@ func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { } func (c *Client) GetSessions(id string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id+"/sessions", "", ""); err != nil { + if r, err := c.DoApiGet("/users/"+id+"/sessions", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -321,7 +350,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul m["command"] = command m["channelId"] = channelId m["suggest"] = strconv.FormatBool(suggest) - if r, err := c.DoPost("/command", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/command", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -330,7 +359,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul } func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id+"/audits", "", etag); err != nil { + if r, err := c.DoApiGet("/users/"+id+"/audits", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -339,7 +368,7 @@ func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { } func (c *Client) GetLogs() (*Result, *AppError) { - if r, err := c.DoGet("/admin/logs", "", ""); err != nil { + if r, err := c.DoApiGet("/admin/logs", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -347,8 +376,17 @@ func (c *Client) GetLogs() (*Result, *AppError) { } } +func (c *Client) GetClientProperties() (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -357,7 +395,7 @@ func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { } func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/create_direct", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/create_direct", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -366,7 +404,7 @@ func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError } func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update", channel.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/update", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -375,7 +413,7 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { } func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update_desc", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -384,7 +422,7 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) } func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update_notify_level", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -393,7 +431,7 @@ func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) } func (c *Client) GetChannels(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -402,7 +440,7 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/"+id+"/", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/"+id+"/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -411,7 +449,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { } func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/more", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/more", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -420,7 +458,7 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/counts", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/counts", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -429,7 +467,7 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { } func (c *Client) JoinChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/join", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -438,7 +476,7 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) { } func (c *Client) LeaveChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/leave", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/leave", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -447,7 +485,7 @@ func (c *Client) LeaveChannel(id string) (*Result, *AppError) { } func (c *Client) DeleteChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/delete", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/delete", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -458,7 +496,7 @@ func (c *Client) DeleteChannel(id string) (*Result, *AppError) { func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoPost("/channels/"+id+"/add", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/add", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -469,7 +507,7 @@ func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -478,7 +516,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { } func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -487,7 +525,7 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { } func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/"+id+"/extra_info", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/"+id+"/extra_info", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -496,7 +534,7 @@ func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError } func (c *Client) CreatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -505,7 +543,7 @@ func (c *Client) CreatePost(post *Post) (*Result, *AppError) { } func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -514,7 +552,7 @@ func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) { } func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -523,7 +561,7 @@ func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { } func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -532,7 +570,7 @@ func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) } func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -541,7 +579,7 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError } func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -550,7 +588,7 @@ func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, } func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil { + if r, err := c.DoApiPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -559,7 +597,7 @@ func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError } func (c *Client) SearchPosts(terms string) (*Result, *AppError) { - if r, err := c.DoGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { + if r, err := c.DoApiGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -568,7 +606,7 @@ func (c *Client) SearchPosts(terms string) (*Result, *AppError) { } func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) { - rq, _ := http.NewRequest("POST", c.Url+url, bytes.NewReader(data)) + rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) if len(c.AuthToken) > 0 { @@ -590,7 +628,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { if isFullUrl { rq, _ = http.NewRequest("GET", url, nil) } else { - rq, _ = http.NewRequest("GET", c.Url+"/files/get"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get"+url, nil) } if len(c.AuthToken) > 0 { @@ -609,7 +647,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { func (c *Client) GetFileInfo(url string) (*Result, *AppError) { var rq *http.Request - rq, _ = http.NewRequest("GET", c.Url+"/files/get_info"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get_info"+url, nil) if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -626,7 +664,7 @@ func (c *Client) GetFileInfo(url string) (*Result, *AppError) { } func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/files/get_public_link", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/files/get_public_link", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -635,7 +673,7 @@ func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { } func (c *Client) UpdateUser(user *User) (*Result, *AppError) { - if r, err := c.DoPost("/users/update", user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/update", user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -644,7 +682,7 @@ func (c *Client) UpdateUser(user *User) (*Result, *AppError) { } func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/update_roles", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_roles", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -656,7 +694,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = userId data["active"] = strconv.FormatBool(active) - if r, err := c.DoPost("/users/update_active", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_active", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -665,7 +703,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { } func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/update_notify", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_notify", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -679,7 +717,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) data["new_password"] = newPassword data["user_id"] = userId - if r, err := c.DoPost("/users/newpassword", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/newpassword", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -688,7 +726,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) } func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/send_password_reset", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/send_password_reset", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -697,7 +735,7 @@ func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) } func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/reset_password", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/reset_password", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -706,7 +744,7 @@ func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { } func (c *Client) GetStatuses() (*Result, *AppError) { - if r, err := c.DoGet("/users/status", "", ""); err != nil { + if r, err := c.DoApiGet("/users/status", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -715,7 +753,7 @@ func (c *Client) GetStatuses() (*Result, *AppError) { } func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/teams/me", "", etag); err != nil { + if r, err := c.DoApiGet("/teams/me", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -723,6 +761,33 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { } } +func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) { + if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil + } +} + +func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) { + if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { + if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), AccessResponseFromJson(r.Body)}, nil + } +} + func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken } diff --git a/model/oauth.go b/model/oauth.go new file mode 100644 index 000000000..3b31e677d --- /dev/null +++ b/model/oauth.go @@ -0,0 +1,151 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +type OAuthApp struct { + Id string `json:"id"` + CreatorId string `json:"creator_id"` + CreateAt int64 `json:"update_at"` + UpdateAt int64 `json:"update_at"` + ClientSecret string `json:"client_secret"` + Name string `json:"name"` + Description string `json:"description"` + CallbackUrls StringArray `json:"callback_urls"` + Homepage string `json:"homepage"` +} + +// IsValid validates the app and returns an error if it isn't configured +// correctly. +func (a *OAuthApp) IsValid() *AppError { + + if len(a.Id) != 26 { + return NewAppError("OAuthApp.IsValid", "Invalid app id", "") + } + + if a.CreateAt == 0 { + return NewAppError("OAuthApp.IsValid", "Create at must be a valid time", "app_id="+a.Id) + } + + if a.UpdateAt == 0 { + return NewAppError("OAuthApp.IsValid", "Update at must be a valid time", "app_id="+a.Id) + } + + if len(a.CreatorId) != 26 { + return NewAppError("OAuthApp.IsValid", "Invalid creator id", "app_id="+a.Id) + } + + if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 { + return NewAppError("OAuthApp.IsValid", "Invalid client secret", "app_id="+a.Id) + } + + if len(a.Name) == 0 || len(a.Name) > 64 { + return NewAppError("OAuthApp.IsValid", "Invalid name", "app_id="+a.Id) + } + + if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 { + return NewAppError("OAuthApp.IsValid", "Invalid callback urls", "app_id="+a.Id) + } + + if len(a.Homepage) == 0 || len(a.Homepage) > 256 { + return NewAppError("OAuthApp.IsValid", "Invalid homepage", "app_id="+a.Id) + } + + if len(a.Description) > 512 { + return NewAppError("OAuthApp.IsValid", "Invalid description", "app_id="+a.Id) + } + + return nil +} + +// PreSave will set the Id and ClientSecret if missing. It will also fill +// in the CreateAt, UpdateAt times. It should be run before saving the app to the db. +func (a *OAuthApp) PreSave() { + if a.Id == "" { + a.Id = NewId() + } + + if a.ClientSecret == "" { + a.ClientSecret = NewId() + } + + a.CreateAt = GetMillis() + a.UpdateAt = a.CreateAt + + if len(a.ClientSecret) > 0 { + a.ClientSecret = HashPassword(a.ClientSecret) + } +} + +// PreUpdate should be run before updating the app in the db. +func (a *OAuthApp) PreUpdate() { + a.UpdateAt = GetMillis() +} + +// ToJson convert a User to a json string +func (a *OAuthApp) ToJson() string { + b, err := json.Marshal(a) + if err != nil { + return "" + } else { + return string(b) + } +} + +// Generate a valid strong etag so the browser can cache the results +func (a *OAuthApp) Etag() string { + return Etag(a.Id, a.UpdateAt) +} + +// Remove any private data from the app object +func (a *OAuthApp) Sanitize() { + a.ClientSecret = "" +} + +func (a *OAuthApp) IsValidRedirectURL(url string) bool { + for _, u := range a.CallbackUrls { + if u == url { + return true + } + } + + return false +} + +// OAuthAppFromJson will decode the input and return a User +func OAuthAppFromJson(data io.Reader) *OAuthApp { + decoder := json.NewDecoder(data) + var app OAuthApp + err := decoder.Decode(&app) + if err == nil { + return &app + } else { + return nil + } +} + +func OAuthAppMapToJson(a map[string]*OAuthApp) string { + b, err := json.Marshal(a) + if err != nil { + return "" + } else { + return string(b) + } +} + +func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp { + decoder := json.NewDecoder(data) + var apps map[string]*OAuthApp + err := decoder.Decode(&apps) + if err == nil { + return apps + } else { + return nil + } +} diff --git a/model/oauth_test.go b/model/oauth_test.go new file mode 100644 index 000000000..2530ead98 --- /dev/null +++ b/model/oauth_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestOAuthAppJson(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + + json := a1.ToJson() + ra1 := OAuthAppFromJson(strings.NewReader(json)) + + if a1.Id != ra1.Id { + t.Fatal("ids did not match") + } +} + +func TestOAuthAppPreSave(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + a1.PreSave() + a1.Etag() + a1.Sanitize() +} + +func TestOAuthAppPreUpdate(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + a1.PreUpdate() +} + +func TestOAuthAppIsValid(t *testing.T) { + app := OAuthApp{} + + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Id = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CreateAt = 1 + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.UpdateAt = 1 + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CreatorId = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.ClientSecret = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Name = "TestOAuthApp" + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CallbackUrls = []string{"https://nowhere.com"} + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Homepage = "https://nowhere.com" + if err := app.IsValid(); err != nil { + t.Fatal() + } +} diff --git a/model/session.go b/model/session.go index c812f83e2..3c7c75eb4 100644 --- a/model/session.go +++ b/model/session.go @@ -14,6 +14,8 @@ const ( SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS SESSION_TIME_MOBILE_IN_DAYS = 30 SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS + SESSION_TIME_OAUTH_IN_DAYS = 365 + SESSION_TIME_OAUTH_IN_SECS = 60 * 60 * 24 * SESSION_TIME_OAUTH_IN_DAYS SESSION_CACHE_IN_SECS = 60 * 10 SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" @@ -23,7 +25,7 @@ const ( type Session struct { Id string `json:"id"` - AltId string `json:"alt_id"` + Token string `json:"token"` CreateAt int64 `json:"create_at"` ExpiresAt int64 `json:"expires_at"` LastActivityAt int64 `json:"last_activity_at"` @@ -31,6 +33,7 @@ type Session struct { TeamId string `json:"team_id"` DeviceId string `json:"device_id"` Roles string `json:"roles"` + IsOAuth bool `json:"is_oauth"` Props StringMap `json:"props"` } @@ -59,7 +62,7 @@ func (me *Session) PreSave() { me.Id = NewId() } - me.AltId = NewId() + me.Token = NewId() me.CreateAt = GetMillis() me.LastActivityAt = me.CreateAt @@ -70,7 +73,7 @@ func (me *Session) PreSave() { } func (me *Session) Sanitize() { - me.Id = "" + me.Token = "" } func (me *Session) IsExpired() bool { diff --git a/model/utils.go b/model/utils.go index d5122e805..04b92947b 100644 --- a/model/utils.go +++ b/model/utils.go @@ -32,6 +32,7 @@ type AppError struct { RequestId string `json:"request_id"` // The RequestId that's also set in the header StatusCode int `json:"status_code"` // The http status code Where string `json:"-"` // The function where it happened in the form of Struct.Func + IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific } func (er *AppError) Error() string { @@ -65,6 +66,7 @@ func NewAppError(where string, message string, details string) *AppError { ap.Where = where ap.DetailedError = details ap.StatusCode = 500 + ap.IsOAuth = false return ap } diff --git a/store/sql_oauth_store.go b/store/sql_oauth_store.go new file mode 100644 index 000000000..2a6fa3118 --- /dev/null +++ b/store/sql_oauth_store.go @@ -0,0 +1,334 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "strings" +) + +type SqlOAuthStore struct { + *SqlStore +} + +func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore { + as := &SqlOAuthStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.OAuthApp{}, "OAuthApps").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("CreatorId").SetMaxSize(26) + table.ColMap("ClientSecret").SetMaxSize(128) + table.ColMap("Name").SetMaxSize(64) + table.ColMap("Description").SetMaxSize(512) + table.ColMap("CallbackUrls").SetMaxSize(1024) + table.ColMap("Homepage").SetMaxSize(256) + + tableAuth := db.AddTableWithName(model.AuthData{}, "OAuthAuthData").SetKeys(false, "Code") + tableAuth.ColMap("UserId").SetMaxSize(26) + tableAuth.ColMap("ClientId").SetMaxSize(26) + tableAuth.ColMap("Code").SetMaxSize(128) + tableAuth.ColMap("RedirectUri").SetMaxSize(256) + tableAuth.ColMap("State").SetMaxSize(128) + tableAuth.ColMap("Scope").SetMaxSize(128) + + tableAccess := db.AddTableWithName(model.AccessData{}, "OAuthAccessData").SetKeys(false, "Token") + tableAccess.ColMap("AuthCode").SetMaxSize(128) + tableAccess.ColMap("Token").SetMaxSize(26) + tableAccess.ColMap("RefreshToken").SetMaxSize(26) + tableAccess.ColMap("RedirectUri").SetMaxSize(256) + } + + return as +} + +func (as SqlOAuthStore) UpgradeSchemaIfNeeded() { +} + +func (as SqlOAuthStore) CreateIndexesIfNotExists() { + as.CreateIndexIfNotExists("idx_oauthapps_creator_id", "OAuthApps", "CreatorId") + as.CreateIndexIfNotExists("idx_oauthaccessdata_auth_code", "OAuthAccessData", "AuthCode") + as.CreateIndexIfNotExists("idx_oauthauthdata_client_id", "OAuthAuthData", "Code") +} + +func (as SqlOAuthStore) SaveApp(app *model.OAuthApp) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(app.Id) > 0 { + result.Err = model.NewAppError("SqlOAuthStore.SaveApp", "Must call update for exisiting app", "app_id="+app.Id) + storeChannel <- result + close(storeChannel) + return + } + + app.PreSave() + if result.Err = app.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := as.GetMaster().Insert(app); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.SaveApp", "We couldn't save the app.", "app_id="+app.Id+", "+err.Error()) + } else { + result.Data = app + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + app.PreUpdate() + + if result.Err = app.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if oldAppResult, err := as.GetMaster().Get(model.OAuthApp{}, app.Id); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error finding the app", "app_id="+app.Id+", "+err.Error()) + } else if oldAppResult == nil { + result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't find the existing app to update", "app_id="+app.Id) + } else { + oldApp := oldAppResult.(*model.OAuthApp) + app.CreateAt = oldApp.CreateAt + app.ClientSecret = oldApp.ClientSecret + app.CreatorId = oldApp.CreatorId + + if count, err := as.GetMaster().Update(app); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error updating the app", "app_id="+app.Id+", "+err.Error()) + } else if count != 1 { + result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't update the app", "app_id="+app.Id) + } else { + result.Data = [2]*model.OAuthApp{app, oldApp} + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) GetApp(id string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := as.GetReplica().Get(model.OAuthApp{}, id); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We encounted an error finding the app", "app_id="+id+", "+err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We couldn't find the existing app", "app_id="+id) + } else { + result.Data = obj.(*model.OAuthApp) + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var apps []*model.OAuthApp + + if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "We couldn't find any existing apps", "user_id="+userId+", "+err.Error()) + } + + result.Data = apps + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = accessData.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := as.GetMaster().Insert(accessData); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.SaveAccessData", "We couldn't save the access token.", err.Error()) + } else { + result.Data = accessData + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) GetAccessData(token string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + accessData := model.AccessData{} + + if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAccessData", "We encounted an error finding the access token", err.Error()) + } else { + result.Data = &accessData + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (as SqlOAuthStore) GetAccessDataByAuthCode(authCode string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + accessData := model.AccessData{} + + if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE AuthCode = :AuthCode", map[string]interface{}{"AuthCode": authCode}); err != nil { + if strings.Contains(err.Error(), "no rows") { + result.Data = nil + } else { + result.Err = model.NewAppError("SqlOAuthStore.GetAccessDataByAuthCode", "We encountered an error finding the access token", err.Error()) + } + } else { + result.Data = &accessData + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (as SqlOAuthStore) RemoveAccessData(token string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := as.GetMaster().Exec("DELETE FROM OAuthAccessData WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.RemoveAccessData", "We couldn't remove the access token", "err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + authData.PreSave() + if result.Err = authData.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := as.GetMaster().Insert(authData); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.SaveAuthData", "We couldn't save the authorization code.", err.Error()) + } else { + result.Data = authData + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (as SqlOAuthStore) GetAuthData(code string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := as.GetReplica().Get(model.AuthData{}, code); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We encounted an error finding the authorization code", err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We couldn't find the existing authorization code", "") + } else { + result.Data = obj.(*model.AuthData) + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (as SqlOAuthStore) RemoveAuthData(code string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := as.GetMaster().Exec("DELETE FROM OAuthAuthData WHERE Code = :Code", map[string]interface{}{"Code": code}) + if err != nil { + result.Err = model.NewAppError("SqlOAuthStore.RemoveAuthData", "We couldn't remove the authorization code", "err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_oauth_store_test.go b/store/sql_oauth_store_test.go new file mode 100644 index 000000000..08e1388e0 --- /dev/null +++ b/store/sql_oauth_store_test.go @@ -0,0 +1,182 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestOAuthStoreSaveApp(t *testing.T) { + Setup() + + a1 := model.OAuthApp{} + a1.CreatorId = model.NewId() + a1.Name = "TestApp" + model.NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + + if err := (<-store.OAuth().SaveApp(&a1)).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreGetApp(t *testing.T) { + Setup() + + a1 := model.OAuthApp{} + a1.CreatorId = model.NewId() + a1.Name = "TestApp" + model.NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + Must(store.OAuth().SaveApp(&a1)) + + if err := (<-store.OAuth().GetApp(a1.Id)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.OAuth().GetAppByUser(a1.CreatorId)).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreUpdateApp(t *testing.T) { + Setup() + + a1 := model.OAuthApp{} + a1.CreatorId = model.NewId() + a1.Name = "TestApp" + model.NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + Must(store.OAuth().SaveApp(&a1)) + + a1.CreateAt = 1 + a1.ClientSecret = "pwd" + a1.CreatorId = "12345678901234567890123456" + a1.Name = "NewName" + if result := <-store.OAuth().UpdateApp(&a1); result.Err != nil { + t.Fatal(result.Err) + } else { + ua1 := (result.Data.([2]*model.OAuthApp)[0]) + if ua1.Name != "NewName" { + t.Fatal("name did not update") + } + if ua1.CreateAt == 1 { + t.Fatal("create at should not have updated") + } + if ua1.ClientSecret == "pwd" { + t.Fatal("client secret should not have updated") + } + if ua1.CreatorId == "12345678901234567890123456" { + t.Fatal("creator id should not have updated") + } + } +} + +func TestOAuthStoreSaveAccessData(t *testing.T) { + Setup() + + a1 := model.AccessData{} + a1.AuthCode = model.NewId() + a1.Token = model.NewId() + a1.RefreshToken = model.NewId() + + if err := (<-store.OAuth().SaveAccessData(&a1)).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreGetAccessData(t *testing.T) { + Setup() + + a1 := model.AccessData{} + a1.AuthCode = model.NewId() + a1.Token = model.NewId() + a1.RefreshToken = model.NewId() + Must(store.OAuth().SaveAccessData(&a1)) + + if result := <-store.OAuth().GetAccessData(a1.Token); result.Err != nil { + t.Fatal(result.Err) + } else { + ra1 := result.Data.(*model.AccessData) + if a1.Token != ra1.Token { + t.Fatal("tokens didn't match") + } + } + + if err := (<-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.OAuth().GetAccessDataByAuthCode("junk")).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreRemoveAccessData(t *testing.T) { + Setup() + + a1 := model.AccessData{} + a1.AuthCode = model.NewId() + a1.Token = model.NewId() + a1.RefreshToken = model.NewId() + Must(store.OAuth().SaveAccessData(&a1)) + + if err := (<-store.OAuth().RemoveAccessData(a1.Token)).Err; err != nil { + t.Fatal(err) + } + + if result := <-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode); result.Err != nil { + t.Fatal(result.Err) + } else { + if result.Data != nil { + t.Fatal("did not delete access token") + } + } +} + +func TestOAuthStoreSaveAuthData(t *testing.T) { + Setup() + + a1 := model.AuthData{} + a1.ClientId = model.NewId() + a1.UserId = model.NewId() + a1.Code = model.NewId() + + if err := (<-store.OAuth().SaveAuthData(&a1)).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreGetAuthData(t *testing.T) { + Setup() + + a1 := model.AuthData{} + a1.ClientId = model.NewId() + a1.UserId = model.NewId() + a1.Code = model.NewId() + Must(store.OAuth().SaveAuthData(&a1)) + + if err := (<-store.OAuth().GetAuthData(a1.Code)).Err; err != nil { + t.Fatal(err) + } +} + +func TestOAuthStoreRemoveAuthData(t *testing.T) { + Setup() + + a1 := model.AuthData{} + a1.ClientId = model.NewId() + a1.UserId = model.NewId() + a1.Code = model.NewId() + Must(store.OAuth().SaveAuthData(&a1)) + + if err := (<-store.OAuth().RemoveAuthData(a1.Code)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.OAuth().GetAuthData(a1.Code)).Err; err == nil { + t.Fatal("should have errored - auth code removed") + } +} diff --git a/store/sql_session_store.go b/store/sql_session_store.go index 12004ab78..c1d2c852b 100644 --- a/store/sql_session_store.go +++ b/store/sql_session_store.go @@ -18,7 +18,7 @@ func NewSqlSessionStore(sqlStore *SqlStore) SessionStore { for _, db := range sqlStore.GetAllConns() { table := db.AddTableWithName(model.Session{}, "Sessions").SetKeys(false, "Id") table.ColMap("Id").SetMaxSize(26) - table.ColMap("AltId").SetMaxSize(26) + table.ColMap("Token").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26) table.ColMap("TeamId").SetMaxSize(26) table.ColMap("DeviceId").SetMaxSize(128) @@ -34,7 +34,7 @@ func (me SqlSessionStore) UpgradeSchemaIfNeeded() { func (me SqlSessionStore) CreateIndexesIfNotExists() { me.CreateIndexIfNotExists("idx_sessions_user_id", "Sessions", "UserId") - me.CreateIndexIfNotExists("idx_sessions_alt_id", "Sessions", "AltId") + me.CreateIndexIfNotExists("idx_sessions_token", "Sessions", "Token") } func (me SqlSessionStore) Save(session *model.Session) StoreChannel { @@ -70,19 +70,21 @@ func (me SqlSessionStore) Save(session *model.Session) StoreChannel { return storeChannel } -func (me SqlSessionStore) Get(id string) StoreChannel { +func (me SqlSessionStore) Get(sessionIdOrToken string) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - if obj, err := me.GetReplica().Get(model.Session{}, id); err != nil { - result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "id="+id+", "+err.Error()) - } else if obj == nil { - result.Err = model.NewAppError("SqlSessionStore.Get", "We couldn't find the existing session", "id="+id) + var sessions []*model.Session + + if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE Token = :Token OR Id = :Id LIMIT 1", map[string]interface{}{"Token": sessionIdOrToken, "Id": sessionIdOrToken}); err != nil { + result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken+", "+err.Error()) + } else if sessions == nil || len(sessions) == 0 { + result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken) } else { - result.Data = obj.(*model.Session) + result.Data = sessions[0] } storeChannel <- result @@ -120,15 +122,15 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel { return storeChannel } -func (me SqlSessionStore) Remove(sessionIdOrAlt string) StoreChannel { +func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = :Id Or AltId = :AltId", map[string]interface{}{"Id": sessionIdOrAlt, "AltId": sessionIdOrAlt}) + _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = :Id Or Token = :Token", map[string]interface{}{"Id": sessionIdOrToken, "Token": sessionIdOrToken}) if err != nil { - result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrAlt+", err="+err.Error()) + result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrToken+", err="+err.Error()) } storeChannel <- result @@ -181,7 +183,6 @@ func (me SqlSessionStore) UpdateRoles(userId, roles string) StoreChannel { go func() { result := StoreResult{} - if _, err := me.GetMaster().Exec("UPDATE Sessions SET Roles = :Roles WHERE UserId = :UserId", map[string]interface{}{"Roles": roles, "UserId": userId}); err != nil { result.Err = model.NewAppError("SqlSessionStore.UpdateRoles", "We couldn't update the roles", "userId="+userId) } else { diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go index 581aff971..4ae680556 100644 --- a/store/sql_session_store_test.go +++ b/store/sql_session_store_test.go @@ -80,7 +80,7 @@ func TestSessionRemove(t *testing.T) { } } -func TestSessionRemoveAlt(t *testing.T) { +func TestSessionRemoveToken(t *testing.T) { Setup() s1 := model.Session{} @@ -96,7 +96,7 @@ func TestSessionRemoveAlt(t *testing.T) { } } - Must(store.Session().Remove(s1.AltId)) + Must(store.Session().Remove(s1.Token)) if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil { t.Fatal("should have been removed") diff --git a/store/sql_store.go b/store/sql_store.go index 98c67d668..c0b3c2021 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -38,6 +38,7 @@ type SqlStore struct { user UserStore audit AuditStore session SessionStore + oauth OAuthStore } func NewSqlStore() Store { @@ -55,28 +56,36 @@ func NewSqlStore() Store { utils.Cfg.SqlSettings.Trace) } + // Temporary upgrade code, remove after 0.8.0 release + if sqlStore.DoesColumnExist("Sessions", "AltId") { + sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions") + } + sqlStore.team = NewSqlTeamStore(sqlStore) sqlStore.channel = NewSqlChannelStore(sqlStore) sqlStore.post = NewSqlPostStore(sqlStore) sqlStore.user = NewSqlUserStore(sqlStore) sqlStore.audit = NewSqlAuditStore(sqlStore) sqlStore.session = NewSqlSessionStore(sqlStore) + sqlStore.oauth = NewSqlOAuthStore(sqlStore) sqlStore.master.CreateTablesIfNotExists() - sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() - sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() - sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists() - sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists() - sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists() - sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists() - sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded() sqlStore.channel.(*SqlChannelStore).UpgradeSchemaIfNeeded() sqlStore.post.(*SqlPostStore).UpgradeSchemaIfNeeded() sqlStore.user.(*SqlUserStore).UpgradeSchemaIfNeeded() sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded() sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded() + sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded() + + sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() + sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() + sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists() + sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists() + sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists() + sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists() + sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists() return sqlStore } @@ -363,6 +372,10 @@ func (ss SqlStore) Audit() AuditStore { return ss.audit } +func (ss SqlStore) OAuth() OAuthStore { + return ss.oauth +} + type mattermConverter struct{} func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { diff --git a/store/store.go b/store/store.go index 959e93fa4..0218bc757 100644 --- a/store/store.go +++ b/store/store.go @@ -34,6 +34,7 @@ type Store interface { User() UserStore Audit() AuditStore Session() SessionStore + OAuth() OAuthStore Close() } @@ -104,9 +105,9 @@ type UserStore interface { type SessionStore interface { Save(session *model.Session) StoreChannel - Get(id string) StoreChannel + Get(sessionIdOrToken string) StoreChannel GetSessions(userId string) StoreChannel - Remove(sessionIdOrAlt string) StoreChannel + Remove(sessionIdOrToken string) StoreChannel UpdateLastActivityAt(sessionId string, time int64) StoreChannel UpdateRoles(userId string, roles string) StoreChannel } @@ -115,3 +116,17 @@ type AuditStore interface { Save(audit *model.Audit) StoreChannel Get(user_id string, limit int) StoreChannel } + +type OAuthStore interface { + SaveApp(app *model.OAuthApp) StoreChannel + UpdateApp(app *model.OAuthApp) StoreChannel + GetApp(id string) StoreChannel + GetAppByUser(userId string) StoreChannel + SaveAuthData(authData *model.AuthData) StoreChannel + GetAuthData(code string) StoreChannel + RemoveAuthData(code string) StoreChannel + SaveAccessData(accessData *model.AccessData) StoreChannel + GetAccessData(token string) StoreChannel + GetAccessDataByAuthCode(authCode string) StoreChannel + RemoveAccessData(token string) StoreChannel +} diff --git a/utils/config.go b/utils/config.go index a1d282c29..212a1a559 100644 --- a/utils/config.go +++ b/utils/config.go @@ -4,10 +4,13 @@ package utils import ( - l4g "code.google.com/p/log4go" "encoding/json" + "fmt" "os" "path/filepath" + "strconv" + + l4g "code.google.com/p/log4go" ) const ( @@ -18,20 +21,21 @@ const ( ) type ServiceSettings struct { - SiteName string - Mode string - AllowTesting bool - UseSSL bool - Port string - Version string - InviteSalt string - PublicLinkSalt string - ResetSalt string - AnalyticsUrl string - UseLocalStorage bool - StorageDirectory string - AllowedLoginAttempts int - DisableEmailSignUp bool + SiteName string + Mode string + AllowTesting bool + UseSSL bool + Port string + Version string + InviteSalt string + PublicLinkSalt string + ResetSalt string + AnalyticsUrl string + UseLocalStorage bool + StorageDirectory string + AllowedLoginAttempts int + DisableEmailSignUp bool + EnableOAuthServiceProvider bool } type SSOSetting struct { @@ -109,15 +113,15 @@ type PrivacySettings struct { ShowFullName bool } +type ClientSettings struct { + SegmentDeveloperKey string + GoogleDeveloperKey string +} + type TeamSettings struct { MaxUsersPerTeam int AllowPublicLink bool AllowValetDefault bool - TermsLink string - PrivacyLink string - AboutLink string - HelpLink string - ReportProblemLink string TourLink string DefaultThemeColor string DisableTeamCreation bool @@ -133,6 +137,7 @@ type Config struct { EmailSettings EmailSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings + ClientSettings ClientSettings TeamSettings TeamSettings SSOSettings map[string]SSOSetting } @@ -147,6 +152,8 @@ func (o *Config) ToJson() string { } var Cfg *Config = &Config{} +var CfgLastModified int64 = 0 +var ClientProperties map[string]string = map[string]string{} var SanitizeOptions map[string]bool = map[string]bool{} func FindConfigFile(fileName string) string { @@ -242,22 +249,49 @@ func LoadConfig(fileName string) { panic("Error decoding config file=" + fileName + ", err=" + err.Error()) } + if info, err := file.Stat(); err != nil { + panic("Error getting config info file=" + fileName + ", err=" + err.Error()) + } else { + CfgLastModified = info.ModTime().Unix() + } + configureLog(&config.LogSettings) Cfg = &config - SanitizeOptions = getSanitizeOptions() + SanitizeOptions = getSanitizeOptions(Cfg) + ClientProperties = getClientProperties(Cfg) } -func getSanitizeOptions() map[string]bool { +func getSanitizeOptions(c *Config) map[string]bool { options := map[string]bool{} - options["fullname"] = Cfg.PrivacySettings.ShowFullName - options["email"] = Cfg.PrivacySettings.ShowEmailAddress - options["skypeid"] = Cfg.PrivacySettings.ShowSkypeId - options["phonenumber"] = Cfg.PrivacySettings.ShowPhoneNumber + options["fullname"] = c.PrivacySettings.ShowFullName + options["email"] = c.PrivacySettings.ShowEmailAddress + options["skypeid"] = c.PrivacySettings.ShowSkypeId + options["phonenumber"] = c.PrivacySettings.ShowPhoneNumber return options } +func getClientProperties(c *Config) map[string]string { + props := make(map[string]string) + + props["Version"] = c.ServiceSettings.Version + props["SiteName"] = c.ServiceSettings.SiteName + props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail) + props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) + props["AllowPublicLink"] = strconv.FormatBool(c.TeamSettings.AllowPublicLink) + props["SegmentDeveloperKey"] = c.ClientSettings.SegmentDeveloperKey + props["GoogleDeveloperKey"] = c.ClientSettings.GoogleDeveloperKey + props["AnalyticsUrl"] = c.ServiceSettings.AnalyticsUrl + props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail) + props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight) + props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) + props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) + props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) + + return props +} + func IsS3Configured() bool { if Cfg.AWSSettings.S3AccessKeyId == "" || Cfg.AWSSettings.S3SecretAccessKey == "" || Cfg.AWSSettings.S3Region == "" || Cfg.AWSSettings.S3Bucket == "" { return false diff --git a/web/react/components/authorize.jsx b/web/react/components/authorize.jsx new file mode 100644 index 000000000..dd4479ad4 --- /dev/null +++ b/web/react/components/authorize.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../utils/client.jsx'); + +export default class Authorize extends React.Component { + constructor(props) { + super(props); + + this.handleAllow = this.handleAllow.bind(this); + this.handleDeny = this.handleDeny.bind(this); + + this.state = {}; + } + handleAllow() { + const responseType = this.props.responseType; + const clientId = this.props.clientId; + const redirectUri = this.props.redirectUri; + const state = this.props.state; + const scope = this.props.scope; + + Client.allowOAuth2(responseType, clientId, redirectUri, state, scope, + (data) => { + if (data.redirect) { + window.location.replace(data.redirect); + } + }, + () => {} + ); + } + handleDeny() { + window.location.replace(this.props.redirectUri + '?error=access_denied'); + } + render() { + return ( + <div className='authorize-box'> + <div className='authorize-inner'> + <h3>{'An application would like to connect to your '}{this.props.teamName}{' account'}</h3> + <label>{'The app '}{this.props.appName}{' would like the ability to access and modify your basic information.'}</label> + <br/> + <br/> + <label>{'Allow '}{this.props.appName}{' access?'}</label> + <br/> + <button + type='submit' + className='btn authorize-btn' + onClick={this.handleDeny} + > + {'Deny'} + </button> + <button + type='submit' + className='btn btn-primary authorize-btn' + onClick={this.handleAllow} + > + {'Allow'} + </button> + </div> + </div> + ); + } +} + +Authorize.propTypes = { + appName: React.PropTypes.string, + teamName: React.PropTypes.string, + responseType: React.PropTypes.string, + clientId: React.PropTypes.string, + redirectUri: React.PropTypes.string, + state: React.PropTypes.string, + scope: React.PropTypes.string +}; diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 95948c8dd..92123956f 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -1,8 +1,6 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -import {config} from '../utils/config.js'; - export default class EmailVerify extends React.Component { constructor(props) { super(props); @@ -19,10 +17,10 @@ export default class EmailVerify extends React.Component { var body = ''; var resend = ''; if (this.props.isVerified === 'true') { - title = config.SiteName + ' Email Verified'; + title = global.window.config.SiteName + ' Email Verified'; body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>; } else { - title = config.SiteName + ' Email Not Verified'; + title = global.window.config.SiteName + ' Email Not Verified'; body = <p>Please verify your email address. Check your inbox for an email.</p>; resend = ( <button diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx index 52988886c..eb2683a88 100644 --- a/web/react/components/find_team.jsx +++ b/web/react/components/find_team.jsx @@ -3,7 +3,6 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); -import {strings} from '../utils/config.js'; export default class FindTeam extends React.Component { constructor(props) { @@ -51,8 +50,8 @@ export default class FindTeam extends React.Component { if (this.state.sent) { return ( <div> - <h4>{'Find Your ' + utils.toTitleCase(strings.Team)}</h4> - <p>{'An email was sent with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p> + <h4>{'Find Your team'}</h4> + <p>{'An email was sent with links to any teams to which you are a member.'}</p> </div> ); } @@ -61,7 +60,7 @@ export default class FindTeam extends React.Component { <div> <h4>Find Your Team</h4> <form onSubmit={this.handleSubmit}> - <p>{'Get an email with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p> + <p>{'Get an email with links to any teams to which you are a member.'}</p> <div className='form-group'> <label className='control-label'>Email</label> <div className={emailErrorClass}> diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 1f25ea0b7..5d8b13f00 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -import {strings} from '../utils/config.js'; export default class GetLinkModal extends React.Component { constructor(props) { @@ -76,9 +75,9 @@ export default class GetLinkModal extends React.Component { </div> <div className='modal-body'> <p> - Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site. + Send teammates the link below for them to sign-up to this team site. <br /><br /> - Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}. + Be careful not to share this link publicly, since anyone with the link can join your team. </p> <textarea className='form-control no-resize' diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index c1cfa7800..650a72516 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -2,11 +2,9 @@ // See License.txt for license information. var utils = require('../utils/utils.jsx'); -var ConfigStore = require('../stores/config_store.jsx'); var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); -import {config} from '../utils/config.js'; export default class InviteMemberModal extends React.Component { constructor(props) { @@ -23,7 +21,7 @@ export default class InviteMemberModal extends React.Component { emailErrors: {}, firstNameErrors: {}, lastNameErrors: {}, - emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) + emailEnabled: !global.window.config.ByPassEmail }; } @@ -79,23 +77,9 @@ export default class InviteMemberModal extends React.Component { emailErrors[index] = ''; } - if (config.AllowInviteNames) { - invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim(); - if (!invite.firstName && config.RequireInviteNames) { - firstNameErrors[index] = 'This is a required field'; - valid = false; - } else { - firstNameErrors[index] = ''; - } + invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim(); - invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim(); - if (!invite.lastName && config.RequireInviteNames) { - lastNameErrors[index] = 'This is a required field'; - valid = false; - } else { - lastNameErrors[index] = ''; - } - } + invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim(); invites.push(invite); } @@ -143,10 +127,8 @@ export default class InviteMemberModal extends React.Component { for (var i = 0; i < inviteIds.length; i++) { var index = inviteIds[i]; React.findDOMNode(this.refs['email' + index]).value = ''; - if (config.AllowInviteNames) { - React.findDOMNode(this.refs['first_name' + index]).value = ''; - React.findDOMNode(this.refs['last_name' + index]).value = ''; - } + React.findDOMNode(this.refs['first_name' + index]).value = ''; + React.findDOMNode(this.refs['last_name' + index]).value = ''; } this.setState({ @@ -210,44 +192,43 @@ export default class InviteMemberModal extends React.Component { } var nameFields = null; - if (config.AllowInviteNames) { - var firstNameClass = 'form-group'; - if (firstNameError) { - firstNameClass += ' has-error'; - } - var lastNameClass = 'form-group'; - if (lastNameError) { - lastNameClass += ' has-error'; - } - nameFields = (<div className='row--invite'> - <div className='col-sm-6'> - <div className={firstNameClass}> - <input - type='text' - className='form-control' - ref={'first_name' + index} - placeholder='First name' - maxLength='64' - disabled={!this.state.emailEnabled} - /> - {firstNameError} - </div> + + var firstNameClass = 'form-group'; + if (firstNameError) { + firstNameClass += ' has-error'; + } + var lastNameClass = 'form-group'; + if (lastNameError) { + lastNameClass += ' has-error'; + } + nameFields = (<div className='row--invite'> + <div className='col-sm-6'> + <div className={firstNameClass}> + <input + type='text' + className='form-control' + ref={'first_name' + index} + placeholder='First name' + maxLength='64' + disabled={!this.state.emailEnabled} + /> + {firstNameError} </div> - <div className='col-sm-6'> - <div className={lastNameClass}> - <input - type='text' - className='form-control' - ref={'last_name' + index} - placeholder='Last name' - maxLength='64' - disabled={!this.state.emailEnabled} - /> - {lastNameError} - </div> + </div> + <div className='col-sm-6'> + <div className={lastNameClass}> + <input + type='text' + className='form-control' + ref={'last_name' + index} + placeholder='Last name' + maxLength='64' + disabled={!this.state.emailEnabled} + /> + {lastNameError} </div> - </div>); - } + </div> + </div>); inviteSections[index] = ( <div key={'key' + index}> diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index b20c62833..ffc07a4dd 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,7 +6,6 @@ const Client = require('../utils/client.jsx'); const UserStore = require('../stores/user_store.jsx'); const BrowserStore = require('../stores/browser_store.jsx'); const Constants = require('../utils/constants.jsx'); -import {config, strings} from '../utils/config.js'; export default class Login extends React.Component { constructor(props) { @@ -177,7 +176,7 @@ export default class Login extends React.Component { <div className='signup-team__container'> <h5 className='margin--less'>Sign in to:</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> <form onSubmit={this.handleSubmit}> <div className={'form-group' + errorClass}> {serverError} @@ -185,11 +184,11 @@ export default class Login extends React.Component { {loginMessage} {emailSignup} <div className='form-group margin--extra form-group--small'> - <span><a href='/find_team'>{'Find other ' + strings.TeamPlural}</a></span> + <span><a href='/find_team'>{'Find other teams'}</a></span> </div> {forgotPassword} <div className='margin--extra'> - <span>{'Want to create your own ' + strings.Team + '? '} + <span>{'Want to create your own team? '} <a href='/' className='signup-team-login' diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 99cdfa1ad..b7566cfb9 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -7,7 +7,6 @@ var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); -import {config} from '../utils/config.js'; function getStateFromStores() { return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; @@ -188,7 +187,7 @@ export default class NavbarDropdown extends React.Component { <li> <a target='_blank' - href={config.HelpLink} + href='/static/help/help.html' > Help </a> @@ -196,7 +195,7 @@ export default class NavbarDropdown extends React.Component { <li> <a target='_blank' - href={config.ReportProblemLink} + href='/static/help/report_problem.html' > Report a Problem </a> diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 1b579efbc..dae582627 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. var client = require('../utils/client.jsx'); -import {config} from '../utils/config.js'; export default class PasswordResetForm extends React.Component { constructor(props) { @@ -62,7 +61,7 @@ export default class PasswordResetForm extends React.Component { <div className='signup-team__container'> <h3>Password Reset</h3> <form onSubmit={this.handlePasswordReset}> - <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + config.SiteName + ' account.'}</p> + <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.config.SiteName + ' account.'}</p> <div className={formClass}> <input type='password' diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index fb9522afb..ec873dd00 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -25,7 +25,7 @@ export default class PopoverListMembers extends React.Component { $('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true}); $('body').on('click', function onClick(e) { - if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) { + if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) { $('#member_popover').popover('hide'); } }); diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index e6aa3f8df..faa5e5f0b 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -15,8 +15,6 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -import {strings} from '../utils/config.js'; - export default class PostList extends React.Component { constructor(props) { super(props); @@ -347,7 +345,7 @@ export default class PostList extends React.Component { return ( <div className='channel-intro'> - <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p> + <p className='channel-intro-text'>{'This is the start of your private message history with this teammate. Private messages and files shared here are not shown to people outside this area.'}</p> </div> ); } @@ -369,7 +367,7 @@ export default class PostList extends React.Component { <p className='channel-intro__content'> Welcome to {channel.display_name}! <br/><br/> - This is the first channel {strings.Team}mates see when they + This is the first channel teammates see when they <br/> sign up - use it for posting updates everyone needs to know. <br/><br/> diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx new file mode 100644 index 000000000..3dd5c094e --- /dev/null +++ b/web/react/components/register_app_modal.jsx @@ -0,0 +1,249 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../utils/client.jsx'); + +export default class RegisterAppModal extends React.Component { + constructor() { + super(); + + this.register = this.register.bind(this); + this.onHide = this.onHide.bind(this); + this.save = this.save.bind(this); + + this.state = {clientId: '', clientSecret: '', saved: false}; + } + componentDidMount() { + $(React.findDOMNode(this)).on('hide.bs.modal', this.onHide); + } + register() { + var state = this.state; + state.serverError = null; + + var app = {}; + + var name = this.refs.name.getDOMNode().value; + if (!name || name.length === 0) { + state.nameError = 'Application name must be filled in.'; + this.setState(state); + return; + } + state.nameError = null; + app.name = name; + + var homepage = this.refs.homepage.getDOMNode().value; + if (!homepage || homepage.length === 0) { + state.homepageError = 'Homepage must be filled in.'; + this.setState(state); + return; + } + state.homepageError = null; + app.homepage = homepage; + + var desc = this.refs.desc.getDOMNode().value; + app.description = desc; + + var rawCallbacks = this.refs.callback.getDOMNode().value.trim(); + if (!rawCallbacks || rawCallbacks.length === 0) { + state.callbackError = 'At least one callback URL must be filled in.'; + this.setState(state); + return; + } + state.callbackError = null; + app.callback_urls = rawCallbacks.split('\n'); + + Client.registerOAuthApp(app, + (data) => { + state.clientId = data.id; + state.clientSecret = data.client_secret; + this.setState(state); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + onHide(e) { + if (!this.state.saved && this.state.clientId !== '') { + e.preventDefault(); + return; + } + + this.setState({clientId: '', clientSecret: '', saved: false}); + } + save() { + this.setState({saved: this.refs.save.getDOMNode().checked}); + } + render() { + var nameError; + if (this.state.nameError) { + nameError = <div className='form-group has-error'><label className='control-label'>{this.state.nameError}</label></div>; + } + var homepageError; + if (this.state.homepageError) { + homepageError = <div className='form-group has-error'><label className='control-label'>{this.state.homepageError}</label></div>; + } + var callbackError; + if (this.state.callbackError) { + callbackError = <div className='form-group has-error'><label className='control-label'>{this.state.callbackError}</label></div>; + } + var serverError; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var body = ''; + if (this.state.clientId === '') { + body = ( + <div className='form-group user-settings'> + <h3>{'Register a New Application'}</h3> + <br/> + <label className='col-sm-4 control-label'>{'Application Name'}</label> + <div className='col-sm-7'> + <input + ref='name' + className='form-control' + type='text' + placeholder='Required' + /> + {nameError} + </div> + <br/> + <br/> + <label className='col-sm-4 control-label'>{'Homepage URL'}</label> + <div className='col-sm-7'> + <input + ref='homepage' + className='form-control' + type='text' + placeholder='Required' + /> + {homepageError} + </div> + <br/> + <br/> + <label className='col-sm-4 control-label'>{'Description'}</label> + <div className='col-sm-7'> + <input + ref='desc' + className='form-control' + type='text' + placeholder='Optional' + /> + </div> + <br/> + <br/> + <label className='col-sm-4 control-label'>{'Callback URL'}</label> + <div className='col-sm-7'> + <textarea + ref='callback' + className='form-control' + type='text' + placeholder='Required' + rows='5' + /> + {callbackError} + </div> + <br/> + <br/> + <br/> + <br/> + <br/> + {serverError} + <a + className='btn btn-sm theme pull-right' + href='#' + data-dismiss='modal' + aria-label='Close' + > + {'Cancel'} + </a> + <a + className='btn btn-sm btn-primary pull-right' + onClick={this.register} + > + {'Register'} + </a> + </div> + ); + } else { + var btnClass = ' disabled'; + if (this.state.saved) { + btnClass = ''; + } + + body = ( + <div className='form-group user-settings'> + <h3>{'Your Application Credentials'}</h3> + <br/> + <br/> + <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label> + <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label> + <br/> + <br/> + <br/> + <br/> + <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong> + <br/> + <br/> + <div className='checkbox'> + <label> + <input + ref='save' + type='checkbox' + checked={this.state.saved} + onClick={this.save} + > + {'I have saved both my Client Id and Client Secret somewhere safe'} + </input> + </label> + </div> + <a + className={'btn btn-sm btn-primary pull-right' + btnClass} + href='#' + data-dismiss='modal' + aria-label='Close' + > + {'Close'} + </a> + </div> + ); + } + + return ( + <div + className='modal fade' + ref='modal' + id='register_app' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'x'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + {'Developer Applications'} + </h4> + </div> + <div className='modal-body'> + {body} + </div> + </div> + </div> + </div> + ); + } +} + diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index a53112651..ddad4fd53 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -1,8 +1,6 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -import {config} from '../utils/config.js'; - export default class SettingPicture extends React.Component { constructor(props) { super(props); @@ -81,7 +79,7 @@ export default class SettingPicture extends React.Component { >Save</a> ); } - var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + config.ProfileWidth + 'px in width and ' + config.ProfileHeight + 'px height.'; + var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.config.ProfileWidth + 'px in width and ' + global.window.config.ProfileHeight + 'px height.'; var self = this; return ( diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 0056d7a2f..959411f1e 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -3,7 +3,6 @@ var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); -import {config} from '../utils/config.js'; export default class SidebarHeader extends React.Component { constructor(props) { @@ -59,7 +58,7 @@ export default class SidebarHeader extends React.Component { } SidebarHeader.defaultProps = { - teamDisplayName: config.SiteName, + teamDisplayName: global.window.config.SiteName, teamType: '' }; SidebarHeader.propTypes = { diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index bd10a6ef1..5ecd502ba 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -4,7 +4,6 @@ var UserStore = require('../stores/user_store.jsx'); var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); -import {config} from '../utils/config.js'; export default class SidebarRightMenu extends React.Component { constructor(props) { @@ -75,8 +74,8 @@ export default class SidebarRightMenu extends React.Component { } var siteName = ''; - if (config.SiteName != null) { - siteName = config.SiteName; + if (global.window.config.SiteName != null) { + siteName = global.window.config.SiteName; } var teamDisplayName = siteName; if (this.props.teamDisplayName) { diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index dc0d1d376..9c03c5c2f 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -4,7 +4,6 @@ var WelcomePage = require('./team_signup_welcome_page.jsx'); var TeamDisplayNamePage = require('./team_signup_display_name_page.jsx'); var TeamURLPage = require('./team_signup_url_page.jsx'); -var AllowedDomainsPage = require('./team_signup_allowed_domains_page.jsx'); var SendInivtesPage = require('./team_signup_send_invites_page.jsx'); var UsernamePage = require('./team_signup_username_page.jsx'); var PasswordPage = require('./team_signup_password_page.jsx'); @@ -70,15 +69,6 @@ export default class SignupTeamComplete extends React.Component { ); } - if (this.state.wizard === 'allowed_domains') { - return ( - <AllowedDomainsPage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - if (this.state.wizard === 'send_invites') { return ( <SendInivtesPage diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 6e71eae32..19c3b2d22 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -6,7 +6,6 @@ var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var Constants = require('../utils/constants.jsx'); -import {config} from '../utils/config.js'; export default class SignupUserComplete extends React.Component { constructor(props) { @@ -136,7 +135,7 @@ export default class SignupUserComplete extends React.Component { // set up the email entry and hide it if an email was provided var yourEmailIs = ''; if (this.state.user.email) { - yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {config.SiteName}.</span>; + yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {global.window.config.SiteName}.</span>; } var emailContainerStyle = 'margin--extra'; @@ -237,11 +236,6 @@ export default class SignupUserComplete extends React.Component { ); } - var termsDisclaimer = null; - if (config.ShowTermsDuringSignup) { - termsDisclaimer = <p>By creating an account and using Mattermost you are agreeing to our <a href={config.TermsLink}>Terms of Service</a>. If you do not agree, you cannot use this service.</p>; - } - return ( <div> <form> @@ -251,12 +245,11 @@ export default class SignupUserComplete extends React.Component { /> <h5 className='margin--less'>Welcome to:</h5> <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> <h4 className='color--light'>Let's create your account</h4> {signupMessage} {emailSignup} {serverError} - {termsDisclaimer} </form> </div> ); diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 25139bb95..ca438df78 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -6,7 +6,6 @@ const SettingItemMax = require('./setting_item_max.jsx'); const Client = require('../utils/client.jsx'); const Utils = require('../utils/utils.jsx'); -import {strings} from '../utils/config.js'; export default class GeneralTab extends React.Component { constructor(props) { @@ -30,7 +29,7 @@ export default class GeneralTab extends React.Component { state.clientError = 'This field is required'; valid = false; } else if (name === this.props.teamDisplayName) { - state.clientError = 'Please choose a new name for your ' + strings.Team; + state.clientError = 'Please choose a new name for your team'; valid = false; } else { state.clientError = ''; @@ -99,7 +98,7 @@ export default class GeneralTab extends React.Component { if (this.props.activeSection === 'name') { let inputs = []; - let teamNameLabel = Utils.toTitleCase(strings.Team) + ' Name'; + let teamNameLabel = 'Team Name'; if (Utils.isMobile()) { teamNameLabel = ''; } @@ -123,7 +122,7 @@ export default class GeneralTab extends React.Component { nameSection = ( <SettingItemMax - title={`${Utils.toTitleCase(strings.Team)} Name`} + title={`Team Name`} inputs={inputs} submit={this.handleNameSubmit} server_error={serverError} @@ -136,7 +135,7 @@ export default class GeneralTab extends React.Component { nameSection = ( <SettingItemMin - title={`${Utils.toTitleCase(strings.Team)} Name`} + title={`Team Name`} describe={describe} updateSection={this.onUpdateSection} /> diff --git a/web/react/components/team_signup_allowed_domains_page.jsx b/web/react/components/team_signup_allowed_domains_page.jsx deleted file mode 100644 index 721fa142a..000000000 --- a/web/react/components/team_signup_allowed_domains_page.jsx +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var Client = require('../utils/client.jsx'); -import {strings} from '../utils/config.js'; - -export default class TeamSignupAllowedDomainsPage extends React.Component { - constructor(props) { - super(props); - - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - - this.state = {}; - } - submitBack(e) { - e.preventDefault(); - this.props.state.wizard = 'team_url'; - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - if (React.findDOMNode(this.refs.open_network).checked) { - this.props.state.wizard = 'send_invites'; - this.props.state.team.type = 'O'; - this.props.updateParent(this.props.state); - return; - } - - if (React.findDOMNode(this.refs.allow).checked) { - var name = React.findDOMNode(this.refs.name).value.trim(); - var domainRegex = /^\w+\.\w+$/; - if (!name) { - this.setState({nameError: 'This field is required'}); - return; - } - - if (!name.trim().match(domainRegex)) { - this.setState({nameError: 'The domain doesn\'t appear valid'}); - return; - } - - this.props.state.wizard = 'send_invites'; - this.props.state.team.allowed_domains = name; - this.props.state.team.type = 'I'; - this.props.updateParent(this.props.state); - } else { - this.props.state.wizard = 'send_invites'; - this.props.state.team.type = 'I'; - this.props.updateParent(this.props.state); - } - } - render() { - Client.track('signup', 'signup_team_04_allow_domains'); - - var nameError = null; - var nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = <label className='control-label'>{this.state.nameError}</label>; - nameDivClass += ' has-error'; - } - - return ( - <div> - <form> - <img - className='signup-team-logo' - src='/static/images/logo.png' - /> - <h2>Email Domain</h2> - <p> - <div className='checkbox'> - <label> - <input - type='checkbox' - ref='allow' - defaultChecked={true} - /> - {' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'} - </label> - </div> - </p> - <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p> - <h4>{'Your ' + strings.Team + '\'s domain for emails'}</h4> - <div className={nameDivClass}> - <div className='row'> - <div className='col-sm-9'> - <div className='input-group'> - <span className='input-group-addon'>@</span> - <input - type='text' - ref='name' - className='form-control' - placeholder='' - maxLength='128' - defaultValue={this.props.state.team.allowed_domains} - autoFocus={true} - onFocus={this.handleFocus} - /> - </div> - </div> - </div> - {nameError} - </div> - <p>To allow signups from multiple domains, separate each with a comma.</p> - <p> - <div className='checkbox'> - <label> - <input - type='checkbox' - ref='open_network' - defaultChecked={this.props.state.team.type === 'O'} - /> Allow anyone to signup to this domain without an invitation.</label> - </div> - </p> - <button - type='button' - className='btn btn-default' - onClick={this.submitBack} - > - <i className='glyphicon glyphicon-chevron-left'></i> Back - </button> - <button - type='submit' - className='btn-primary btn' - onClick={this.submitNext} - > - Next<i className='glyphicon glyphicon-chevron-right'></i> - </button> - </form> - </div> - ); - } -} - -TeamSignupAllowedDomainsPage.defaultProps = { - state: {} -}; -TeamSignupAllowedDomainsPage.propTypes = { - state: React.PropTypes.object, - updateParent: React.PropTypes.func -}; diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index acce6ab49..d3107c5c7 100644 --- a/web/react/components/team_signup_choose_auth.jsx +++ b/web/react/components/team_signup_choose_auth.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. var Constants = require('../utils/constants.jsx'); -import {strings} from '../utils/config.js'; export default class ChooseAuthPage extends React.Component { constructor(props) { @@ -24,7 +23,7 @@ export default class ChooseAuthPage extends React.Component { } > <span className='icon' /> - <span>Create new {strings.Team} with GitLab Account</span> + <span>Create new team with GitLab Account</span> </a> ); } @@ -42,7 +41,7 @@ export default class ChooseAuthPage extends React.Component { } > <span className='fa fa-envelope' /> - <span>Create new {strings.Team} with email address</span> + <span>Create new team with email address</span> </a> ); } @@ -55,7 +54,7 @@ export default class ChooseAuthPage extends React.Component { <div> {buttons} <div className='form-group margin--extra-2x'> - <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + <span><a href='/find_team'>{'Find my team'}</a></span> </div> </div> ); diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx index 1849f8222..c0d0ed366 100644 --- a/web/react/components/team_signup_display_name_page.jsx +++ b/web/react/components/team_signup_display_name_page.jsx @@ -3,7 +3,6 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); -import {strings} from '../utils/config.js'; export default class TeamSignupDisplayNamePage extends React.Component { constructor(props) { @@ -54,7 +53,7 @@ export default class TeamSignupDisplayNamePage extends React.Component { className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{utils.toTitleCase(strings.Team) + ' Name'}</h2> + <h2>{'Team Name'}</h2> <div className={nameDivClass}> <div className='row'> <div className='col-sm-9'> @@ -73,7 +72,7 @@ export default class TeamSignupDisplayNamePage extends React.Component { {nameError} </div> <div> - {'Name your ' + strings.Team + ' in any language. Your ' + strings.Team + ' name shows in menus and headings.'} + {'Name your team in any language. Your team name shows in menus and headings.'} </div> <button type='submit' diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index aa402846b..b26d9f6ce 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -4,7 +4,6 @@ var Client = require('../utils/client.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -import {strings, config} from '../utils/config.js'; export default class TeamSignupPasswordPage extends React.Component { constructor(props) { @@ -123,13 +122,13 @@ export default class TeamSignupPasswordPage extends React.Component { type='submit' className='btn btn-primary margin--extra' id='finish-button' - data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating team...'} onClick={this.submitNext} > Finish </button> </div> - <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> + <p>By proceeding to create your account and use {global.window.config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.config.SiteName}.</p> <div className='margin--extra'> <a href='#' diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index 11a9980d7..41ac98303 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -2,10 +2,7 @@ // See License.txt for license information. var EmailItem = require('./team_signup_email_item.jsx'); -var Utils = require('../utils/utils.jsx'); -var ConfigStore = require('../stores/config_store.jsx'); var Client = require('../utils/client.jsx'); -import {strings, config} from '../utils/config.js'; export default class TeamSignupSendInvitesPage extends React.Component { constructor(props) { @@ -16,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { this.submitSkip = this.submitSkip.bind(this); this.keySubmit = this.keySubmit.bind(this); this.state = { - emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) + emailEnabled: !global.window.config.ByPassEmail }; if (!this.state.emailEnabled) { @@ -26,12 +23,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { } submitBack(e) { e.preventDefault(); - - if (config.AllowSignupDomainsWizard) { - this.props.state.wizard = 'allowed_domains'; - } else { - this.props.state.wizard = 'team_url'; - } + this.props.state.wizard = 'team_url'; this.props.updateParent(this.props.state); } @@ -138,7 +130,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { bottomContent = ( <p className='color--light'> - {'if you prefer, you can invite ' + strings.Team + ' members later'} + {'if you prefer, you can invite team members later'} <br /> {' and '} <a @@ -153,7 +145,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { } else { content = ( <div className='form-group color--light'> - {'Email is currently disabled for your ' + strings.Team + ', and emails cannot be sent. Contact your system administrator to enable email and email invitations.'} + {'Email is currently disabled for your team, and emails cannot be sent. Contact your system administrator to enable email and email invitations.'} </div> ); } @@ -165,7 +157,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{'Invite ' + Utils.toTitleCase(strings.Team) + ' Members'}</h2> + <h2>{'Invite Team Members'}</h2> {content} <div className='form-group'> <button diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index ffe9d9fe8..1b722d611 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -4,7 +4,6 @@ const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); -import {strings, config} from '../utils/config.js'; export default class TeamSignupUrlPage extends React.Component { constructor(props) { @@ -51,12 +50,8 @@ export default class TeamSignupUrlPage extends React.Component { Client.findTeamByName(name, function success(data) { if (!data) { - if (config.AllowSignupDomainsWizard) { - this.props.state.wizard = 'allowed_domains'; - } else { - this.props.state.wizard = 'send_invites'; - this.props.state.team.type = 'O'; - } + this.props.state.wizard = 'send_invites'; + this.props.state.team.type = 'O'; this.props.state.team.name = name; this.props.updateParent(this.props.state); @@ -97,7 +92,7 @@ export default class TeamSignupUrlPage extends React.Component { className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{`${Utils.toTitleCase(strings.Team)} URL`}</h2> + <h2>{`Team URL`}</h2> <div className={nameDivClass}> <div className='row'> <div className='col-sm-11'> @@ -124,7 +119,7 @@ export default class TeamSignupUrlPage extends React.Component { </div> {nameError} </div> - <p>{`Choose the web address of your new ${strings.Team}:`}</p> + <p>{`Choose the web address of your new team:`}</p> <ul className='color--light'> <li>Short and memorable is best</li> <li>Use lowercase letters, numbers and dashes</li> diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index 984c7afab..0053b011d 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -3,7 +3,6 @@ var Utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); -import {strings} from '../utils/config.js'; export default class TeamSignupUsernamePage extends React.Component { constructor(props) { @@ -55,7 +54,7 @@ export default class TeamSignupUsernamePage extends React.Component { src='/static/images/logo.png' /> <h2 className='margin--less'>Your username</h2> - <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5> + <h5 className='color--light'>{'Select a memorable username that makes it easy for teammates to identify you:'}</h5> <div className='inner__content margin--extra'> <div className={nameDivClass}> <div className='row'> diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index 43b7aea0e..626c6a17b 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -4,7 +4,6 @@ var Utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); -import {config} from '../utils/config.js'; export default class TeamSignupWelcomePage extends React.Component { constructor(props) { @@ -112,7 +111,7 @@ export default class TeamSignupWelcomePage extends React.Component { src='/static/images/logo.png' /> <h3 className='sub-heading'>Welcome to:</h3> - <h1 className='margin--top-none'>{config.SiteName}</h1> + <h1 className='margin--top-none'>{global.window.config.SiteName}</h1> </p> <p className='margin--less'>Let's set up your new team</p> <p> diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index d75736bd3..4fb1c0d01 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -3,7 +3,6 @@ const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); -import {strings} from '../utils/config.js'; export default class EmailSignUpPage extends React.Component { constructor() { @@ -70,7 +69,7 @@ export default class EmailSignUpPage extends React.Component { </button> </div> <div className='form-group margin--extra-2x'> - <span><a href='/find_team'>{`Find my ${strings.Team}`}</a></span> + <span><a href='/find_team'>{`Find my team`}</a></span> </div> </form> ); diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx index 521c21733..2849b4cbb 100644 --- a/web/react/components/team_signup_with_sso.jsx +++ b/web/react/components/team_signup_with_sso.jsx @@ -4,7 +4,6 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); -import {strings} from '../utils/config.js'; export default class SSOSignUpPage extends React.Component { constructor(props) { @@ -84,7 +83,7 @@ export default class SSOSignUpPage extends React.Component { disabled={disabled} > <span className='icon'/> - <span>Create {strings.Team} with GitLab Account</span> + <span>Create team with GitLab Account</span> </a> ); } @@ -111,7 +110,7 @@ export default class SSOSignUpPage extends React.Component { {serverError} </div> <div className='form-group margin--extra-2x'> - <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + <span><a href='/find_team'>{'Find my team'}</a></span> </div> </form> ); diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 739084053..7cfac69e7 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -3,7 +3,6 @@ var Utils = require('../utils/utils.jsx'); var UserStore = require('../stores/user_store.jsx'); -import {config} from '../utils/config.js'; var id = 0; @@ -58,7 +57,7 @@ export default class UserProfile extends React.Component { } var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />'; - if (!config.ShowEmail) { + if (!global.window.config.ShowEmailAddress) { dataContent += '<div class="text-nowrap">Email not shared</div>'; } else { dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>'; diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 2a607b3e0..48b499068 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -7,6 +7,7 @@ var NotificationsTab = require('./user_settings_notifications.jsx'); var SecurityTab = require('./user_settings_security.jsx'); var GeneralTab = require('./user_settings_general.jsx'); var AppearanceTab = require('./user_settings_appearance.jsx'); +var DeveloperTab = require('./user_settings_developer.jsx'); export default class UserSettings extends React.Component { constructor(props) { @@ -76,6 +77,15 @@ export default class UserSettings extends React.Component { /> </div> ); + } else if (this.props.activeTab === 'developer') { + return ( + <div> + <DeveloperTab + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + /> + </div> + ); } return <div/>; diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx index 3afdd7349..3df013d03 100644 --- a/web/react/components/user_settings_appearance.jsx +++ b/web/react/components/user_settings_appearance.jsx @@ -6,7 +6,8 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); var Client = require('../utils/client.jsx'); var Utils = require('../utils/utils.jsx'); -import {config} from '../utils/config.js'; + +var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']; export default class UserSettingsAppearance extends React.Component { constructor(props) { @@ -21,8 +22,8 @@ export default class UserSettingsAppearance extends React.Component { getStateFromStores() { var user = UserStore.getCurrentUser(); var theme = '#2389d7'; - if (config.ThemeColors != null) { - theme = config.ThemeColors[0]; + if (ThemeColors != null) { + theme = ThemeColors[0]; } if (user.props && user.props.theme) { theme = user.props.theme; @@ -83,18 +84,18 @@ export default class UserSettingsAppearance extends React.Component { var themeSection; var self = this; - if (config.ThemeColors != null) { + if (ThemeColors != null) { if (this.props.activeSection === 'theme') { var themeButtons = []; - for (var i = 0; i < config.ThemeColors.length; i++) { + for (var i = 0; i < ThemeColors.length; i++) { themeButtons.push( <button - key={config.ThemeColors[i] + 'key' + i} - ref={config.ThemeColors[i]} + key={ThemeColors[i] + 'key' + i} + ref={ThemeColors[i]} type='button' className='btn btn-lg color-btn' - style={{backgroundColor: config.ThemeColors[i]}} + style={{backgroundColor: ThemeColors[i]}} onClick={this.updateTheme} /> ); diff --git a/web/react/components/user_settings_developer.jsx b/web/react/components/user_settings_developer.jsx new file mode 100644 index 000000000..1b04149dc --- /dev/null +++ b/web/react/components/user_settings_developer.jsx @@ -0,0 +1,93 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); + +export default class DeveloperTab extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + register() { + $('#user_settings1').modal('hide'); + $('#register_app').modal('show'); + } + render() { + var appSection; + var self = this; + if (this.props.activeSection === 'app') { + var inputs = []; + + inputs.push( + <div className='form-group'> + <div className='col-sm-7'> + <a + className='btn btn-sm btn-primary' + onClick={this.register} + > + {'Register New Application'} + </a> + </div> + </div> + ); + + appSection = ( + <SettingItemMax + title='Applications (Preview)' + inputs={inputs} + updateSection={function updateSection(e) { + self.props.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + appSection = ( + <SettingItemMin + title='Applications (Preview)' + describe='Open to register a new third-party application' + updateSection={function updateSection() { + self.props.updateSection('app'); + }} + /> + ); + } + + return ( + <div> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'x'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i>{'Developer Settings'} + </h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>{'Developer Settings'}</h3> + <div className='divider-dark first'/> + {appSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +} + +DeveloperTab.defaultProps = { + activeSection: '' +}; +DeveloperTab.propTypes = { + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func +}; diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx index dd0abc8a5..66cde6ca2 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/user_settings_general.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -var ConfigStore = require('../stores/config_store.jsx'); var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); var SettingPicture = require('./setting_picture.jsx'); @@ -209,7 +208,7 @@ export default class UserSettingsGeneralTab extends React.Component { } setupInitialState(props) { var user = props.user; - var emailEnabled = !ConfigStore.getSettingAsBoolean('ByPassEmail', false); + var emailEnabled = !global.window.config.ByPassEmail; return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled}; } diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 7ec75e000..1daf6ebb9 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -17,8 +17,8 @@ export default class UserSettingsModal extends React.Component { $('body').on('click', '.modal-back', function changeDisplay() { $(this).closest('.modal-dialog').removeClass('display--content'); }); - $('body').on('click', '.modal-header .close', function closeModal() { - setTimeout(function finishClose() { + $('body').on('click', '.modal-header .close', () => { + setTimeout(() => { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); @@ -35,6 +35,9 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'}); tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'}); + if (global.window.config.EnableOAuthServiceProvider) { + tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); + } return ( <div @@ -54,13 +57,13 @@ export default class UserSettingsModal extends React.Component { data-dismiss='modal' aria-label='Close' > - <span aria-hidden='true'>×</span> + <span aria-hidden='true'>{'x'}</span> </button> <h4 className='modal-title' ref='title' > - Account Settings + {'Account Settings'} </h4> </div> <div className='modal-body'> diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings_notifications.jsx index 33db1a332..dadbb669b 100644 --- a/web/react/components/user_settings_notifications.jsx +++ b/web/react/components/user_settings_notifications.jsx @@ -8,7 +8,6 @@ var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); var assign = require('object-assign'); -import {config} from '../utils/config.js'; function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); @@ -415,7 +414,7 @@ export default class NotificationsTab extends React.Component { </label> <br/> </div> - <div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + config.SiteName + ' for 5 minutes.'}</div> + <div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div> </div> ); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 8d3495e3b..f7c980396 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -3,7 +3,6 @@ var Client = require('../utils/client.jsx'); var Utils = require('../utils/utils.jsx'); -import {config} from '../utils/config.js'; export default class ViewImageModal extends React.Component { constructor(props) { @@ -301,7 +300,7 @@ export default class ViewImageModal extends React.Component { } var publicLink = ''; - if (config.AllowPublicLink) { + if (global.window.config.AllowPublicLink) { publicLink = ( <div> <a diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx new file mode 100644 index 000000000..db42c8266 --- /dev/null +++ b/web/react/pages/authorize.jsx @@ -0,0 +1,21 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Authorize = require('../components/authorize.jsx'); + +function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) { + React.render( + <Authorize + teamName={teamName} + appName={appName} + responseType={responseType} + clientId={clientId} + redirectUri={redirectUri} + scope={scope} + state={state} + />, + document.getElementById('authorize') + ); +} + +global.window.setup_authorize_page = setupAuthorizePage; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index e70b51865..43493de45 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -33,24 +33,21 @@ var AccessHistoryModal = require('../components/access_history_modal.jsx'); var ActivityLogModal = require('../components/activity_log_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx'); var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); - -var AsyncClient = require('../utils/async_client.jsx'); +var RegisterAppModal = require('../components/register_app_modal.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -function setupChannelPage(teamName, teamType, teamId, channelName, channelId) { - AsyncClient.getConfig(); - +function setupChannelPage(props) { AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, - name: channelName, - id: channelId + name: props.ChannelName, + id: props.ChannelId }); AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_TEAM, - id: teamId + id: props.TeamId }); // ChannelLoader must be rendered first @@ -65,14 +62,14 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) { ); React.render( - <Navbar teamDisplayName={teamName} />, + <Navbar teamDisplayName={props.TeamDisplayName} />, document.getElementById('navbar') ); React.render( <Sidebar - teamDisplayName={teamName} - teamType={teamType} + teamDisplayName={props.TeamDisplayName} + teamType={props.TeamType} />, document.getElementById('sidebar-left') ); @@ -88,17 +85,17 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) { ); React.render( - <TeamSettingsModal teamDisplayName={teamName} />, + <TeamSettingsModal teamDisplayName={props.TeamDisplayName} />, document.getElementById('team_settings_modal') ); React.render( - <TeamMembersModal teamDisplayName={teamName} />, + <TeamMembersModal teamDisplayName={props.TeamDisplayName} />, document.getElementById('team_members_modal') ); React.render( - <MemberInviteModal teamType={teamType} />, + <MemberInviteModal teamType={props.TeamType} />, document.getElementById('invite_member_modal') ); @@ -184,8 +181,8 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) { React.render( <SidebarRightMenu - teamDisplayName={teamName} - teamType={teamType} + teamDisplayName={props.TeamDisplayName} + teamType={props.TeamType} />, document.getElementById('sidebar-menu') ); @@ -226,6 +223,11 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) { />, document.getElementById('file_upload_overlay') ); + + React.render( + <RegisterAppModal />, + document.getElementById('register_app_modal') + ); } global.window.setup_channel_page = setupChannelPage; diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx index 18553542c..2299c306e 100644 --- a/web/react/pages/home.jsx +++ b/web/react/pages/home.jsx @@ -4,12 +4,12 @@ var ChannelStore = require('../stores/channel_store.jsx'); var Constants = require('../utils/constants.jsx'); -function setupHomePage(teamURL) { +function setupHomePage(props) { var last = ChannelStore.getLastVisitedName(); if (last == null || last.length === 0) { - window.location = teamURL + '/channels/' + Constants.DEFAULT_CHANNEL; + window.location = props.TeamURL + '/channels/' + Constants.DEFAULT_CHANNEL; } else { - window.location = teamURL + '/channels/' + last; + window.location = props.TeamURL + '/channels/' + last; } } diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index 424ae0e84..830f622fa 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -3,12 +3,12 @@ var Login = require('../components/login.jsx'); -function setupLoginPage(teamDisplayName, teamName, authServices) { +function setupLoginPage(props) { React.render( <Login - teamDisplayName={teamDisplayName} - teamName={teamName} - authServices={authServices} + teamDisplayName={props.TeamDisplayName} + teamName={props.TeamName} + authServices={props.AuthServices} />, document.getElementById('login') ); diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx index 2ca468bea..b7bfdcd5e 100644 --- a/web/react/pages/password_reset.jsx +++ b/web/react/pages/password_reset.jsx @@ -3,14 +3,14 @@ var PasswordReset = require('../components/password_reset.jsx'); -function setupPasswordResetPage(isReset, teamDisplayName, teamName, hash, data) { +function setupPasswordResetPage(props) { React.render( <PasswordReset - isReset={isReset} - teamDisplayName={teamDisplayName} - teamName={teamName} - hash={hash} - data={data} + isReset={props.IsReset} + teamDisplayName={props.TeamDisplayName} + teamName={props.TeamName} + hash={props.Hash} + data={props.Data} />, document.getElementById('reset') ); diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index e9e803aa4..427daf577 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -3,12 +3,8 @@ var SignupTeam = require('../components/signup_team.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); - -function setupSignupTeamPage(authServices) { - AsyncClient.getConfig(); - - var services = JSON.parse(authServices); +function setupSignupTeamPage(props) { + var services = JSON.parse(props.AuthServices); React.render( <SignupTeam services={services} />, diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx index 72f9992a8..ec77e6602 100644 --- a/web/react/pages/signup_team_complete.jsx +++ b/web/react/pages/signup_team_complete.jsx @@ -3,12 +3,12 @@ var SignupTeamComplete = require('../components/signup_team_complete.jsx'); -function setupSignupTeamCompletePage(email, data, hash) { +function setupSignupTeamCompletePage(props) { React.render( <SignupTeamComplete - email={email} - hash={hash} - data={data} + email={props.Email} + hash={props.Hash} + data={props.Data} />, document.getElementById('signup-team-complete') ); diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index eaf93a61c..112aaa3f2 100644 --- a/web/react/pages/signup_user_complete.jsx +++ b/web/react/pages/signup_user_complete.jsx @@ -3,16 +3,16 @@ var SignupUserComplete = require('../components/signup_user_complete.jsx'); -function setupSignupUserCompletePage(email, name, uiName, id, data, hash, authServices) { +function setupSignupUserCompletePage(props) { React.render( <SignupUserComplete - teamId={id} - teamName={name} - teamDisplayName={uiName} - email={email} - hash={hash} - data={data} - authServices={authServices} + teamId={props.TeamId} + teamName={props.TeamName} + teamDisplayName={props.TeamDisplayName} + email={props.Email} + hash={props.Hash} + data={props.Data} + authServices={props.AuthServices} />, document.getElementById('signup-user-complete') ); diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx index 7077b40b8..e48471bbd 100644 --- a/web/react/pages/verify.jsx +++ b/web/react/pages/verify.jsx @@ -3,12 +3,12 @@ var EmailVerify = require('../components/email_verify.jsx'); -global.window.setupVerifyPage = function setupVerifyPage(isVerified, teamURL, userEmail) { +global.window.setupVerifyPage = function setupVerifyPage(props) { React.render( <EmailVerify - isVerified={isVerified} - teamURL={teamURL} - userEmail={userEmail} + isVerified={props.IsVerified} + teamURL={props.TeamURL} + userEmail={props.UserEmail} />, document.getElementById('verify') ); diff --git a/web/react/stores/config_store.jsx b/web/react/stores/config_store.jsx deleted file mode 100644 index b397937be..000000000 --- a/web/react/stores/config_store.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; - -var BrowserStore = require('../stores/browser_store.jsx'); - -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; - -var CHANGE_EVENT = 'change'; - -class ConfigStoreClass extends EventEmitter { - constructor() { - super(); - - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.getSetting = this.getSetting.bind(this); - this.getSettingAsBoolean = this.getSettingAsBoolean.bind(this); - this.updateStoredSettings = this.updateStoredSettings.bind(this); - } - emitChange() { - this.emit(CHANGE_EVENT); - } - addChangeListener(callback) { - this.on(CHANGE_EVENT, callback); - } - removeChangeListener(callback) { - this.removeListener(CHANGE_EVENT, callback); - } - getSetting(key, defaultValue) { - return BrowserStore.getItem('config_' + key, defaultValue); - } - getSettingAsBoolean(key, defaultValue) { - var value = this.getSetting(key, defaultValue); - - if (typeof value !== 'string') { - return Boolean(value); - } - - return value === 'true'; - } - updateStoredSettings(settings) { - for (let key in settings) { - if (settings.hasOwnProperty(key)) { - BrowserStore.setItem('config_' + key, settings[key]); - } - } - } -} - -var ConfigStore = new ConfigStoreClass(); - -ConfigStore.dispatchToken = AppDispatcher.register(function registry(payload) { - var action = payload.action; - - switch (action.type) { - case ActionTypes.RECIEVED_CONFIG: - ConfigStore.updateStoredSettings(action.settings); - ConfigStore.emitChange(); - break; - default: - } -}); - -export default ConfigStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 6b8e73c5a..3e23e5c33 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -582,28 +582,4 @@ export function getMyTeam() { dispatchError(err, 'getMyTeam'); } ); -} - -export function getConfig() { - if (isCallInProgress('getConfig')) { - return; - } - - callTracker.getConfig = utils.getTimestamp(); - client.getConfig( - function getConfigSuccess(data, textStatus, xhr) { - callTracker.getConfig = 0; - - if (data && xhr.status !== 304) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_CONFIG, - settings: data - }); - } - }, - function getConfigFailure(err) { - callTracker.getConfig = 0; - dispatchError(err, 'getConfig'); - } - ); -} +}
\ No newline at end of file diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 75ffdb274..ba3042d78 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -14,8 +14,6 @@ export function trackPage() { } function handleError(methodName, xhr, status, err) { - var LTracker = global.window.LTracker || []; - var e = null; try { e = JSON.parse(xhr.responseText); @@ -39,7 +37,6 @@ function handleError(methodName, xhr, status, err) { console.error(msg); //eslint-disable-line no-console console.error(e); //eslint-disable-line no-console - LTracker.push(msg); track('api', 'api_weberror', methodName, 'message', msg); @@ -991,16 +988,35 @@ export function updateValetFeature(data, success, error) { track('api', 'api_teams_update_valet_feature'); } -export function getConfig(success, error) { +export function registerOAuthApp(app, success, error) { + $.ajax({ + url: '/api/v1/oauth/register', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(app), + success: success, + error: (xhr, status, err) => { + const e = handleError('registerApp', xhr, status, err); + error(e); + } + }); + + module.exports.track('api', 'api_apps_register'); +} + +export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) { $.ajax({ - url: '/api/v1/config/get_all', + url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state, dataType: 'json', + contentType: 'application/json', type: 'GET', - ifModified: true, success: success, - error: function onError(xhr, status, err) { - var e = handleError('getConfig', xhr, status, err); + error: (xhr, status, err) => { + const e = handleError('allowOAuth2', xhr, status, err); error(e); } }); + + module.exports.track('api', 'api_users_allow_oauth2'); } diff --git a/web/react/utils/config.js b/web/react/utils/config.js deleted file mode 100644 index c7d1aa2bc..000000000 --- a/web/react/utils/config.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -export var config = { - - // Loggly configs - LogglyWriteKey: '', - LogglyConsoleErrors: true, - - // Segment configs - SegmentWriteKey: '', - - // Feature switches - AllowPublicLink: true, - AllowInviteNames: true, - RequireInviteNames: false, - AllowSignupDomainsWizard: false, - - // Google Developer Key (for Youtube API links) - // Leave blank to disable - GoogleDeveloperKey: '', - - // Privacy switches - ShowEmail: true, - - // Links - TermsLink: '/static/help/configure_links.html', - PrivacyLink: '/static/help/configure_links.html', - AboutLink: '/static/help/configure_links.html', - HelpLink: '/static/help/configure_links.html', - ReportProblemLink: '/static/help/configure_links.html', - HomeLink: '', - - // Toggle whether or not users are shown a message about agreeing to the Terms of Service during the signup process - ShowTermsDuringSignup: false, - - ThemeColors: ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000'] -}; - -// Flavor strings -export var strings = { - Team: 'team', - TeamPlural: 'teams', - Company: 'company', - CompanyPlural: 'companies' -}; - -global.window.config = config; diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 2c67d7a46..2025e16da 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -56,7 +56,7 @@ function autolinkUrls(text, tokens) { const linkText = match.getMatchedText(); let url = linkText; - if (!url.startsWith('http')) { + if (!url.lastIndexOf('http', 0) === 0) { url = `http://${linkText}`; } @@ -160,7 +160,7 @@ function autolinkHashtags(text, tokens) { var newTokens = new Map(); for (const [alias, token] of tokens) { - if (token.originalText.startsWith('#')) { + if (token.originalText.lastIndexOf('#', 0) === 0) { const index = tokens.size + newTokens.size; const newAlias = `__MM_HASHTAG${index}__`; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index c2307f5e9..032cf4ff4 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -9,7 +9,6 @@ var ActionTypes = Constants.ActionTypes; var AsyncClient = require('./async_client.jsx'); var client = require('./client.jsx'); var Autolinker = require('autolinker'); -import {config} from '../utils/config.js'; export function isEmail(email) { var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; @@ -295,12 +294,12 @@ function getYoutubeEmbed(link) { $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight); } - if (config.GoogleDeveloperKey) { + if (global.window.config.GoogleDeveloperKey) { $.ajax({ async: true, url: 'https://www.googleapis.com/youtube/v3/videos', type: 'GET', - data: {part: 'snippet', id: youtubeId, key: config.GoogleDeveloperKey}, + data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey}, success: success }); } diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index 2fb56e537..924f0718a 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -315,3 +315,18 @@ } } + +.authorize-box { + margin: 100px auto; + width:500px; + height:280px; + border: 1px solid black; +} + +.authorize-inner { + padding: 20px; +} + +.authorize-btn { + margin-right: 6px; +} diff --git a/web/static/help/configure_links.html b/web/static/help/about.html index 1c564e0d6..4659aa9cc 100644 --- a/web/static/help/configure_links.html +++ b/web/static/help/about.html @@ -7,10 +7,6 @@ Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p> -<h1>How to update this link</h1> -<p>In the source code, search for "config.js" and update the links pointing to this page to whatever policies and product description you prefer. -</p> - <h1>Join the community</h1> <p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> diff --git a/web/static/help/help.html b/web/static/help/help.html new file mode 100644 index 000000000..52f5be994 --- /dev/null +++ b/web/static/help/help.html @@ -0,0 +1,24 @@ +<htmL> +<body> +<h1>Help with Mattermost</h1> +<p>Mattermost is a team communication service. It brings team real-time messaging and file sharing into one place, with easy archiving and search, accessible across PCs and phones. +</p> +<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too. + +Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p> + +<h1>Join the community</h1> +<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> + +<p>Here's some links to get started:<br> +<ul> + <li><a href="https://github.com/mattermost/platform">Follow Mattermost on Github</a></li> + <li><a href="http://forum.mattermost.org/">Ask us anything at http://forum.mattermost.org/</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Review the Mattermost feature list </a></li> + <li><a href="http://www.mattermost.org/download/">Download our source code and install instructions</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Share feature requests and upvotes</a></li> + <li><a href="http://www.mattermost.org/filing-issues/">File any bugs you find with our Issue tracking system</a></li> +</ul> +</p> +</body> +</html> diff --git a/web/static/help/privacy.html b/web/static/help/privacy.html new file mode 100644 index 000000000..fe6c1598f --- /dev/null +++ b/web/static/help/privacy.html @@ -0,0 +1,24 @@ +<htmL> +<body> +<h1>Mattermost Privacy</h1> +<p>Mattermost is a team communication service. It brings team real-time messaging and file sharing into one place, with easy archiving and search, accessible across PCs and phones. +</p> +<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too. + +Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p> + +<h1>Join the community</h1> +<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> + +<p>Here's some links to get started:<br> +<ul> + <li><a href="https://github.com/mattermost/platform">Follow Mattermost on Github</a></li> + <li><a href="http://forum.mattermost.org/">Ask us anything at http://forum.mattermost.org/</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Review the Mattermost feature list </a></li> + <li><a href="http://www.mattermost.org/download/">Download our source code and install instructions</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Share feature requests and upvotes</a></li> + <li><a href="http://www.mattermost.org/filing-issues/">File any bugs you find with our Issue tracking system</a></li> +</ul> +</p> +</body> +</html> diff --git a/web/static/help/report_problem.html b/web/static/help/report_problem.html new file mode 100644 index 000000000..6b73619b4 --- /dev/null +++ b/web/static/help/report_problem.html @@ -0,0 +1,24 @@ +<htmL> +<body> +<h1>Report a Problem About Mattermost</h1> +<p>Mattermost is a team communication service. It brings team real-time messaging and file sharing into one place, with easy archiving and search, accessible across PCs and phones. +</p> +<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too. + +Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p> + +<h1>Join the community</h1> +<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> + +<p>Here's some links to get started:<br> +<ul> + <li><a href="https://github.com/mattermost/platform">Follow Mattermost on Github</a></li> + <li><a href="http://forum.mattermost.org/">Ask us anything at http://forum.mattermost.org/</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Review the Mattermost feature list </a></li> + <li><a href="http://www.mattermost.org/download/">Download our source code and install instructions</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Share feature requests and upvotes</a></li> + <li><a href="http://www.mattermost.org/filing-issues/">File any bugs you find with our Issue tracking system</a></li> +</ul> +</p> +</body> +</html> diff --git a/web/static/help/terms.html b/web/static/help/terms.html new file mode 100644 index 000000000..6e1f13897 --- /dev/null +++ b/web/static/help/terms.html @@ -0,0 +1,24 @@ +<htmL> +<body> +<h1>Mattermost Terms</h1> +<p>Mattermost is a team communication service. It brings team real-time messaging and file sharing into one place, with easy archiving and search, accessible across PCs and phones. +</p> +<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too. + +Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p> + +<h1>Join the community</h1> +<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> + +<p>Here's some links to get started:<br> +<ul> + <li><a href="https://github.com/mattermost/platform">Follow Mattermost on Github</a></li> + <li><a href="http://forum.mattermost.org/">Ask us anything at http://forum.mattermost.org/</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Review the Mattermost feature list </a></li> + <li><a href="http://www.mattermost.org/download/">Download our source code and install instructions</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Share feature requests and upvotes</a></li> + <li><a href="http://www.mattermost.org/filing-issues/">File any bugs you find with our Issue tracking system</a></li> +</ul> +</p> +</body> +</html> diff --git a/web/templates/authorize.html b/web/templates/authorize.html new file mode 100644 index 000000000..3392c1b1e --- /dev/null +++ b/web/templates/authorize.html @@ -0,0 +1,26 @@ +{{define "authorize"}} +<html> +{{template "head" . }} +<body class="white"> + <div class="container-fluid"> + <div class="inner__wrap"> + <div class="row content"> + <div class="signup-header"> + {{.Props.TeamName}} + </div> + <div class="col-sm-12"> + <div id="authorize"></div> + </div> + <div class="footer-push"></div> + </div> + <div class="row footer"> + {{template "footer" . }} + </div> + </div> + </div> + <script> + window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' ); + </script> +</body> +</html> +{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html index a732a25ce..92aaaf02f 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -49,8 +49,9 @@ <div id="access_history_modal"></div> <div id="activity_log_modal"></div> <div id="removed_from_channel_modal"></div> + <div id="register_app_modal"></div> <script> - window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}'); + window.setup_channel_page({{ .Props }}); $('body').tooltip( {selector: '[data-toggle=tooltip]'} ); $('.modal-body').css('max-height', $(window).height() * 0.7); $('.modal-body').perfectScrollbar(); diff --git a/web/templates/footer.html b/web/templates/footer.html index 204a89f03..4b15295b4 100644 --- a/web/templates/footer.html +++ b/web/templates/footer.html @@ -1,7 +1,7 @@ {{define "footer"}} <div class="footer-pane col-xs-12"> <div class="col-xs-12"> - <span class="pull-right footer-site-name">{{ .SiteName }}</span> + <span class="pull-right footer-site-name">{{ .ClientProps.SiteName }}</span> </div> <div class="col-xs-12"> <span class="pull-right footer-link copyright">© 2015 SpinPunch</span> @@ -12,9 +12,9 @@ </div> </div> <script> - document.getElementById("help_link").setAttribute("href", config.HelpLink); - document.getElementById("terms_link").setAttribute("href", config.TermsLink); - document.getElementById("privacy_link").setAttribute("href", config.PrivacyLink); - document.getElementById("about_link").setAttribute("href", config.AboutLink); + document.getElementById("help_link").setAttribute("href", '/static/help/help.html'); + document.getElementById("terms_link").setAttribute("href", '/static/help/terms.html'); + document.getElementById("privacy_link").setAttribute("href", '/static/help/privacy.html'); + document.getElementById("about_link").setAttribute("href", '/static/help/about.html'); </script> {{end}} diff --git a/web/templates/head.html b/web/templates/head.html index e4b9bfe19..dcd643b58 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -3,14 +3,14 @@ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="robots" content="noindex, nofollow"> - <title>{{ .Title }}</title> + <title>{{ .Props.Title }}</title> <!-- iOS add to homescreen --> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-title" content="{{ .Title }}"> - <meta name="application-name" content="{{ .Title }}"> + <meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}"> + <meta name="application-name" content="{{ .Props.Title }}"> <meta name="format-detection" content="telephone=no"> <!-- iOS add to homescreen --> @@ -18,6 +18,11 @@ <link rel="manifest" href="/static/config/manifest.json"> <!-- Android add to homescreen --> + <script> + window.config = {{ .ClientProps }}; + </script> + + <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet"> @@ -35,9 +40,7 @@ <script src="/static/js/jquery-dragster/jquery.dragster.js"></script> - <script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['annotationchart']}]}"></script> - <script type="text/javascript" src="https://cloudfront.loggly.com/js/loggly.tracker.js" async></script> <style id="antiClickjack">body{display:none !important;}</style> <script src="/static/js/bundle.js"></script> <script type="text/javascript"> @@ -46,28 +49,8 @@ blocker.parentNode.removeChild(blocker); } </script> - <script> - if (window.config == null) { - window.config = {}; - } - window.config.SiteName = '{{ .SiteName }}'; - window.config.ProfileWidth = '{{ .Props.ProfileWidth }}' - window.config.ProfileHeight = '{{ .Props.ProfileHeight }}' - </script> - - - <script> - if (window.config.LogglyWriteKey != null && window.config.LogglyWriteKey !== "") { - var LTracker = LTracker || []; - window.LTracker = LTracker; - LTracker.push({'logglyKey': window.config.LogglyWriteKey, 'sendConsoleErrors' : window.config.LogglyConsoleErrors }); - } else { - window.LTracker = []; - console.warn("config.js missing LogglyWriteKey, Loggly analytics is not reporting"); - } - </script> <script type="text/javascript"> - if (window.config.SegmentWriteKey != null && window.config.SegmentWriteKey !== "") { + if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") { !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1"; analytics.load(window.config.SegmentWriteKey); var user = window.UserStore.getCurrentUser(true); @@ -88,7 +71,6 @@ analytics = {}; analytics.page = function(){}; analytics.track = function(){}; - console.warn("config.js missing SegmentWriteKey, SegmentIO analytics is not tracking"); } </script> <!-- Snowplow starts plowing --> @@ -100,7 +82,7 @@ n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","//d1fc8wv8zag5ca.cloudfront.net/2.4.2/sp.js","snowplow")); window.snowplow('newTracker', 'cf', '{{ .Props.AnalyticsUrl }}', { - appId: '{{ .SiteName }}' + appId: window.config.SiteName }); var user = window.UserStore.getCurrentUser(true); @@ -111,7 +93,6 @@ window.snowplow('trackPageView'); } else { window.snowplow = function(){}; - console.warn("config.json missing AnalyticsUrl, Snowplow analytics is not tracking"); } </script> <!-- Snowplow stops plowing --> diff --git a/web/templates/home.html b/web/templates/home.html index 9ec8b7000..0d8b89061 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -17,7 +17,7 @@ </div> </div> <script> - window.setup_home_page({{.Props.TeamURL}}); + window.setup_home_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/login.html b/web/templates/login.html index 4b2813358..a5809a1f4 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -20,7 +20,7 @@ </div> </div> <script> -window.setup_login_page('{{.Props.TeamDisplayName}}', '{{.Props.TeamName}}', '{{.Props.AuthServices}}'); +window.setup_login_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html index 6244f6418..7f6335c92 100644 --- a/web/templates/password_reset.html +++ b/web/templates/password_reset.html @@ -9,7 +9,7 @@ </div> </div> <script> - window.setup_password_reset_page('{{ .Props.IsReset }}', '{{ .Props.TeamDisplayName }}', '{{ .Props.TeamName }}', '{{ .Props.Hash }}', '{{ .Props.Data }}'); + window.setup_password_reset_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html index 8d9d6e0b8..a6000696e 100644 --- a/web/templates/signup_team.html +++ b/web/templates/signup_team.html @@ -9,7 +9,7 @@ <div class="col-sm-12"> <div class="signup-team__container"> <img class="signup-team-logo" src="/static/images/logo.png" /> - <h1>{{ .SiteName }}</h1> + <h1>{{ .ClientProps.SiteName }}</h1> <h4 class="color--light">All team communication in one place, searchable and accessible anywhere</h4> <div id="signup-team"></div> </div> @@ -22,7 +22,7 @@ </div> </div> <script> -window.setup_signup_team_page('{{.Props.AuthServices}}'); +window.setup_signup_team_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html index 041889435..4b179b1e1 100644 --- a/web/templates/signup_team_complete.html +++ b/web/templates/signup_team_complete.html @@ -19,7 +19,7 @@ </div> </div> <script> -window.setup_signup_team_complete_page('{{.Props.Email}}', '{{.Props.Data}}', '{{.Props.Hash}}'); +window.setup_signup_team_complete_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html index e9f6bafcf..2400b7b77 100644 --- a/web/templates/signup_user_complete.html +++ b/web/templates/signup_user_complete.html @@ -19,7 +19,7 @@ </div> </div> <script> - window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}', '{{.Props.AuthServices}}'); + window.setup_signup_user_complete_page({{ .Props }}); </script> </body> </html> diff --git a/web/templates/verify.html b/web/templates/verify.html index de839db68..cb4832512 100644 --- a/web/templates/verify.html +++ b/web/templates/verify.html @@ -9,7 +9,7 @@ </div> </div> <script> - window.setupVerifyPage('{{.Props.IsVerified}}', '{{.Props.TeamURL}}', '{{.Props.UserEmail}}'); + window.setupVerifyPage({{ .Props }}); </script> </body> </html> diff --git a/web/templates/welcome.html b/web/templates/welcome.html index bab7a135d..e7eeb5648 100644 --- a/web/templates/welcome.html +++ b/web/templates/welcome.html @@ -11,7 +11,7 @@ <div class="row main"> <div class="app__content"> <div class="welcome-info"> - <h1>Welcome to {{ .SiteName }}!</h1> + <h1>Welcome to {{ .ClientProps.SiteName }}!</h1> <p> You do not appear to be part of any teams. Please contact your administrator to have him send you an invitation to a private team. diff --git a/web/web.go b/web/web.go index 1ed055a62..305e4f199 100644 --- a/web/web.go +++ b/web/web.go @@ -4,19 +4,18 @@ package web import ( - "fmt" - "html/template" - "net/http" - "strconv" - "strings" - l4g "code.google.com/p/log4go" + "fmt" "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "gopkg.in/fsnotify.v1" + "html/template" + "net/http" + "strconv" + "strings" ) var Templates *template.Template @@ -30,10 +29,8 @@ func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { } props := make(map[string]string) - props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl - props["ProfileHeight"] = fmt.Sprintf("%v", utils.Cfg.ImageSettings.ProfileHeight) - props["ProfileWidth"] = fmt.Sprintf("%v", utils.Cfg.ImageSettings.ProfileWidth) - return &HtmlTemplatePage{TemplateName: templateName, Title: title, SiteName: utils.Cfg.ServiceSettings.SiteName, Props: props} + props["Title"] = title + return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientProps: utils.ClientProperties} } func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { @@ -52,6 +49,8 @@ func InitWeb() { mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") + mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") + mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") @@ -65,7 +64,7 @@ func InitWeb() { mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team spefic should go below this line + // *ANYTHING* team specific should go below this line // ---------------------------------------------------------------------------------------------- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") @@ -344,7 +343,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("channel", "") - page.Title = name + " - " + team.DisplayName + " " + page.SiteName + page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id @@ -447,7 +446,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("password_reset", "") - page.Title = "Reset Password - " + page.SiteName + page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = teamDisplayName page.Props["Hash"] = hash page.Props["Data"] = data @@ -650,3 +649,192 @@ func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("admin_console", "Admin Console") page.Render(c, w) } + +func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("authorizeOAuth", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if !CheckBrowserCompatability(c, r) { + return + } + + responseType := r.URL.Query().Get("response_type") + clientId := r.URL.Query().Get("client_id") + redirect := r.URL.Query().Get("redirect_uri") + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { + c.Err = model.NewAppError("authorizeOAuth", "Missing one or more of response_type, client_id, or redirect_uri", "") + return + } + + var app *model.OAuthApp + if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := NewHtmlTemplatePage("authorize", "Authorize Application") + page.Props["TeamName"] = team.Name + page.Props["AppName"] = app.Name + page.Props["ResponseType"] = responseType + page.Props["ClientId"] = clientId + page.Props["RedirectUri"] = redirect + page.Props["Scope"] = scope + page.Props["State"] = state + page.Render(c, w) +} + +func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("getAccessToken", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + r.ParseForm() + + grantType := r.FormValue("grant_type") + if grantType != model.ACCESS_TOKEN_GRANT_TYPE { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad grant_type", "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad client_id", "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing client_secret", "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing code", "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := api.Srv.Store.OAuth().GetApp(clientId) + tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := api.GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + uchan := api.Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewAppError("getAccessToken", "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri", "") + return + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + c.LogAudit("fail - auth code is invalid") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while accessing database", "") + return + } else if result.Data != nil { + c.LogAudit("fail - auth code has been used previously") + accessData := result.Data.(*model.AccessData) + + // Revoke access token, related auth code, and session from DB as well as from cache + if err := api.RevokeAccessToken(accessData.Token); err != nil { + l4g.Error("Encountered an error revoking an access token, err=" + err.Message) + } + + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Authorization code already exchanged for an access token", "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while pulling user from database", "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving session to database", "") + return + } else { + session = result.Data.(*model.Session) + api.AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving access token to database", "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: model.SESSION_TIME_OAUTH_IN_SECS} + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(accessRsp.ToJson())) +} diff --git a/web/web_test.go b/web/web_test.go index ccd0bba56..3da7eb2dc 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -6,8 +6,11 @@ package web import ( "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" + "net/url" + "strings" "testing" "time" ) @@ -23,7 +26,7 @@ func Setup() { api.InitApi() InitWeb() URL = "http://localhost:" + utils.Cfg.ServiceSettings.Port - ApiClient = model.NewClient(URL + "/api/v1") + ApiClient = model.NewClient(URL) } } @@ -48,6 +51,135 @@ func TestStatic(t *testing.T) { } } +func TestGetAccessToken(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := ApiClient.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User) + store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id)) + + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}} + + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - oauth providing turned off") + } + } else { + + ApiClient.Must(ApiClient.LoginById(ruser.Id, "pwd")) + app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp) + + redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"] + rurl, _ := url.Parse(redirect) + + ApiClient.Logout() + + data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}} + + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - bad grant type") + } + + data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + data.Set("client_id", "") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - missing client id") + } + data.Set("client_id", "junk") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - bad client id") + } + + data.Set("client_id", app.Id) + data.Set("client_secret", "") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - missing client secret") + } + + data.Set("client_secret", "junk") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - bad client secret") + } + + data.Set("client_secret", app.ClientSecret) + data.Set("code", "") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - missing code") + } + + data.Set("code", "junk") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - bad code") + } + + data.Set("code", rurl.Query().Get("code")) + data.Set("redirect_uri", "junk") + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - non-matching redirect uri") + } + + // reset data for successful request + data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + data.Set("client_id", app.Id) + data.Set("client_secret", app.ClientSecret) + data.Set("code", rurl.Query().Get("code")) + data.Set("redirect_uri", app.CallbackUrls[0]) + + token := "" + if result, err := ApiClient.GetAccessToken(data); err != nil { + t.Fatal(err) + } else { + rsp := result.Data.(*model.AccessResponse) + if len(rsp.AccessToken) == 0 { + t.Fatal("access token not returned") + } else { + token = rsp.AccessToken + } + if rsp.TokenType != model.ACCESS_TOKEN_TYPE { + t.Fatal("access token type incorrect") + } + } + + if result, err := ApiClient.DoApiGet("/users/profiles?access_token="+token, "", ""); err != nil { + t.Fatal(err) + } else { + userMap := model.UserMapFromJson(result.Body) + if len(userMap) == 0 { + t.Fatal("user map empty - did not get results correctly") + } + } + + if _, err := ApiClient.DoApiGet("/users/profiles", "", ""); err == nil { + t.Fatal("should have failed - no access token provided") + } + + if _, err := ApiClient.DoApiGet("/users/profiles?access_token=junk", "", ""); err == nil { + t.Fatal("should have failed - bad access token provided") + } + + ApiClient.SetOAuthToken(token) + if result, err := ApiClient.DoApiGet("/users/profiles", "", ""); err != nil { + t.Fatal(err) + } else { + userMap := model.UserMapFromJson(result.Body) + if len(userMap) == 0 { + t.Fatal("user map empty - did not get results correctly") + } + } + + if _, err := ApiClient.GetAccessToken(data); err == nil { + t.Fatal("should have failed - tried to reuse auth code") + } + + ApiClient.ClearOAuthToken() + } +} + func TestZZWebTearDown(t *testing.T) { // *IMPORTANT* // This should be the last function in any test file |