summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin.go5
-rw-r--r--api/admin_test.go9
-rw-r--r--api/api.go12
-rw-r--r--api/api_test.go2
-rw-r--r--api/channel_test.go4
-rw-r--r--api/command.go8
-rw-r--r--api/config.go34
-rw-r--r--api/context.go89
-rw-r--r--api/oauth.go165
-rw-r--r--api/oauth_test.go157
-rw-r--r--api/post.go6
-rw-r--r--api/post_test.go4
-rw-r--r--api/team.go21
-rw-r--r--api/team_test.go2
-rw-r--r--api/templates/email_change_body.html6
-rw-r--r--api/templates/error.html6
-rw-r--r--api/templates/find_teams_body.html6
-rw-r--r--api/templates/find_teams_subject.html2
-rw-r--r--api/templates/invite_body.html8
-rw-r--r--api/templates/invite_subject.html2
-rw-r--r--api/templates/password_change_body.html6
-rw-r--r--api/templates/password_change_subject.html2
-rw-r--r--api/templates/post_body.html6
-rw-r--r--api/templates/post_subject.html2
-rw-r--r--api/templates/reset_body.html6
-rw-r--r--api/templates/signup_team_body.html8
-rw-r--r--api/templates/signup_team_subject.html2
-rw-r--r--api/templates/verify_body.html6
-rw-r--r--api/templates/verify_subject.html2
-rw-r--r--api/templates/welcome_body.html10
-rw-r--r--api/templates/welcome_subject.html2
-rw-r--r--api/user.go72
-rw-r--r--api/user_test.go17
-rw-r--r--config/config.json13
-rw-r--r--docker/dev/config_docker.json3
-rw-r--r--docker/local/config_docker.json3
-rw-r--r--manualtesting/manual_testing.go4
-rw-r--r--model/access.go56
-rw-r--r--model/access_test.go41
-rw-r--r--model/authorize.go103
-rw-r--r--model/authorize_test.go66
-rw-r--r--model/client.go199
-rw-r--r--model/oauth.go151
-rw-r--r--model/oauth_test.go95
-rw-r--r--model/session.go9
-rw-r--r--model/utils.go2
-rw-r--r--store/sql_oauth_store.go334
-rw-r--r--store/sql_oauth_store_test.go182
-rw-r--r--store/sql_session_store.go25
-rw-r--r--store/sql_session_store_test.go4
-rw-r--r--store/sql_store.go27
-rw-r--r--store/store.go19
-rw-r--r--utils/config.go86
-rw-r--r--web/react/components/authorize.jsx72
-rw-r--r--web/react/components/email_verify.jsx6
-rw-r--r--web/react/components/find_team.jsx7
-rw-r--r--web/react/components/get_link_modal.jsx5
-rw-r--r--web/react/components/invite_member_modal.jsx99
-rw-r--r--web/react/components/login.jsx7
-rw-r--r--web/react/components/navbar_dropdown.jsx5
-rw-r--r--web/react/components/password_reset_form.jsx3
-rw-r--r--web/react/components/popover_list_members.jsx2
-rw-r--r--web/react/components/post_list.jsx6
-rw-r--r--web/react/components/register_app_modal.jsx249
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/sidebar_right_menu.jsx5
-rw-r--r--web/react/components/signup_team_complete.jsx10
-rw-r--r--web/react/components/signup_user_complete.jsx11
-rw-r--r--web/react/components/team_general_tab.jsx9
-rw-r--r--web/react/components/team_signup_allowed_domains_page.jsx143
-rw-r--r--web/react/components/team_signup_choose_auth.jsx7
-rw-r--r--web/react/components/team_signup_display_name_page.jsx5
-rw-r--r--web/react/components/team_signup_password_page.jsx5
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx18
-rw-r--r--web/react/components/team_signup_url_page.jsx13
-rw-r--r--web/react/components/team_signup_username_page.jsx3
-rw-r--r--web/react/components/team_signup_welcome_page.jsx3
-rw-r--r--web/react/components/team_signup_with_email.jsx3
-rw-r--r--web/react/components/team_signup_with_sso.jsx5
-rw-r--r--web/react/components/user_profile.jsx3
-rw-r--r--web/react/components/user_settings.jsx10
-rw-r--r--web/react/components/user_settings_appearance.jsx17
-rw-r--r--web/react/components/user_settings_developer.jsx93
-rw-r--r--web/react/components/user_settings_general.jsx3
-rw-r--r--web/react/components/user_settings_modal.jsx11
-rw-r--r--web/react/components/user_settings_notifications.jsx3
-rw-r--r--web/react/components/view_image.jsx3
-rw-r--r--web/react/pages/authorize.jsx21
-rw-r--r--web/react/pages/channel.jsx34
-rw-r--r--web/react/pages/home.jsx6
-rw-r--r--web/react/pages/login.jsx8
-rw-r--r--web/react/pages/password_reset.jsx12
-rw-r--r--web/react/pages/signup_team.jsx8
-rw-r--r--web/react/pages/signup_team_complete.jsx8
-rw-r--r--web/react/pages/signup_user_complete.jsx16
-rw-r--r--web/react/pages/verify.jsx8
-rw-r--r--web/react/stores/config_store.jsx69
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx32
-rw-r--r--web/react/utils/config.js48
-rw-r--r--web/react/utils/text_formatting.jsx4
-rw-r--r--web/react/utils/utils.jsx5
-rw-r--r--web/sass-files/sass/partials/_signup.scss15
-rw-r--r--web/static/help/about.html (renamed from web/static/help/configure_links.html)4
-rw-r--r--web/static/help/help.html24
-rw-r--r--web/static/help/privacy.html24
-rw-r--r--web/static/help/report_problem.html24
-rw-r--r--web/static/help/terms.html24
-rw-r--r--web/templates/authorize.html26
-rw-r--r--web/templates/channel.html3
-rw-r--r--web/templates/footer.html10
-rw-r--r--web/templates/head.html39
-rw-r--r--web/templates/home.html2
-rw-r--r--web/templates/login.html2
-rw-r--r--web/templates/password_reset.html2
-rw-r--r--web/templates/signup_team.html4
-rw-r--r--web/templates/signup_team_complete.html2
-rw-r--r--web/templates/signup_user_complete.html2
-rw-r--r--web/templates/verify.html2
-rw-r--r--web/templates/welcome.html2
-rw-r--r--web/web.go214
-rw-r--r--web/web_test.go134
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>&nbsp;
- <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'>&times;</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