summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--api/admin.go56
-rw-r--r--api/admin_test.go44
-rw-r--r--api/api.go13
-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.go99
-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.go25
-rw-r--r--api/team_test.go2
-rw-r--r--api/templates/email_change_body.html10
-rw-r--r--api/templates/error.html28
-rw-r--r--api/templates/find_teams_body.html10
-rw-r--r--api/templates/find_teams_subject.html2
-rw-r--r--api/templates/invite_body.html12
-rw-r--r--api/templates/invite_subject.html2
-rw-r--r--api/templates/password_change_body.html10
-rw-r--r--api/templates/password_change_subject.html2
-rw-r--r--api/templates/post_body.html10
-rw-r--r--api/templates/post_subject.html2
-rw-r--r--api/templates/reset_body.html10
-rw-r--r--api/templates/signup_team_body.html12
-rw-r--r--api/templates/signup_team_subject.html2
-rw-r--r--api/templates/verify_body.html10
-rw-r--r--api/templates/verify_subject.html2
-rw-r--r--api/templates/welcome_body.html14
-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--doc/developer/style-guide.md19
-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--mattermost.go14
-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.go206
-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.go102
-rw-r--r--web/react/components/admin_console/admin_controller.jsx3
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx10
-rw-r--r--web/react/components/admin_console/email_settings.jsx2
-rw-r--r--web/react/components/admin_console/jobs_settings.jsx2
-rw-r--r--web/react/components/admin_console/logs.jsx88
-rw-r--r--web/react/components/authorize.jsx72
-rw-r--r--web/react/components/change_url_modal.jsx2
-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/new_channel_modal.jsx2
-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/search_bar.jsx4
-rw-r--r--web/react/components/setting_item_max.jsx2
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/sidebar.jsx78
-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/unread_channel_indicator.jsx35
-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/package.json7
-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/admin_store.jsx58
-rw-r--r--web/react/stores/config_store.jsx69
-rw-r--r--web/react/stores/post_store.jsx1
-rw-r--r--web/react/utils/async_client.jsx52
-rw-r--r--web/react/utils/client.jsx46
-rw-r--r--web/react/utils/config.js48
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/text_formatting.jsx4
-rw-r--r--web/react/utils/utils.jsx7
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss10
-rw-r--r--web/sass-files/sass/partials/_headers.scss8
-rw-r--r--web/sass-files/sass/partials/_responsive.scss3
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss10
-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
-rwxr-xr-xweb/static/js/perfect-scrollbar-0.6.3.jquery.min.js2
-rwxr-xr-xweb/static/js/perfect-scrollbar-0.6.5.jquery.js (renamed from web/static/js/perfect-scrollbar-0.6.3.jquery.js)65
-rwxr-xr-xweb/static/js/perfect-scrollbar-0.6.5.jquery.min.js2
-rw-r--r--web/static/js/react-bootstrap-0.25.1.min.js1
-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.html41
-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.go222
-rw-r--r--web/web_test.go134
149 files changed, 3486 insertions, 1012 deletions
diff --git a/Makefile b/Makefile
index 137bb0a82..cd82c27a3 100644
--- a/Makefile
+++ b/Makefile
@@ -41,7 +41,7 @@ travis:
cd web/react && $(ESLINT) --quiet components/* dispatcher/* pages/* stores/* utils/*
@$(GO) build $(GOFLAGS) ./...
- @$(GO) install $(GOFLAGS) -a ./...
+ @$(GO) install $(GOFLAGS) ./...
@mkdir -p logs
@@ -204,8 +204,8 @@ cleandb:
fi
dist: install
- @$(GO) build $(GOFLAGS) -i -a ./...
- @$(GO) install $(GOFLAGS) -a ./...
+ @$(GO) build $(GOFLAGS) -i ./...
+ @$(GO) install $(GOFLAGS) ./...
mkdir -p $(DIST_PATH)/bin
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
diff --git a/api/admin.go b/api/admin.go
new file mode 100644
index 000000000..6d7a9028f
--- /dev/null
+++ b/api/admin.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "bufio"
+ "net/http"
+ "os"
+
+ l4g "code.google.com/p/log4go"
+ "github.com/gorilla/mux"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func InitAdmin(r *mux.Router) {
+ l4g.Debug("Initializing admin api routes")
+
+ 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) {
+
+ if !c.HasSystemAdminPermissions("getLogs") {
+ return
+ }
+
+ var lines []string
+
+ if utils.Cfg.LogSettings.FileEnable {
+
+ file, err := os.Open(utils.Cfg.LogSettings.FileLocation)
+ if err != nil {
+ c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error())
+ }
+
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+ } else {
+ lines = append(lines, "")
+ }
+
+ 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
new file mode 100644
index 000000000..e67077c55
--- /dev/null
+++ b/api/admin_test.go
@@ -0,0 +1,44 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestGetLogs(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if logs, err := Client.GetLogs(); err != nil {
+ t.Fatal(err)
+ } else if len(logs.Data.([]string)) <= 0 {
+ 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 9770930f7..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,7 +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 d97295e5e..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)
}
@@ -295,6 +306,16 @@ func (c *Context) IsSystemAdmin() bool {
return false
}
+func (c *Context) HasSystemAdminPermissions(where string) bool {
+ if c.IsSystemAdmin() {
+ return true
+ }
+
+ c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+c.Session.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return false
+}
+
func (c *Context) IsTeamAdmin(userId string) bool {
if uresult := <-Srv.Store.User().Get(userId); uresult.Err != nil {
c.Err = uresult.Err
@@ -307,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,
@@ -463,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..92fcbff93 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)
@@ -98,6 +100,10 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if !isTreamCreationAllowed(c, team.Email) {
+ return
+ }
+
team.PreSave()
team.Name = model.CleanTeamName(team.Name)
@@ -401,8 +407,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 +491,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..0ec4ace2a 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>
@@ -23,9 +23,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -34,11 +34,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
diff --git a/api/templates/error.html b/api/templates/error.html
index adb8f9f7d..760578896 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -1,20 +1,30 @@
<html>
<head>
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- <title>{{ .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>
- <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
- <link rel="stylesheet" href="/static/css/styles.css">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <title>{{ .ClientProps.SiteName }} - Error</title>
+
+ <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
+ <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
+
+ <script src="/static/js/react-with-addons-0.13.3.min.js"></script>
+ <script src="/static/js/jquery-1.11.1.min.js"></script>
+ <script src="/static/js/bootstrap-3.3.5.min.js"></script>
+ <script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
+
+ <link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
+ <link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
+ <link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
+ <link rel="stylesheet" href="/static/css/styles.css">
+
+
</head>
<body class="white error">
<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..9d34b7a23 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>
@@ -31,9 +31,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -42,11 +42,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..9e1ce33b2 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>
@@ -26,9 +26,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -37,11 +37,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..3fef3a5c8 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>
@@ -23,9 +23,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -34,11 +34,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..a6b81e2f6 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>
@@ -26,9 +26,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -37,11 +37,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..dc6152627 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>
@@ -26,9 +26,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -37,11 +37,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index b49cf5f36..f5c0e62b0 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>
@@ -29,9 +29,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -40,11 +40,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..8187c8908 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>
@@ -26,9 +26,9 @@
</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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -37,11 +37,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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..c75e14c6a 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>
+ Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
Best wishes,<br>
- The {{.SiteName}} Team<br>
+ The {{.ClientProps.SiteName}} Team<br>
</td>
</tr>
</table>
@@ -34,11 +34,11 @@
<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>
- If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
+ If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a>
</p>
</td>
</tr>
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/doc/developer/style-guide.md b/doc/developer/style-guide.md
index 470788cf5..a06f9e29b 100644
--- a/doc/developer/style-guide.md
+++ b/doc/developer/style-guide.md
@@ -14,11 +14,13 @@ In addition all code must be run though the official go formatter tool [gofmt](h
## Javascript
-Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors or warnings running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](https://github.com/mattermost/platform/blob/master/web/react/.eslintrc.json)
+Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](/web/react/.eslintrc)
Instructions on how to use ESLint with your favourite editor can be found here: [http://eslint.org/docs/user-guide/integrations](http://eslint.org/docs/user-guide/integrations)
-The following is an abridged version of the [Airbnb Javascript Style Guide](https://github.com/airbnb/javascript/blob/master/README.md#airbnb-javascript-style-guide-), with modifications. Anything that is unclear here follow that guide. If there is a conflict, follow what is said below.
+You can run eslint using the makefile by using `make check`
+
+The following is a subset of what ESLint checks for. ESLint is always the authority.
### Whitespace
@@ -53,10 +55,10 @@ function myFunction ( parm1, parm2 ){
```javascript
// Correct
-var x = 1;
+let x = 1;
// Incorrect
-var x = 1
+let x = 1
```
### Variables
@@ -110,7 +112,7 @@ if (something)
### Strings
-- Use template strings instead of concatenation.
+- Use of template strings is preferred instead of concatenation.
```javascript
// Correct
@@ -126,17 +128,18 @@ function wrongGetStr(stuff) {
## React-JSX
-Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors or warnings running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](https://github.com/mattermost/platform/blob/master/web/react/.eslintrc.json)
+Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](/web/react/.eslintrc)
Instructions on how to use ESLint with your favourite editor can be found here: [http://eslint.org/docs/user-guide/integrations](http://eslint.org/docs/user-guide/integrations)
-This is an abridged version of the [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react#airbnb-reactjsx-style-guide). Anything that is unclear here follow that guide. If there is a conflict, follow what is said below.
+You can run eslint using the makefile by using `make check`
+
+The following is a subset of what ESLint checks for. ESLint is always the authority.
### General
- Include only one React component per file.
- Use class \<name\> extends React.Component over React.createClass unless you need mixins
-- CapitalCamelCase with .jsx extension for component filenames.
- Filenames should be the component name.
### Alignment
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/mattermost.go b/mattermost.go
index 499abcd92..0bdb90424 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -318,24 +318,24 @@ Usage:
role can only be created on the
team named "admin"
- -create_team Creates a team. It requres the -team_name
+ -create_team Creates a team. It requires the -team_name
and -email flag to create a team.
Example:
platform -create_team -team_name="name" -email="user@example.com"
- -create_user Creates a user. It requres the -team_name,
+ -create_user Creates a user. It requires the -team_name,
-email and -password flag to create a user.
Example:
platform -create_user -team_name="name" -email="user@example.com" -password="mypassword"
- -assign_role Assigns role to a user. It requres the -role,
- -email and -team_name flag. If you're assigning the
- role="system_admin" role it must be for a user on the
- team_name="admin"
+ -assign_role Assigns role to a user. It requires the -role,
+ -email and -team_name flag. You may need to logout
+ of your current sessions for the new role to be
+ applied.
Example:
platform -assign_role -team_name="name" -email="user@example.com" -role="admin"
- -reset_password Resets the password for a user. It requres the
+ -reset_password Resets the password for a user. It requires the
-team_name, -email and -password flag.
Example:
platform -reset_password -team_name="name" -email="user@example.com" -paossword="newpassword"
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 c355b90f5..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),
@@ -338,8 +367,26 @@ func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
}
}
+func (c *Client) GetLogs() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/logs", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
+ }
+}
+
+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),
@@ -348,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),
@@ -357,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),
@@ -366,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),
@@ -375,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),
@@ -384,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),
@@ -393,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),
@@ -402,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),
@@ -411,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),
@@ -420,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),
@@ -429,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),
@@ -438,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),
@@ -449,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),
@@ -460,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),
@@ -469,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),
@@ -478,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),
@@ -487,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),
@@ -496,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),
@@ -505,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),
@@ -514,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),
@@ -523,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),
@@ -532,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),
@@ -541,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),
@@ -550,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),
@@ -559,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 {
@@ -581,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 {
@@ -600,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)
@@ -617,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),
@@ -626,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),
@@ -635,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),
@@ -647,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),
@@ -656,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),
@@ -670,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),
@@ -679,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),
@@ -688,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),
@@ -697,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),
@@ -706,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),
@@ -714,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 c67e17e79..0eb8329d1 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -4,33 +4,38 @@
package utils
import (
- l4g "code.google.com/p/log4go"
"encoding/json"
+ "fmt"
"os"
"path/filepath"
+ "strconv"
+
+ l4g "code.google.com/p/log4go"
)
const (
- MODE_DEV = "dev"
- MODE_BETA = "beta"
- MODE_PROD = "prod"
+ MODE_DEV = "dev"
+ MODE_BETA = "beta"
+ MODE_PROD = "prod"
+ LOG_ROTATE_SIZE = 10000
)
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 {
@@ -108,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
@@ -132,6 +137,7 @@ type Config struct {
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
+ ClientSettings ClientSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
}
@@ -146,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 {
@@ -180,10 +188,10 @@ func ConfigureCmdLineLog() {
ls.ConsoleEnable = true
ls.ConsoleLevel = "ERROR"
ls.FileEnable = false
- configureLog(ls)
+ configureLog(&ls)
}
-func configureLog(s LogSettings) {
+func configureLog(s *LogSettings) {
l4g.Close()
@@ -217,7 +225,7 @@ func configureLog(s LogSettings) {
flw := l4g.NewFileLogWriter(s.FileLocation, false)
flw.SetFormat(s.FileFormat)
flw.SetRotate(true)
- flw.SetRotateLines(100000)
+ flw.SetRotateLines(LOG_ROTATE_SIZE)
l4g.AddFilter("file", level, flw)
}
}
@@ -241,22 +249,50 @@ func LoadConfig(fileName string) {
panic("Error decoding config file=" + fileName + ", err=" + err.Error())
}
- configureLog(config.LogSettings)
+ 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["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
+ 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/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index bb43af802..68984c9e0 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -4,6 +4,7 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var EmailTab = require('./email_settings.jsx');
var JobsTab = require('./jobs_settings.jsx');
+var LogsTab = require('./logs.jsx');
var Navbar = require('../../components/navbar.jsx');
export default class AdminController extends React.Component {
@@ -28,6 +29,8 @@ export default class AdminController extends React.Component {
tab = <EmailTab />;
} else if (this.state.selected === 'job_settings') {
tab = <JobsTab />;
+ } else if (this.state.selected === 'logs') {
+ tab = <LogsTab />;
}
return (
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 6b3be89d0..a04bceef5 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -83,7 +83,15 @@ export default class AdminSidebar extends React.Component {
{'Email Settings'}
</a>
</li>
- <li><a href='#'>{'Other Settings'}</a></li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('logs')}
+ onClick={this.handleClick.bind(null, 'logs')}
+ >
+ {'Logs'}
+ </a>
+ </li>
</ul>
</li>
<li>
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 3c53a8ee1..e8fb25858 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -300,7 +300,7 @@ export default class EmailSettings extends React.Component {
type='submit'
className='btn btn-primary'
>
- {'Submit'}
+ {'Save'}
</button>
</div>
</div>
diff --git a/web/react/components/admin_console/jobs_settings.jsx b/web/react/components/admin_console/jobs_settings.jsx
index 34ec9693d..0b4fc4185 100644
--- a/web/react/components/admin_console/jobs_settings.jsx
+++ b/web/react/components/admin_console/jobs_settings.jsx
@@ -172,7 +172,7 @@ export default class Jobs extends React.Component {
type='submit'
className='btn btn-primary'
>
- {'Submit'}
+ {'Save'}
</button>
</div>
</div>
diff --git a/web/react/components/admin_console/logs.jsx b/web/react/components/admin_console/logs.jsx
new file mode 100644
index 000000000..d7de76a94
--- /dev/null
+++ b/web/react/components/admin_console/logs.jsx
@@ -0,0 +1,88 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AdminStore = require('../../stores/admin_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class Logs extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onLogListenerChange = this.onLogListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ logs: AdminStore.getLogs()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addLogChangeListener(this.onLogListenerChange);
+ AsyncClient.getLogs();
+ }
+ componentWillUnmount() {
+ AdminStore.removeLogChangeListener(this.onLogListenerChange);
+ }
+ onLogListenerChange() {
+ this.setState({
+ logs: AdminStore.getLogs()
+ });
+ }
+
+ reload() {
+ AdminStore.saveLogs(null);
+ this.setState({
+ logs: null
+ });
+
+ AsyncClient.getLogs();
+ }
+
+ render() {
+ var content = null;
+
+ if (this.state.logs === null) {
+ content = <LoadingScreen />;
+ } else {
+ content = [];
+
+ for (var i = 0; i < this.state.logs.length; i++) {
+ var style = {
+ whiteSpace: 'nowrap',
+ fontFamily: 'monospace'
+ };
+
+ if (this.state.logs[i].indexOf('[EROR]') > 0) {
+ style.color = 'red';
+ }
+
+ content.push(<br key={'br_' + i} />);
+ content.push(
+ <span
+ key={'log_' + i}
+ style={style}
+ >
+ {this.state.logs[i]}
+ </span>
+ );
+ }
+ }
+
+ return (
+ <div className='panel'>
+ <h3>{'Server Logs'}</h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ {'Reload'}
+ </button>
+ <div className='log__panel'>
+ {content}
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
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/change_url_modal.jsx b/web/react/components/change_url_modal.jsx
index 28fa70c1f..3553e1107 100644
--- a/web/react/components/change_url_modal.jsx
+++ b/web/react/components/change_url_modal.jsx
@@ -159,7 +159,7 @@ ChangeUrlModal.defaultProps = {
title: 'Change URL',
desciption: '',
urlLabel: 'URL',
- submitButtonText: 'Submit',
+ submitButtonText: 'Save',
currentURL: '',
serverError: ''
};
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/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index 2bf645dc5..f3fb8da2a 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -127,7 +127,7 @@ export default class NewChannelModal extends React.Component {
href='#'
onClick={this.props.onChangeURLPressed}
>
- {'change this URL'}
+ {'Edit'}
</a>
{')'}
</p>
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/search_bar.jsx b/web/react/components/search_bar.jsx
index 006d15459..77166fef9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -75,6 +75,9 @@ export default class SearchBar extends React.Component {
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
}
+ handleMouseInput(e) {
+ e.preventDefault();
+ }
handleUserFocus(e) {
e.target.select();
$('.search-bar__container').addClass('focused');
@@ -140,6 +143,7 @@ export default class SearchBar extends React.Component {
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onChange={this.handleUserInput}
+ onMouseUp={this.handleMouseInput}
/>
{isSearching}
</form>
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index b1bab1d48..1bffa7c79 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -26,7 +26,7 @@ export default class SettingItemMax extends React.Component {
href='#'
onClick={this.props.submit}
>
- Submit
+ Save
</a>
);
}
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.jsx b/web/react/components/sidebar.jsx
index 977fecb5c..87007edcc 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -13,6 +13,7 @@ var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx');
var Constants = require('../utils/constants.jsx');
var NewChannelFlow = require('./new_channel_flow.jsx');
+var UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -153,6 +154,16 @@ export default class Sidebar extends React.Component {
$(window).on('resize', this.onResize);
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areStatesEqual(nextProps, this.props)) {
+ return true;
+ }
+
+ if (!Utils.areStatesEqual(nextState, this.state)) {
+ return true;
+ }
+ return false;
+ }
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
@@ -274,15 +285,16 @@ export default class Sidebar extends React.Component {
this.updateUnreadIndicators();
}
updateUnreadIndicators() {
- var container = $(React.findDOMNode(this.refs.container));
+ const container = $(React.findDOMNode(this.refs.container));
+
+ var showTopUnread = false;
+ var showBottomUnread = false;
if (this.firstUnreadChannel) {
var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel]));
if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) {
- $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'initial');
- } else {
- $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'none');
+ showTopUnread = true;
}
}
@@ -290,11 +302,14 @@ export default class Sidebar extends React.Component {
var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel]));
if (lastUnreadElement.position().top > container.height()) {
- $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'initial');
- } else {
- $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'none');
+ showBottomUnread = true;
}
}
+
+ this.setState({
+ showTopUnread,
+ showBottomUnread
+ });
}
createChannelElement(channel, index) {
var members = this.state.members;
@@ -348,6 +363,11 @@ export default class Sidebar extends React.Component {
);
}
+ var badgeClass;
+ if (msgCount > 0) {
+ badgeClass = 'has-badge';
+ }
+
// set up status icon for direct message channels
var status = null;
if (channel.type === 'D') {
@@ -408,7 +428,7 @@ export default class Sidebar extends React.Component {
className={linkClass}
>
<a
- className={'sidebar-channel ' + titleClass}
+ className={'sidebar-channel ' + titleClass + ' ' + badgeClass}
href={href}
onClick={handleClick}
>
@@ -427,19 +447,13 @@ export default class Sidebar extends React.Component {
this.lastUnreadChannel = null;
// create elements for all 3 types of channels
- var channelItems = this.state.channels.filter(
- function filterPublicChannels(channel) {
- return channel.type === 'O';
- }
- ).map(this.createChannelElement);
+ const publicChannels = this.state.channels.filter((channel) => channel.type === 'O');
+ const publicChannelItems = publicChannels.map(this.createChannelElement);
- var privateChannelItems = this.state.channels.filter(
- function filterPrivateChannels(channel) {
- return channel.type === 'P';
- }
- ).map(this.createChannelElement);
+ const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
+ const privateChannelItems = privateChannels.map(this.createChannelElement);
- var directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
+ const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
// update the favicon to show if there are any notifications
var link = document.createElement('link');
@@ -493,20 +507,16 @@ export default class Sidebar extends React.Component {
/>
<SearchBox />
- <div
- ref='topUnreadIndicator'
- className='nav-pills__unread-indicator nav-pills__unread-indicator-top'
- style={{display: 'none'}}
- >
- Unread post(s) above
- </div>
- <div
- ref='bottomUnreadIndicator'
- className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom'
- style={{display: 'none'}}
- >
- Unread post(s) below
- </div>
+ <UnreadChannelIndicator
+ show={this.state.showTopUnread}
+ extraClass='nav-pills__unread-indicator-top'
+ text={'Unread post(s) above'}
+ />
+ <UnreadChannelIndicator
+ show={this.state.showBottomUnread}
+ extraClass='nav-pills__unread-indicator-bottom'
+ text={'Unread post(s) below'}
+ />
<div
ref='container'
@@ -526,7 +536,7 @@ export default class Sidebar extends React.Component {
</a>
</h4>
</li>
- {channelItems}
+ {publicChannelItems}
<li>
<a
href='#'
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/unread_channel_indicator.jsx b/web/react/components/unread_channel_indicator.jsx
new file mode 100644
index 000000000..12a67633e
--- /dev/null
+++ b/web/react/components/unread_channel_indicator.jsx
@@ -0,0 +1,35 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+// Indicator for the left sidebar which indicate if there's unread posts in a channel that is not shown
+// because it is either above or below the screen
+export default class UnreadChannelIndicator extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ let displayValue = 'none';
+ if (this.props.show) {
+ displayValue = 'initial';
+ }
+ return (
+ <div
+ className={'nav-pills__unread-indicator ' + this.props.extraClass}
+ style={{display: displayValue}}
+ >
+ {this.props.text}
+ </div>
+ );
+ }
+}
+
+UnreadChannelIndicator.defaultProps = {
+ show: false,
+ extraClass: '',
+ text: ''
+};
+UnreadChannelIndicator.propTypes = {
+ show: React.PropTypes.bool,
+ extraClass: React.PropTypes.string,
+ text: React.PropTypes.string
+};
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..67a4d0041 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 === 'true') {
+ 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/package.json b/web/react/package.json
index 11d60376d..04e0f6bab 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -8,7 +8,8 @@
"keymirror": "0.1.1",
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
- "twemoji": "1.4.1"
+ "twemoji": "1.4.1",
+ "babel-runtime": "5.8.24"
},
"devDependencies": {
"browserify": "11.0.1",
@@ -26,8 +27,8 @@
},
"browserify": {
"transform": [
- "babelify",
- "envify"
+ ["babelify", { "optional": ["runtime"] }],
+ "envify"
]
},
"jest": {
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/admin_store.jsx b/web/react/stores/admin_store.jsx
new file mode 100644
index 000000000..591b52d05
--- /dev/null
+++ b/web/react/stores/admin_store.jsx
@@ -0,0 +1,58 @@
+// 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 Constants = require('../utils/constants.jsx');
+var ActionTypes = Constants.ActionTypes;
+
+var LOG_CHANGE_EVENT = 'log_change';
+
+class AdminStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.logs = null;
+
+ this.emitLogChange = this.emitLogChange.bind(this);
+ this.addLogChangeListener = this.addLogChangeListener.bind(this);
+ this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
+ }
+
+ emitLogChange() {
+ this.emit(LOG_CHANGE_EVENT);
+ }
+
+ addLogChangeListener(callback) {
+ this.on(LOG_CHANGE_EVENT, callback);
+ }
+
+ removeLogChangeListener(callback) {
+ this.removeListener(LOG_CHANGE_EVENT, callback);
+ }
+
+ getLogs() {
+ return this.logs;
+ }
+
+ saveLogs(logs) {
+ this.logs = logs;
+ }
+}
+
+var AdminStore = new AdminStoreClass();
+
+AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIEVED_LOGS:
+ AdminStore.saveLogs(action.logs);
+ AdminStore.emitLogChange();
+ break;
+ default:
+ }
+});
+
+export default AdminStore;
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/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 5ffe65021..29ce47300 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -297,6 +297,7 @@ class PostStoreClass extends EventEmitter {
post.message = '(message deleted)';
post.state = Constants.POST_DELETED;
+ post.filenames = [];
posts[post.id] = post;
this.storeUnseenDeletedPosts(post.channel_id, posts);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 6ccef0506..3e23e5c33 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -319,6 +319,32 @@ export function getAudits() {
);
}
+export function getLogs() {
+ if (isCallInProgress('getLogs')) {
+ return;
+ }
+
+ callTracker.getLogs = utils.getTimestamp();
+ client.getLogs(
+ (data, textStatus, xhr) => {
+ callTracker.getLogs = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_LOGS,
+ logs: data
+ });
+ },
+ (err) => {
+ callTracker.getLogs = 0;
+ dispatchError(err, 'getLogs');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
@@ -556,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 51fd16474..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);
@@ -294,6 +291,20 @@ export function getAudits(userId, success, error) {
});
}
+export function getLogs(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/logs',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success: success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getLogs', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -977,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/config/get_all',
+ 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/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/constants.jsx b/web/react/utils/constants.jsx
index 7ead079d7..03e4635b5 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -34,7 +34,9 @@ module.exports = {
CLICK_TEAM: null,
RECIEVED_TEAM: null,
- RECIEVED_CONFIG: null
+ RECIEVED_CONFIG: null,
+
+ RECIEVED_LOGS: null
}),
PayloadSources: keyMirror({
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 85c6137a7..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
});
}
@@ -934,6 +933,6 @@ export function getTeamURLFromAddressBar() {
export function getShortenedTeamURL() {
const teamURL = getTeamURLFromAddressBar();
if (teamURL.length > 24) {
- return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length - 1) + '/';
+ return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/';
}
}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index b32cc1218..9823d2611 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -73,6 +73,16 @@
}
}
+.log__panel {
+ overflow: scroll;
+ width: 100%;
+ height: 800px;
+ border: 1px solid #ddd;
+ margin-top: 10px;
+ padding: 5px;
+ background-color: white;
+}
+
.app__content {
&.admin {
overflow: auto;
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index e83981397..702f0fd60 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -133,7 +133,7 @@
.navbar-right {
font-size: 0.85em;
position: absolute;
- top: 10px;
+ top: 11px;
right: 22px;
z-index: 5;
.dropdown-toggle {
@@ -171,7 +171,6 @@
}
.team__name, .user__name {
display: block;
- line-height: 18px;
font-weight: 600;
font-size: 16px;
max-width: 80%;
@@ -180,9 +179,14 @@
text-overflow: ellipsis;
text-decoration: none;
}
+ .team__name {
+ line-height: 22px;
+ margin-top: -2px;
+ }
.user__name {
@include single-transition(all, 0.1s, linear);
font-size: 14px;
+ line-height: 18px;
font-weight: 400;
color: #eee;
color: rgba(#fff, 0.8);
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index a30782dd0..d29c653ff 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -584,6 +584,9 @@
&.move--right {
@include translate3d(0, 0, 0);
}
+ .badge {
+ top: 13px;
+ }
> div {
padding-bottom: 105px;
}
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 514d22f24..f714a23f8 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -30,6 +30,9 @@
}
.badge {
background-color: $primary-color;
+ position: absolute;
+ right: 10px;
+ top: 5px;
}
.status {
position:relative;
@@ -90,6 +93,12 @@
line-height: 1.5;
border-radius: 0;
color: #999;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ &.has-badge {
+ padding-right: 30px;
+ }
&.nav-more {
text-decoration: underline;
}
@@ -104,6 +113,7 @@
&.active {
a, a:hover, a:focus {
color: #111;
+ padding-right: 10px;
background-color: #e1e1e1;
border-radius: 0;
font-weight: 400;
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/static/js/perfect-scrollbar-0.6.3.jquery.min.js b/web/static/js/perfect-scrollbar-0.6.3.jquery.min.js
deleted file mode 100755
index c2769dfab..000000000
--- a/web/static/js/perfect-scrollbar-0.6.3.jquery.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/* perfect-scrollbar v0.6.3 */
-!function t(e,n,r){function o(l,s){if(!n[l]){if(!e[l]){var a="function"==typeof require&&require;if(!s&&a)return a(l,!0);if(i)return i(l,!0);var c=new Error("Cannot find module '"+l+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[l]={exports:{}};e[l][0].call(u.exports,function(t){var n=e[l][1][t];return o(n?n:t)},u,u.exports,t,e,n,r)}return n[l].exports}for(var i="function"==typeof require&&require,l=0;l<r.length;l++)o(r[l]);return o}({1:[function(t,e,n){"use strict";function r(t){t.fn.perfectScrollbar=function(e){return this.each(function(){if("object"==typeof e||"undefined"==typeof e){var n=e;i.get(this)||o.initialize(this,n)}else{var r=e;"update"===r?o.update(this):"destroy"===r&&o.destroy(this)}return t(this)})}}var o=t("../main"),i=t("../plugin/instances");if("function"==typeof define&&define.amd)define(["jquery"],r);else{var l=window.jQuery?window.jQuery:window.$;"undefined"!=typeof l&&r(l)}e.exports=r},{"../main":7,"../plugin/instances":18}],2:[function(t,e,n){"use strict";function r(t,e){var n=t.className.split(" ");n.indexOf(e)<0&&n.push(e),t.className=n.join(" ")}function o(t,e){var n=t.className.split(" "),r=n.indexOf(e);r>=0&&n.splice(r,1),t.className=n.join(" ")}n.add=function(t,e){t.classList?t.classList.add(e):r(t,e)},n.remove=function(t,e){t.classList?t.classList.remove(e):o(t,e)},n.list=function(t){return t.classList?t.classList:t.className.split(" ")}},{}],3:[function(t,e,n){"use strict";function r(t,e){return window.getComputedStyle(t)[e]}function o(t,e,n){return"number"==typeof n&&(n=n.toString()+"px"),t.style[e]=n,t}function i(t,e){for(var n in e){var r=e[n];"number"==typeof r&&(r=r.toString()+"px"),t.style[n]=r}return t}n.e=function(t,e){var n=document.createElement(t);return n.className=e,n},n.appendTo=function(t,e){return e.appendChild(t),t},n.css=function(t,e,n){return"object"==typeof e?i(t,e):"undefined"==typeof n?r(t,e):o(t,e,n)},n.matches=function(t,e){return"undefined"!=typeof t.matches?t.matches(e):"undefined"!=typeof t.matchesSelector?t.matchesSelector(e):"undefined"!=typeof t.webkitMatchesSelector?t.webkitMatchesSelector(e):"undefined"!=typeof t.mozMatchesSelector?t.mozMatchesSelector(e):"undefined"!=typeof t.msMatchesSelector?t.msMatchesSelector(e):void 0},n.remove=function(t){"undefined"!=typeof t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)}},{}],4:[function(t,e,n){"use strict";var r=function(t){this.element=t,this.events={}};r.prototype.bind=function(t,e){"undefined"==typeof this.events[t]&&(this.events[t]=[]),this.events[t].push(e),this.element.addEventListener(t,e,!1)},r.prototype.unbind=function(t,e){var n="undefined"!=typeof e;this.events[t]=this.events[t].filter(function(r){return n&&r!==e?!0:(this.element.removeEventListener(t,r,!1),!1)},this)},r.prototype.unbindAll=function(){for(var t in this.events)this.unbind(t)};var o=function(){this.eventElements=[]};o.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return"undefined"==typeof e&&(e=new r(t),this.eventElements.push(e)),e},o.prototype.bind=function(t,e,n){this.eventElement(t).bind(e,n)},o.prototype.unbind=function(t,e,n){this.eventElement(t).unbind(e,n)},o.prototype.unbindAll=function(){for(var t=0;t<this.eventElements.length;t++)this.eventElements[t].unbindAll()},o.prototype.once=function(t,e,n){var r=this.eventElement(t),o=function(t){r.unbind(e,o),n(t)};r.bind(e,o)},e.exports=o},{}],5:[function(t,e,n){"use strict";e.exports=function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return function(){return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}}()},{}],6:[function(t,e,n){"use strict";var r=t("./class"),o=t("./dom");n.toInt=function(t){return parseInt(t,10)||0},n.clone=function(t){if(null===t)return null;if("object"==typeof t){var e={};for(var n in t)e[n]=this.clone(t[n]);return e}return t},n.extend=function(t,e){var n=this.clone(t);for(var r in e)n[r]=this.clone(e[r]);return n},n.isEditable=function(t){return o.matches(t,"input,[contenteditable]")||o.matches(t,"select,[contenteditable]")||o.matches(t,"textarea,[contenteditable]")||o.matches(t,"button,[contenteditable]")},n.removePsClasses=function(t){for(var e=r.list(t),n=0;n<e.length;n++){var o=e[n];0===o.indexOf("ps-")&&r.remove(t,o)}},n.outerWidth=function(t){return this.toInt(o.css(t,"width"))+this.toInt(o.css(t,"paddingLeft"))+this.toInt(o.css(t,"paddingRight"))+this.toInt(o.css(t,"borderLeftWidth"))+this.toInt(o.css(t,"borderRightWidth"))},n.startScrolling=function(t,e){r.add(t,"ps-in-scrolling"),"undefined"!=typeof e?r.add(t,"ps-"+e):(r.add(t,"ps-x"),r.add(t,"ps-y"))},n.stopScrolling=function(t,e){r.remove(t,"ps-in-scrolling"),"undefined"!=typeof e?r.remove(t,"ps-"+e):(r.remove(t,"ps-x"),r.remove(t,"ps-y"))},n.env={isWebKit:"WebkitAppearance"in document.documentElement.style,supportsTouch:"ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,supportsIePointer:null!==window.navigator.msMaxTouchPoints}},{"./class":2,"./dom":3}],7:[function(t,e,n){"use strict";var r=t("./plugin/destroy"),o=t("./plugin/initialize"),i=t("./plugin/update");e.exports={initialize:o,update:i,destroy:r}},{"./plugin/destroy":9,"./plugin/initialize":17,"./plugin/update":20}],8:[function(t,e,n){"use strict";e.exports={wheelSpeed:1,wheelPropagation:!1,swipePropagation:!0,minScrollbarLength:null,maxScrollbarLength:null,useBothWheelAxes:!1,useKeyboard:!0,suppressScrollX:!1,suppressScrollY:!1,scrollXMarginOffset:0,scrollYMarginOffset:0}},{}],9:[function(t,e,n){"use strict";var r=t("../lib/dom"),o=t("../lib/helper"),i=t("./instances");e.exports=function(t){var e=i.get(t);e.event.unbindAll(),r.remove(e.scrollbarX),r.remove(e.scrollbarY),r.remove(e.scrollbarXRail),r.remove(e.scrollbarYRail),o.removePsClasses(t),i.remove(t)}},{"../lib/dom":3,"../lib/helper":6,"./instances":18}],10:[function(t,e,n){"use strict";function r(t,e){function n(t){return t.getBoundingClientRect()}var r=window.Event.prototype.stopPropagation.bind;e.event.bind(e.scrollbarY,"click",r),e.event.bind(e.scrollbarYRail,"click",function(r){var i=o.toInt(e.scrollbarYHeight/2),s=e.railYRatio*(r.pageY-window.scrollY-n(e.scrollbarYRail).top-i),a=e.railYRatio*(e.railYHeight-e.scrollbarYHeight),c=s/a;0>c?c=0:c>1&&(c=1),t.scrollTop=(e.contentHeight-e.containerHeight)*c,l(t),r.stopPropagation()}),e.event.bind(e.scrollbarX,"click",r),e.event.bind(e.scrollbarXRail,"click",function(r){var i=o.toInt(e.scrollbarXWidth/2),s=e.railXRatio*(r.pageX-window.scrollX-n(e.scrollbarXRail).left-i),a=e.railXRatio*(e.railXWidth-e.scrollbarXWidth),c=s/a;0>c?c=0:c>1&&(c=1),t.scrollLeft=(e.contentWidth-e.containerWidth)*c-e.negativeScrollAdjustment,l(t),r.stopPropagation()})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],11:[function(t,e,n){"use strict";function r(t,e){function n(n){var o=r+n*e.railXRatio,i=e.scrollbarXRail.getBoundingClientRect().left+e.railXRatio*(e.railXWidth-e.scrollbarXWidth);e.scrollbarXLeft=0>o?0:o>i?i:o;var s=l.toInt(e.scrollbarXLeft*(e.contentWidth-e.containerWidth)/(e.containerWidth-e.railXRatio*e.scrollbarXWidth))-e.negativeScrollAdjustment;t.scrollLeft=s}var r=null,o=null,s=function(e){n(e.pageX-o),a(t),e.stopPropagation(),e.preventDefault()},c=function(){l.stopScrolling(t,"x"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarX,"mousedown",function(n){o=n.pageX,r=l.toInt(i.css(e.scrollbarX,"left"))*e.railXRatio,l.startScrolling(t,"x"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",c),n.stopPropagation(),n.preventDefault()})}function o(t,e){function n(n){var o=r+n*e.railYRatio,i=e.scrollbarYRail.getBoundingClientRect().top+e.railYRatio*(e.railYHeight-e.scrollbarYHeight);e.scrollbarYTop=0>o?0:o>i?i:o;var s=l.toInt(e.scrollbarYTop*(e.contentHeight-e.containerHeight)/(e.containerHeight-e.railYRatio*e.scrollbarYHeight));t.scrollTop=s}var r=null,o=null,s=function(e){n(e.pageY-o),a(t),e.stopPropagation(),e.preventDefault()},c=function(){l.stopScrolling(t,"y"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarY,"mousedown",function(n){o=n.pageY,r=l.toInt(i.css(e.scrollbarY,"top"))*e.railYRatio,l.startScrolling(t,"y"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",c),n.stopPropagation(),n.preventDefault()})}var i=t("../../lib/dom"),l=t("../../lib/helper"),s=t("../instances"),a=t("../update-geometry");e.exports=function(t){var e=s.get(t);r(t,e),o(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19}],12:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&0>r)return!e.settings.wheelPropagation}var i=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===i&&0>n||i>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}var r=!1;e.event.bind(t,"mouseenter",function(){r=!0}),e.event.bind(t,"mouseleave",function(){r=!1});var i=!1;e.event.bind(e.ownerDocument,"keydown",function(s){if((!s.isDefaultPrevented||!s.isDefaultPrevented())&&r){var a=document.activeElement?document.activeElement:e.ownerDocument.activeElement;if(a){for(;a.shadowRoot;)a=a.shadowRoot.activeElement;if(o.isEditable(a))return}var c=0,u=0;switch(s.which){case 37:c=-30;break;case 38:u=30;break;case 39:c=30;break;case 40:u=-30;break;case 33:u=90;break;case 32:case 34:u=-90;break;case 35:u=s.ctrlKey?-e.contentHeight:-e.containerHeight;break;case 36:u=s.ctrlKey?t.scrollTop:e.containerHeight;break;default:return}t.scrollTop=t.scrollTop-u,t.scrollLeft=t.scrollLeft+c,l(t),i=n(c,u),i&&s.preventDefault()}})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],13:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&0>r)return!e.settings.wheelPropagation}var i=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===i&&0>n||i>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}function r(t){var e=t.deltaX,n=-1*t.deltaY;return("undefined"==typeof e||"undefined"==typeof n)&&(e=-1*t.wheelDeltaX/6,n=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,n*=10),e!==e&&n!==n&&(e=0,n=t.wheelDelta),[e,n]}function i(e,n){var r=t.querySelector("textarea:hover");if(r){var o=r.scrollHeight-r.clientHeight;if(o>0&&!(0===r.scrollTop&&n>0||r.scrollTop===o&&0>n))return!0;var i=r.scrollLeft-r.clientWidth;if(i>0&&!(0===r.scrollLeft&&0>e||r.scrollLeft===i&&e>0))return!0}return!1}function s(s){if(o.env.isWebKit||!t.querySelector("select:focus")){var c=r(s),u=c[0],d=c[1];i(u,d)||(a=!1,e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(t.scrollTop=d?t.scrollTop-d*e.settings.wheelSpeed:t.scrollTop+u*e.settings.wheelSpeed,a=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(t.scrollLeft=u?t.scrollLeft+u*e.settings.wheelSpeed:t.scrollLeft-d*e.settings.wheelSpeed,a=!0):(t.scrollTop=t.scrollTop-d*e.settings.wheelSpeed,t.scrollLeft=t.scrollLeft+u*e.settings.wheelSpeed),l(t),a=a||n(u,d),a&&(s.stopPropagation(),s.preventDefault()))}}var a=!1;"undefined"!=typeof window.onwheel?e.event.bind(t,"wheel",s):"undefined"!=typeof window.onmousewheel&&e.event.bind(t,"mousewheel",s)}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],14:[function(t,e,n){"use strict";function r(t,e){e.event.bind(t,"scroll",function(){i(t)})}var o=t("../instances"),i=t("../update-geometry");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19}],15:[function(t,e,n){"use strict";function r(t,e){function n(){var t=window.getSelection?window.getSelection():document.getSelection?document.getSelection():"";return 0===t.toString().length?null:t.getRangeAt(0).commonAncestorContainer}function r(){a||(a=setInterval(function(){return i.get(t)?(t.scrollTop=t.scrollTop+c.top,t.scrollLeft=t.scrollLeft+c.left,void l(t)):void clearInterval(a)},50))}function s(){a&&(clearInterval(a),a=null),o.stopScrolling(t)}var a=null,c={top:0,left:0},u=!1;e.event.bind(e.ownerDocument,"selectionchange",function(){t.contains(n())?u=!0:(u=!1,s())}),e.event.bind(window,"mouseup",function(){u&&(u=!1,s())}),e.event.bind(window,"mousemove",function(e){if(u){var n={x:e.pageX,y:e.pageY},i={left:t.offsetLeft,right:t.offsetLeft+t.offsetWidth,top:t.offsetTop,bottom:t.offsetTop+t.offsetHeight};n.x<i.left+3?(c.left=-5,o.startScrolling(t,"x")):n.x>i.right-3?(c.left=5,o.startScrolling(t,"x")):c.left=0,n.y<i.top+3?(c.top=i.top+3-n.y<5?-5:-20,o.startScrolling(t,"y")):n.y>i.bottom-3?(c.top=n.y-i.bottom+3<5?5:20,o.startScrolling(t,"y")):c.top=0,0===c.top&&0===c.left?s():r()}})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],16:[function(t,e,n){"use strict";function r(t,e,n,r){function l(n,r){var o=t.scrollTop,i=t.scrollLeft,l=Math.abs(n),s=Math.abs(r);if(s>l){if(0>r&&o===e.contentHeight-e.containerHeight||r>0&&0===o)return!e.settings.swipePropagation}else if(l>s&&(0>n&&i===e.contentWidth-e.containerWidth||n>0&&0===i))return!e.settings.swipePropagation;return!0}function s(e,n){t.scrollTop=t.scrollTop-n,t.scrollLeft=t.scrollLeft-e,i(t)}function a(){Y=!0}function c(){Y=!1}function u(t){return t.targetTouches?t.targetTouches[0]:t}function d(t){return t.targetTouches&&1===t.targetTouches.length?!0:t.pointerType&&"mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE?!0:!1}function p(t){if(d(t)){y=!0;var e=u(t);b.pageX=e.pageX,b.pageY=e.pageY,g=(new Date).getTime(),null!==m&&clearInterval(m),t.stopPropagation()}}function f(t){if(!Y&&y&&d(t)){var e=u(t),n={pageX:e.pageX,pageY:e.pageY},r=n.pageX-b.pageX,o=n.pageY-b.pageY;s(r,o),b=n;var i=(new Date).getTime(),a=i-g;a>0&&(v.x=r/a,v.y=o/a,g=i),l(r,o)&&(t.stopPropagation(),t.preventDefault())}}function h(){!Y&&y&&(y=!1,clearInterval(m),m=setInterval(function(){return o.get(t)?Math.abs(v.x)<.01&&Math.abs(v.y)<.01?void clearInterval(m):(s(30*v.x,30*v.y),v.x*=.8,void(v.y*=.8)):void clearInterval(m)},10))}var b={},g=0,v={},m=null,Y=!1,y=!1;n&&(e.event.bind(window,"touchstart",a),e.event.bind(window,"touchend",c),e.event.bind(t,"touchstart",p),e.event.bind(t,"touchmove",f),e.event.bind(t,"touchend",h)),r&&(window.PointerEvent?(e.event.bind(window,"pointerdown",a),e.event.bind(window,"pointerup",c),e.event.bind(t,"pointerdown",p),e.event.bind(t,"pointermove",f),e.event.bind(t,"pointerup",h)):window.MSPointerEvent&&(e.event.bind(window,"MSPointerDown",a),e.event.bind(window,"MSPointerUp",c),e.event.bind(t,"MSPointerDown",p),e.event.bind(t,"MSPointerMove",f),e.event.bind(t,"MSPointerUp",h)))}var o=t("../instances"),i=t("../update-geometry");e.exports=function(t,e,n){var i=o.get(t);r(t,i,e,n)}},{"../instances":18,"../update-geometry":19}],17:[function(t,e,n){"use strict";var r=t("../lib/class"),o=t("../lib/helper"),i=t("./instances"),l=t("./update-geometry"),s=t("./handler/click-rail"),a=t("./handler/drag-scrollbar"),c=t("./handler/keyboard"),u=t("./handler/mouse-wheel"),d=t("./handler/native-scroll"),p=t("./handler/selection"),f=t("./handler/touch");e.exports=function(t,e){e="object"==typeof e?e:{},r.add(t,"ps-container");var n=i.add(t);n.settings=o.extend(n.settings,e),s(t),a(t),u(t),d(t),p(t),(o.env.supportsTouch||o.env.supportsIePointer)&&f(t,o.env.supportsTouch,o.env.supportsIePointer),n.settings.useKeyboard&&c(t),l(t)}},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(t,e,n){"use strict";function r(t){var e=this;e.settings=d.clone(a),e.containerWidth=null,e.containerHeight=null,e.contentWidth=null,e.contentHeight=null,e.isRtl="rtl"===s.css(t,"direction"),e.isNegativeScroll=function(){var e=t.scrollLeft,n=null;return t.scrollLeft=-1,n=t.scrollLeft<0,t.scrollLeft=e,n}(),e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,e.event=new c,e.ownerDocument=t.ownerDocument||document,e.scrollbarXRail=s.appendTo(s.e("div","ps-scrollbar-x-rail"),t),e.scrollbarX=s.appendTo(s.e("div","ps-scrollbar-x"),e.scrollbarXRail),e.scrollbarXActive=null,e.scrollbarXWidth=null,e.scrollbarXLeft=null,e.scrollbarXBottom=d.toInt(s.css(e.scrollbarXRail,"bottom")),e.isScrollbarXUsingBottom=e.scrollbarXBottom===e.scrollbarXBottom,e.scrollbarXTop=e.isScrollbarXUsingBottom?null:d.toInt(s.css(e.scrollbarXRail,"top")),e.railBorderXWidth=d.toInt(s.css(e.scrollbarXRail,"borderLeftWidth"))+d.toInt(s.css(e.scrollbarXRail,"borderRightWidth")),s.css(e.scrollbarXRail,"display","block"),e.railXMarginWidth=d.toInt(s.css(e.scrollbarXRail,"marginLeft"))+d.toInt(s.css(e.scrollbarXRail,"marginRight")),s.css(e.scrollbarXRail,"display",""),e.railXWidth=null,e.railXRatio=null,e.scrollbarYRail=s.appendTo(s.e("div","ps-scrollbar-y-rail"),t),e.scrollbarY=s.appendTo(s.e("div","ps-scrollbar-y"),e.scrollbarYRail),e.scrollbarYActive=null,e.scrollbarYHeight=null,e.scrollbarYTop=null,e.scrollbarYRight=d.toInt(s.css(e.scrollbarYRail,"right")),e.isScrollbarYUsingRight=e.scrollbarYRight===e.scrollbarYRight,e.scrollbarYLeft=e.isScrollbarYUsingRight?null:d.toInt(s.css(e.scrollbarYRail,"left")),e.scrollbarYOuterWidth=e.isRtl?d.outerWidth(e.scrollbarY):null,e.railBorderYWidth=d.toInt(s.css(e.scrollbarYRail,"borderTopWidth"))+d.toInt(s.css(e.scrollbarYRail,"borderBottomWidth")),s.css(e.scrollbarYRail,"display","block"),e.railYMarginHeight=d.toInt(s.css(e.scrollbarYRail,"marginTop"))+d.toInt(s.css(e.scrollbarYRail,"marginBottom")),s.css(e.scrollbarYRail,"display",""),e.railYHeight=null,e.railYRatio=null}function o(t){return"undefined"==typeof t.dataset?t.getAttribute("data-ps-id"):t.dataset.psId}function i(t,e){"undefined"==typeof t.dataset?t.setAttribute("data-ps-id",e):t.dataset.psId=e}function l(t){"undefined"==typeof t.dataset?t.removeAttribute("data-ps-id"):delete t.dataset.psId}var s=t("../lib/dom"),a=t("./default-setting"),c=t("../lib/event-manager"),u=t("../lib/guid"),d=t("../lib/helper"),p={};n.add=function(t){var e=u();return i(t,e),p[e]=new r(t),p[e]},n.remove=function(t){delete p[o(t)],l(t)},n.get=function(t){return p[o(t)]}},{"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(t,e,n){"use strict";function r(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function o(t,e){var n={width:e.railXWidth};n.left=e.isRtl?e.negativeScrollAdjustment+t.scrollLeft+e.containerWidth-e.contentWidth:t.scrollLeft,e.isScrollbarXUsingBottom?n.bottom=e.scrollbarXBottom-t.scrollTop:n.top=e.scrollbarXTop+t.scrollTop,l.css(e.scrollbarXRail,n);var r={top:t.scrollTop,height:e.railYHeight};e.isScrollbarYUsingRight?r.right=e.isRtl?e.contentWidth-(e.negativeScrollAdjustment+t.scrollLeft)-e.scrollbarYRight-e.scrollbarYOuterWidth:e.scrollbarYRight-t.scrollLeft:r.left=e.isRtl?e.negativeScrollAdjustment+t.scrollLeft+2*e.containerWidth-e.contentWidth-e.scrollbarYLeft-e.scrollbarYOuterWidth:e.scrollbarYLeft+t.scrollLeft,l.css(e.scrollbarYRail,r),l.css(e.scrollbarX,{left:e.scrollbarXLeft,width:e.scrollbarXWidth-e.railBorderXWidth}),l.css(e.scrollbarY,{top:e.scrollbarYTop,height:e.scrollbarYHeight-e.railBorderYWidth})}var i=t("../lib/class"),l=t("../lib/dom"),s=t("../lib/helper"),a=t("./instances");e.exports=function(t){var e=a.get(t);e.containerWidth=t.clientWidth,e.containerHeight=t.clientHeight,e.contentWidth=t.scrollWidth,e.contentHeight=t.scrollHeight,t.contains(e.scrollbarXRail)||l.appendTo(e.scrollbarXRail,t),t.contains(e.scrollbarYRail)||l.appendTo(e.scrollbarYRail,t),!e.settings.suppressScrollX&&e.containerWidth+e.settings.scrollXMarginOffset<e.contentWidth?(e.scrollbarXActive=!0,e.railXWidth=e.containerWidth-e.railXMarginWidth,e.railXRatio=e.containerWidth/e.railXWidth,e.scrollbarXWidth=r(e,s.toInt(e.railXWidth*e.containerWidth/e.contentWidth)),e.scrollbarXLeft=s.toInt((e.negativeScrollAdjustment+t.scrollLeft)*(e.railXWidth-e.scrollbarXWidth)/(e.contentWidth-e.containerWidth))):(e.scrollbarXActive=!1,e.scrollbarXWidth=0,e.scrollbarXLeft=0,t.scrollLeft=0),!e.settings.suppressScrollY&&e.containerHeight+e.settings.scrollYMarginOffset<e.contentHeight?(e.scrollbarYActive=!0,e.railYHeight=e.containerHeight-e.railYMarginHeight,e.railYRatio=e.containerHeight/e.railYHeight,e.scrollbarYHeight=r(e,s.toInt(e.railYHeight*e.containerHeight/e.contentHeight)),e.scrollbarYTop=s.toInt(t.scrollTop*(e.railYHeight-e.scrollbarYHeight)/(e.contentHeight-e.containerHeight))):(e.scrollbarYActive=!1,e.scrollbarYHeight=0,e.scrollbarYTop=0,t.scrollTop=0),e.scrollbarXLeft>=e.railXWidth-e.scrollbarXWidth&&(e.scrollbarXLeft=e.railXWidth-e.scrollbarXWidth),e.scrollbarYTop>=e.railYHeight-e.scrollbarYHeight&&(e.scrollbarYTop=e.railYHeight-e.scrollbarYHeight),o(t,e),i[e.scrollbarXActive?"add":"remove"](t,"ps-active-x"),i[e.scrollbarYActive?"add":"remove"](t,"ps-active-y")}},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18}],20:[function(t,e,n){"use strict";var r=t("../lib/dom"),o=t("../lib/helper"),i=t("./instances"),l=t("./update-geometry");e.exports=function(t){var e=i.get(t);e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,r.css(e.scrollbarXRail,"display","block"),r.css(e.scrollbarYRail,"display","block"),e.railXMarginWidth=o.toInt(r.css(e.scrollbarXRail,"marginLeft"))+o.toInt(r.css(e.scrollbarXRail,"marginRight")),e.railYMarginHeight=o.toInt(r.css(e.scrollbarYRail,"marginTop"))+o.toInt(r.css(e.scrollbarYRail,"marginBottom")),r.css(e.scrollbarXRail,"display","none"),r.css(e.scrollbarYRail,"display","none"),l(t),r.css(e.scrollbarXRail,"display",""),r.css(e.scrollbarYRail,"display","")}},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19}]},{},[1]); \ No newline at end of file
diff --git a/web/static/js/perfect-scrollbar-0.6.3.jquery.js b/web/static/js/perfect-scrollbar-0.6.5.jquery.js
index c4f0998a0..1473db9ad 100755
--- a/web/static/js/perfect-scrollbar-0.6.3.jquery.js
+++ b/web/static/js/perfect-scrollbar-0.6.5.jquery.js
@@ -1,4 +1,4 @@
-/* perfect-scrollbar v0.6.3 */
+/* perfect-scrollbar v0.6.5-1 */
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* Copyright (c) 2015 Hyunje Alex Jun and other contributors
* Licensed under the MIT License
@@ -100,13 +100,15 @@ exports.list = function (element) {
*/
'use strict';
-exports.e = function (tagName, className) {
+var DOM = {};
+
+DOM.e = function (tagName, className) {
var element = document.createElement(tagName);
element.className = className;
return element;
};
-exports.appendTo = function (child, parent) {
+DOM.appendTo = function (child, parent) {
parent.appendChild(child);
return child;
};
@@ -134,7 +136,7 @@ function cssMultiSet(element, obj) {
return element;
}
-exports.css = function (element, styleNameOrObject, styleValue) {
+DOM.css = function (element, styleNameOrObject, styleValue) {
if (typeof styleNameOrObject === 'object') {
// multiple set with object
return cssMultiSet(element, styleNameOrObject);
@@ -147,7 +149,7 @@ exports.css = function (element, styleNameOrObject, styleValue) {
}
};
-exports.matches = function (element, query) {
+DOM.matches = function (element, query) {
if (typeof element.matches !== 'undefined') {
return element.matches(query);
} else {
@@ -163,7 +165,7 @@ exports.matches = function (element, query) {
}
};
-exports.remove = function (element) {
+DOM.remove = function (element) {
if (typeof element.remove !== 'undefined') {
element.remove();
} else {
@@ -173,6 +175,14 @@ exports.remove = function (element) {
}
};
+DOM.queryChildren = function (element, selector) {
+ return Array.prototype.filter.call(element.childNodes, function (child) {
+ return DOM.matches(child, selector);
+ });
+};
+
+module.exports = DOM;
+
},{}],4:[function(require,module,exports){
/* Copyright (c) 2015 Hyunje Alex Jun and other contributors
* Licensed under the MIT License
@@ -386,7 +396,8 @@ module.exports = {
suppressScrollX: false,
suppressScrollY: false,
scrollXMarginOffset: 0,
- scrollYMarginOffset: 0
+ scrollYMarginOffset: 0,
+ stopPropagationOnClick: true
};
},{}],9:[function(require,module,exports){
@@ -402,6 +413,10 @@ var d = require('../lib/dom')
module.exports = function (element) {
var i = instances.get(element);
+ if (!i) {
+ return;
+ }
+
i.event.unbindAll();
d.remove(i.scrollbarX);
d.remove(i.scrollbarY);
@@ -426,9 +441,12 @@ function bindClickRailHandler(element, i) {
function pageOffset(el) {
return el.getBoundingClientRect();
}
- var stopPropagation = window.Event.prototype.stopPropagation.bind;
- i.event.bind(i.scrollbarY, 'click', stopPropagation);
+ if (i.settings.stopPropagationOnClick) {
+ i.event.bind(i.scrollbarY, 'click', function (e) {
+ e.stopPropagation();
+ });
+ }
i.event.bind(i.scrollbarYRail, 'click', function (e) {
var halfOfScrollbarLength = h.toInt(i.scrollbarYHeight / 2);
var positionTop = i.railYRatio * (e.pageY - window.scrollY - pageOffset(i.scrollbarYRail).top - halfOfScrollbarLength);
@@ -447,7 +465,11 @@ function bindClickRailHandler(element, i) {
e.stopPropagation();
});
- i.event.bind(i.scrollbarX, 'click', stopPropagation);
+ if (i.settings.stopPropagationOnClick) {
+ i.event.bind(i.scrollbarY, 'click', function (e) {
+ e.stopPropagation();
+ });
+ }
i.event.bind(i.scrollbarXRail, 'click', function (e) {
var halfOfScrollbarLength = h.toInt(i.scrollbarXWidth / 2);
var positionLeft = i.railXRatio * (e.pageX - window.scrollX - pageOffset(i.scrollbarXRail).left - halfOfScrollbarLength);
@@ -662,6 +684,12 @@ function bindKeyboardHandler(element, i) {
deltaY = 90;
break;
case 32: // space bar
+ if (e.shiftKey) {
+ deltaY = 90;
+ } else {
+ deltaY = -90;
+ }
+ break;
case 34: // page down
deltaY = -90;
break;
@@ -1368,10 +1396,23 @@ module.exports = function (element) {
i.contentWidth = element.scrollWidth;
i.contentHeight = element.scrollHeight;
+ var existingRails;
if (!element.contains(i.scrollbarXRail)) {
+ existingRails = d.queryChildren(element, '.ps-scrollbar-x-rail');
+ if (existingRails.length > 0) {
+ existingRails.forEach(function (rail) {
+ d.remove(rail);
+ });
+ }
d.appendTo(i.scrollbarXRail, element);
}
if (!element.contains(i.scrollbarYRail)) {
+ existingRails = d.queryChildren(element, '.ps-scrollbar-y-rail');
+ if (existingRails.length > 0) {
+ existingRails.forEach(function (rail) {
+ d.remove(rail);
+ });
+ }
d.appendTo(i.scrollbarYRail, element);
}
@@ -1428,6 +1469,10 @@ var d = require('../lib/dom')
module.exports = function (element) {
var i = instances.get(element);
+ if (!i) {
+ return;
+ }
+
// Recalcuate negative scrollLeft adjustment
i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0;
diff --git a/web/static/js/perfect-scrollbar-0.6.5.jquery.min.js b/web/static/js/perfect-scrollbar-0.6.5.jquery.min.js
new file mode 100755
index 000000000..804ae8c9b
--- /dev/null
+++ b/web/static/js/perfect-scrollbar-0.6.5.jquery.min.js
@@ -0,0 +1,2 @@
+/* perfect-scrollbar v0.6.5-1 */
+!function t(e,n,r){function o(l,s){if(!n[l]){if(!e[l]){var a="function"==typeof require&&require;if(!s&&a)return a(l,!0);if(i)return i(l,!0);var c=new Error("Cannot find module '"+l+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[l]={exports:{}};e[l][0].call(u.exports,function(t){var n=e[l][1][t];return o(n?n:t)},u,u.exports,t,e,n,r)}return n[l].exports}for(var i="function"==typeof require&&require,l=0;l<r.length;l++)o(r[l]);return o}({1:[function(t,e,n){"use strict";function r(t){t.fn.perfectScrollbar=function(e){return this.each(function(){if("object"==typeof e||"undefined"==typeof e){var n=e;i.get(this)||o.initialize(this,n)}else{var r=e;"update"===r?o.update(this):"destroy"===r&&o.destroy(this)}return t(this)})}}var o=t("../main"),i=t("../plugin/instances");if("function"==typeof define&&define.amd)define(["jquery"],r);else{var l=window.jQuery?window.jQuery:window.$;"undefined"!=typeof l&&r(l)}e.exports=r},{"../main":7,"../plugin/instances":18}],2:[function(t,e,n){"use strict";function r(t,e){var n=t.className.split(" ");n.indexOf(e)<0&&n.push(e),t.className=n.join(" ")}function o(t,e){var n=t.className.split(" "),r=n.indexOf(e);r>=0&&n.splice(r,1),t.className=n.join(" ")}n.add=function(t,e){t.classList?t.classList.add(e):r(t,e)},n.remove=function(t,e){t.classList?t.classList.remove(e):o(t,e)},n.list=function(t){return t.classList?t.classList:t.className.split(" ")}},{}],3:[function(t,e,n){"use strict";function r(t,e){return window.getComputedStyle(t)[e]}function o(t,e,n){return"number"==typeof n&&(n=n.toString()+"px"),t.style[e]=n,t}function i(t,e){for(var n in e){var r=e[n];"number"==typeof r&&(r=r.toString()+"px"),t.style[n]=r}return t}var l={};l.e=function(t,e){var n=document.createElement(t);return n.className=e,n},l.appendTo=function(t,e){return e.appendChild(t),t},l.css=function(t,e,n){return"object"==typeof e?i(t,e):"undefined"==typeof n?r(t,e):o(t,e,n)},l.matches=function(t,e){return"undefined"!=typeof t.matches?t.matches(e):"undefined"!=typeof t.matchesSelector?t.matchesSelector(e):"undefined"!=typeof t.webkitMatchesSelector?t.webkitMatchesSelector(e):"undefined"!=typeof t.mozMatchesSelector?t.mozMatchesSelector(e):"undefined"!=typeof t.msMatchesSelector?t.msMatchesSelector(e):void 0},l.remove=function(t){"undefined"!=typeof t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)},l.queryChildren=function(t,e){return Array.prototype.filter.call(t.childNodes,function(t){return l.matches(t,e)})},e.exports=l},{}],4:[function(t,e,n){"use strict";var r=function(t){this.element=t,this.events={}};r.prototype.bind=function(t,e){"undefined"==typeof this.events[t]&&(this.events[t]=[]),this.events[t].push(e),this.element.addEventListener(t,e,!1)},r.prototype.unbind=function(t,e){var n="undefined"!=typeof e;this.events[t]=this.events[t].filter(function(r){return n&&r!==e?!0:(this.element.removeEventListener(t,r,!1),!1)},this)},r.prototype.unbindAll=function(){for(var t in this.events)this.unbind(t)};var o=function(){this.eventElements=[]};o.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return"undefined"==typeof e&&(e=new r(t),this.eventElements.push(e)),e},o.prototype.bind=function(t,e,n){this.eventElement(t).bind(e,n)},o.prototype.unbind=function(t,e,n){this.eventElement(t).unbind(e,n)},o.prototype.unbindAll=function(){for(var t=0;t<this.eventElements.length;t++)this.eventElements[t].unbindAll()},o.prototype.once=function(t,e,n){var r=this.eventElement(t),o=function(t){r.unbind(e,o),n(t)};r.bind(e,o)},e.exports=o},{}],5:[function(t,e,n){"use strict";e.exports=function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return function(){return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}}()},{}],6:[function(t,e,n){"use strict";var r=t("./class"),o=t("./dom");n.toInt=function(t){return parseInt(t,10)||0},n.clone=function(t){if(null===t)return null;if("object"==typeof t){var e={};for(var n in t)e[n]=this.clone(t[n]);return e}return t},n.extend=function(t,e){var n=this.clone(t);for(var r in e)n[r]=this.clone(e[r]);return n},n.isEditable=function(t){return o.matches(t,"input,[contenteditable]")||o.matches(t,"select,[contenteditable]")||o.matches(t,"textarea,[contenteditable]")||o.matches(t,"button,[contenteditable]")},n.removePsClasses=function(t){for(var e=r.list(t),n=0;n<e.length;n++){var o=e[n];0===o.indexOf("ps-")&&r.remove(t,o)}},n.outerWidth=function(t){return this.toInt(o.css(t,"width"))+this.toInt(o.css(t,"paddingLeft"))+this.toInt(o.css(t,"paddingRight"))+this.toInt(o.css(t,"borderLeftWidth"))+this.toInt(o.css(t,"borderRightWidth"))},n.startScrolling=function(t,e){r.add(t,"ps-in-scrolling"),"undefined"!=typeof e?r.add(t,"ps-"+e):(r.add(t,"ps-x"),r.add(t,"ps-y"))},n.stopScrolling=function(t,e){r.remove(t,"ps-in-scrolling"),"undefined"!=typeof e?r.remove(t,"ps-"+e):(r.remove(t,"ps-x"),r.remove(t,"ps-y"))},n.env={isWebKit:"WebkitAppearance"in document.documentElement.style,supportsTouch:"ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,supportsIePointer:null!==window.navigator.msMaxTouchPoints}},{"./class":2,"./dom":3}],7:[function(t,e,n){"use strict";var r=t("./plugin/destroy"),o=t("./plugin/initialize"),i=t("./plugin/update");e.exports={initialize:o,update:i,destroy:r}},{"./plugin/destroy":9,"./plugin/initialize":17,"./plugin/update":20}],8:[function(t,e,n){"use strict";e.exports={wheelSpeed:1,wheelPropagation:!1,swipePropagation:!0,minScrollbarLength:null,maxScrollbarLength:null,useBothWheelAxes:!1,useKeyboard:!0,suppressScrollX:!1,suppressScrollY:!1,scrollXMarginOffset:0,scrollYMarginOffset:0,stopPropagationOnClick:!0}},{}],9:[function(t,e,n){"use strict";var r=t("../lib/dom"),o=t("../lib/helper"),i=t("./instances");e.exports=function(t){var e=i.get(t);e&&(e.event.unbindAll(),r.remove(e.scrollbarX),r.remove(e.scrollbarY),r.remove(e.scrollbarXRail),r.remove(e.scrollbarYRail),o.removePsClasses(t),i.remove(t))}},{"../lib/dom":3,"../lib/helper":6,"./instances":18}],10:[function(t,e,n){"use strict";function r(t,e){function n(t){return t.getBoundingClientRect()}e.settings.stopPropagationOnClick&&e.event.bind(e.scrollbarY,"click",function(t){t.stopPropagation()}),e.event.bind(e.scrollbarYRail,"click",function(r){var i=o.toInt(e.scrollbarYHeight/2),s=e.railYRatio*(r.pageY-window.scrollY-n(e.scrollbarYRail).top-i),a=e.railYRatio*(e.railYHeight-e.scrollbarYHeight),c=s/a;0>c?c=0:c>1&&(c=1),t.scrollTop=(e.contentHeight-e.containerHeight)*c,l(t),r.stopPropagation()}),e.settings.stopPropagationOnClick&&e.event.bind(e.scrollbarY,"click",function(t){t.stopPropagation()}),e.event.bind(e.scrollbarXRail,"click",function(r){var i=o.toInt(e.scrollbarXWidth/2),s=e.railXRatio*(r.pageX-window.scrollX-n(e.scrollbarXRail).left-i),a=e.railXRatio*(e.railXWidth-e.scrollbarXWidth),c=s/a;0>c?c=0:c>1&&(c=1),t.scrollLeft=(e.contentWidth-e.containerWidth)*c-e.negativeScrollAdjustment,l(t),r.stopPropagation()})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],11:[function(t,e,n){"use strict";function r(t,e){function n(n){var o=r+n*e.railXRatio,i=e.scrollbarXRail.getBoundingClientRect().left+e.railXRatio*(e.railXWidth-e.scrollbarXWidth);0>o?e.scrollbarXLeft=0:o>i?e.scrollbarXLeft=i:e.scrollbarXLeft=o;var s=l.toInt(e.scrollbarXLeft*(e.contentWidth-e.containerWidth)/(e.containerWidth-e.railXRatio*e.scrollbarXWidth))-e.negativeScrollAdjustment;t.scrollLeft=s}var r=null,o=null,s=function(e){n(e.pageX-o),a(t),e.stopPropagation(),e.preventDefault()},c=function(){l.stopScrolling(t,"x"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarX,"mousedown",function(n){o=n.pageX,r=l.toInt(i.css(e.scrollbarX,"left"))*e.railXRatio,l.startScrolling(t,"x"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",c),n.stopPropagation(),n.preventDefault()})}function o(t,e){function n(n){var o=r+n*e.railYRatio,i=e.scrollbarYRail.getBoundingClientRect().top+e.railYRatio*(e.railYHeight-e.scrollbarYHeight);0>o?e.scrollbarYTop=0:o>i?e.scrollbarYTop=i:e.scrollbarYTop=o;var s=l.toInt(e.scrollbarYTop*(e.contentHeight-e.containerHeight)/(e.containerHeight-e.railYRatio*e.scrollbarYHeight));t.scrollTop=s}var r=null,o=null,s=function(e){n(e.pageY-o),a(t),e.stopPropagation(),e.preventDefault()},c=function(){l.stopScrolling(t,"y"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarY,"mousedown",function(n){o=n.pageY,r=l.toInt(i.css(e.scrollbarY,"top"))*e.railYRatio,l.startScrolling(t,"y"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",c),n.stopPropagation(),n.preventDefault()})}var i=t("../../lib/dom"),l=t("../../lib/helper"),s=t("../instances"),a=t("../update-geometry");e.exports=function(t){var e=s.get(t);r(t,e),o(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19}],12:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&0>r)return!e.settings.wheelPropagation}var i=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===i&&0>n||i>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}var r=!1;e.event.bind(t,"mouseenter",function(){r=!0}),e.event.bind(t,"mouseleave",function(){r=!1});var i=!1;e.event.bind(e.ownerDocument,"keydown",function(s){if((!s.isDefaultPrevented||!s.isDefaultPrevented())&&r){var a=document.activeElement?document.activeElement:e.ownerDocument.activeElement;if(a){for(;a.shadowRoot;)a=a.shadowRoot.activeElement;if(o.isEditable(a))return}var c=0,u=0;switch(s.which){case 37:c=-30;break;case 38:u=30;break;case 39:c=30;break;case 40:u=-30;break;case 33:u=90;break;case 32:u=s.shiftKey?90:-90;break;case 34:u=-90;break;case 35:u=s.ctrlKey?-e.contentHeight:-e.containerHeight;break;case 36:u=s.ctrlKey?t.scrollTop:e.containerHeight;break;default:return}t.scrollTop=t.scrollTop-u,t.scrollLeft=t.scrollLeft+c,l(t),i=n(c,u),i&&s.preventDefault()}})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],13:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&0>r)return!e.settings.wheelPropagation}var i=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===i&&0>n||i>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}function r(t){var e=t.deltaX,n=-1*t.deltaY;return("undefined"==typeof e||"undefined"==typeof n)&&(e=-1*t.wheelDeltaX/6,n=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,n*=10),e!==e&&n!==n&&(e=0,n=t.wheelDelta),[e,n]}function i(e,n){var r=t.querySelector("textarea:hover");if(r){var o=r.scrollHeight-r.clientHeight;if(o>0&&!(0===r.scrollTop&&n>0||r.scrollTop===o&&0>n))return!0;var i=r.scrollLeft-r.clientWidth;if(i>0&&!(0===r.scrollLeft&&0>e||r.scrollLeft===i&&e>0))return!0}return!1}function s(s){if(o.env.isWebKit||!t.querySelector("select:focus")){var c=r(s),u=c[0],d=c[1];i(u,d)||(a=!1,e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(d?t.scrollTop=t.scrollTop-d*e.settings.wheelSpeed:t.scrollTop=t.scrollTop+u*e.settings.wheelSpeed,a=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(u?t.scrollLeft=t.scrollLeft+u*e.settings.wheelSpeed:t.scrollLeft=t.scrollLeft-d*e.settings.wheelSpeed,a=!0):(t.scrollTop=t.scrollTop-d*e.settings.wheelSpeed,t.scrollLeft=t.scrollLeft+u*e.settings.wheelSpeed),l(t),a=a||n(u,d),a&&(s.stopPropagation(),s.preventDefault()))}}var a=!1;"undefined"!=typeof window.onwheel?e.event.bind(t,"wheel",s):"undefined"!=typeof window.onmousewheel&&e.event.bind(t,"mousewheel",s)}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],14:[function(t,e,n){"use strict";function r(t,e){e.event.bind(t,"scroll",function(){i(t)})}var o=t("../instances"),i=t("../update-geometry");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19}],15:[function(t,e,n){"use strict";function r(t,e){function n(){var t=window.getSelection?window.getSelection():document.getSelection?document.getSelection():"";return 0===t.toString().length?null:t.getRangeAt(0).commonAncestorContainer}function r(){a||(a=setInterval(function(){return i.get(t)?(t.scrollTop=t.scrollTop+c.top,t.scrollLeft=t.scrollLeft+c.left,void l(t)):void clearInterval(a)},50))}function s(){a&&(clearInterval(a),a=null),o.stopScrolling(t)}var a=null,c={top:0,left:0},u=!1;e.event.bind(e.ownerDocument,"selectionchange",function(){t.contains(n())?u=!0:(u=!1,s())}),e.event.bind(window,"mouseup",function(){u&&(u=!1,s())}),e.event.bind(window,"mousemove",function(e){if(u){var n={x:e.pageX,y:e.pageY},i={left:t.offsetLeft,right:t.offsetLeft+t.offsetWidth,top:t.offsetTop,bottom:t.offsetTop+t.offsetHeight};n.x<i.left+3?(c.left=-5,o.startScrolling(t,"x")):n.x>i.right-3?(c.left=5,o.startScrolling(t,"x")):c.left=0,n.y<i.top+3?(i.top+3-n.y<5?c.top=-5:c.top=-20,o.startScrolling(t,"y")):n.y>i.bottom-3?(n.y-i.bottom+3<5?c.top=5:c.top=20,o.startScrolling(t,"y")):c.top=0,0===c.top&&0===c.left?s():r()}})}var o=t("../../lib/helper"),i=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19}],16:[function(t,e,n){"use strict";function r(t,e,n,r){function l(n,r){var o=t.scrollTop,i=t.scrollLeft,l=Math.abs(n),s=Math.abs(r);if(s>l){if(0>r&&o===e.contentHeight-e.containerHeight||r>0&&0===o)return!e.settings.swipePropagation}else if(l>s&&(0>n&&i===e.contentWidth-e.containerWidth||n>0&&0===i))return!e.settings.swipePropagation;return!0}function s(e,n){t.scrollTop=t.scrollTop-n,t.scrollLeft=t.scrollLeft-e,i(t)}function a(){y=!0}function c(){y=!1}function u(t){return t.targetTouches?t.targetTouches[0]:t}function d(t){return t.targetTouches&&1===t.targetTouches.length?!0:t.pointerType&&"mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE?!0:!1}function p(t){if(d(t)){Y=!0;var e=u(t);b.pageX=e.pageX,b.pageY=e.pageY,g=(new Date).getTime(),null!==m&&clearInterval(m),t.stopPropagation()}}function f(t){if(!y&&Y&&d(t)){var e=u(t),n={pageX:e.pageX,pageY:e.pageY},r=n.pageX-b.pageX,o=n.pageY-b.pageY;s(r,o),b=n;var i=(new Date).getTime(),a=i-g;a>0&&(v.x=r/a,v.y=o/a,g=i),l(r,o)&&(t.stopPropagation(),t.preventDefault())}}function h(){!y&&Y&&(Y=!1,clearInterval(m),m=setInterval(function(){return o.get(t)?Math.abs(v.x)<.01&&Math.abs(v.y)<.01?void clearInterval(m):(s(30*v.x,30*v.y),v.x*=.8,void(v.y*=.8)):void clearInterval(m)},10))}var b={},g=0,v={},m=null,y=!1,Y=!1;n&&(e.event.bind(window,"touchstart",a),e.event.bind(window,"touchend",c),e.event.bind(t,"touchstart",p),e.event.bind(t,"touchmove",f),e.event.bind(t,"touchend",h)),r&&(window.PointerEvent?(e.event.bind(window,"pointerdown",a),e.event.bind(window,"pointerup",c),e.event.bind(t,"pointerdown",p),e.event.bind(t,"pointermove",f),e.event.bind(t,"pointerup",h)):window.MSPointerEvent&&(e.event.bind(window,"MSPointerDown",a),e.event.bind(window,"MSPointerUp",c),e.event.bind(t,"MSPointerDown",p),e.event.bind(t,"MSPointerMove",f),e.event.bind(t,"MSPointerUp",h)))}var o=t("../instances"),i=t("../update-geometry");e.exports=function(t,e,n){var i=o.get(t);r(t,i,e,n)}},{"../instances":18,"../update-geometry":19}],17:[function(t,e,n){"use strict";var r=t("../lib/class"),o=t("../lib/helper"),i=t("./instances"),l=t("./update-geometry"),s=t("./handler/click-rail"),a=t("./handler/drag-scrollbar"),c=t("./handler/keyboard"),u=t("./handler/mouse-wheel"),d=t("./handler/native-scroll"),p=t("./handler/selection"),f=t("./handler/touch");e.exports=function(t,e){e="object"==typeof e?e:{},r.add(t,"ps-container");var n=i.add(t);n.settings=o.extend(n.settings,e),s(t),a(t),u(t),d(t),p(t),(o.env.supportsTouch||o.env.supportsIePointer)&&f(t,o.env.supportsTouch,o.env.supportsIePointer),n.settings.useKeyboard&&c(t),l(t)}},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(t,e,n){"use strict";function r(t){var e=this;e.settings=d.clone(a),e.containerWidth=null,e.containerHeight=null,e.contentWidth=null,e.contentHeight=null,e.isRtl="rtl"===s.css(t,"direction"),e.isNegativeScroll=function(){var e=t.scrollLeft,n=null;return t.scrollLeft=-1,n=t.scrollLeft<0,t.scrollLeft=e,n}(),e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,e.event=new c,e.ownerDocument=t.ownerDocument||document,e.scrollbarXRail=s.appendTo(s.e("div","ps-scrollbar-x-rail"),t),e.scrollbarX=s.appendTo(s.e("div","ps-scrollbar-x"),e.scrollbarXRail),e.scrollbarXActive=null,e.scrollbarXWidth=null,e.scrollbarXLeft=null,e.scrollbarXBottom=d.toInt(s.css(e.scrollbarXRail,"bottom")),e.isScrollbarXUsingBottom=e.scrollbarXBottom===e.scrollbarXBottom,e.scrollbarXTop=e.isScrollbarXUsingBottom?null:d.toInt(s.css(e.scrollbarXRail,"top")),e.railBorderXWidth=d.toInt(s.css(e.scrollbarXRail,"borderLeftWidth"))+d.toInt(s.css(e.scrollbarXRail,"borderRightWidth")),s.css(e.scrollbarXRail,"display","block"),e.railXMarginWidth=d.toInt(s.css(e.scrollbarXRail,"marginLeft"))+d.toInt(s.css(e.scrollbarXRail,"marginRight")),s.css(e.scrollbarXRail,"display",""),e.railXWidth=null,e.railXRatio=null,e.scrollbarYRail=s.appendTo(s.e("div","ps-scrollbar-y-rail"),t),e.scrollbarY=s.appendTo(s.e("div","ps-scrollbar-y"),e.scrollbarYRail),e.scrollbarYActive=null,e.scrollbarYHeight=null,e.scrollbarYTop=null,e.scrollbarYRight=d.toInt(s.css(e.scrollbarYRail,"right")),e.isScrollbarYUsingRight=e.scrollbarYRight===e.scrollbarYRight,e.scrollbarYLeft=e.isScrollbarYUsingRight?null:d.toInt(s.css(e.scrollbarYRail,"left")),e.scrollbarYOuterWidth=e.isRtl?d.outerWidth(e.scrollbarY):null,e.railBorderYWidth=d.toInt(s.css(e.scrollbarYRail,"borderTopWidth"))+d.toInt(s.css(e.scrollbarYRail,"borderBottomWidth")),s.css(e.scrollbarYRail,"display","block"),e.railYMarginHeight=d.toInt(s.css(e.scrollbarYRail,"marginTop"))+d.toInt(s.css(e.scrollbarYRail,"marginBottom")),s.css(e.scrollbarYRail,"display",""),e.railYHeight=null,e.railYRatio=null}function o(t){return"undefined"==typeof t.dataset?t.getAttribute("data-ps-id"):t.dataset.psId}function i(t,e){"undefined"==typeof t.dataset?t.setAttribute("data-ps-id",e):t.dataset.psId=e}function l(t){"undefined"==typeof t.dataset?t.removeAttribute("data-ps-id"):delete t.dataset.psId}var s=t("../lib/dom"),a=t("./default-setting"),c=t("../lib/event-manager"),u=t("../lib/guid"),d=t("../lib/helper"),p={};n.add=function(t){var e=u();return i(t,e),p[e]=new r(t),p[e]},n.remove=function(t){delete p[o(t)],l(t)},n.get=function(t){return p[o(t)]}},{"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(t,e,n){"use strict";function r(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function o(t,e){var n={width:e.railXWidth};e.isRtl?n.left=e.negativeScrollAdjustment+t.scrollLeft+e.containerWidth-e.contentWidth:n.left=t.scrollLeft,e.isScrollbarXUsingBottom?n.bottom=e.scrollbarXBottom-t.scrollTop:n.top=e.scrollbarXTop+t.scrollTop,l.css(e.scrollbarXRail,n);var r={top:t.scrollTop,height:e.railYHeight};e.isScrollbarYUsingRight?e.isRtl?r.right=e.contentWidth-(e.negativeScrollAdjustment+t.scrollLeft)-e.scrollbarYRight-e.scrollbarYOuterWidth:r.right=e.scrollbarYRight-t.scrollLeft:e.isRtl?r.left=e.negativeScrollAdjustment+t.scrollLeft+2*e.containerWidth-e.contentWidth-e.scrollbarYLeft-e.scrollbarYOuterWidth:r.left=e.scrollbarYLeft+t.scrollLeft,l.css(e.scrollbarYRail,r),l.css(e.scrollbarX,{left:e.scrollbarXLeft,width:e.scrollbarXWidth-e.railBorderXWidth}),l.css(e.scrollbarY,{top:e.scrollbarYTop,height:e.scrollbarYHeight-e.railBorderYWidth})}var i=t("../lib/class"),l=t("../lib/dom"),s=t("../lib/helper"),a=t("./instances");e.exports=function(t){var e=a.get(t);e.containerWidth=t.clientWidth,e.containerHeight=t.clientHeight,e.contentWidth=t.scrollWidth,e.contentHeight=t.scrollHeight;var n;t.contains(e.scrollbarXRail)||(n=l.queryChildren(t,".ps-scrollbar-x-rail"),n.length>0&&n.forEach(function(t){l.remove(t)}),l.appendTo(e.scrollbarXRail,t)),t.contains(e.scrollbarYRail)||(n=l.queryChildren(t,".ps-scrollbar-y-rail"),n.length>0&&n.forEach(function(t){l.remove(t)}),l.appendTo(e.scrollbarYRail,t)),!e.settings.suppressScrollX&&e.containerWidth+e.settings.scrollXMarginOffset<e.contentWidth?(e.scrollbarXActive=!0,e.railXWidth=e.containerWidth-e.railXMarginWidth,e.railXRatio=e.containerWidth/e.railXWidth,e.scrollbarXWidth=r(e,s.toInt(e.railXWidth*e.containerWidth/e.contentWidth)),e.scrollbarXLeft=s.toInt((e.negativeScrollAdjustment+t.scrollLeft)*(e.railXWidth-e.scrollbarXWidth)/(e.contentWidth-e.containerWidth))):(e.scrollbarXActive=!1,e.scrollbarXWidth=0,e.scrollbarXLeft=0,t.scrollLeft=0),!e.settings.suppressScrollY&&e.containerHeight+e.settings.scrollYMarginOffset<e.contentHeight?(e.scrollbarYActive=!0,e.railYHeight=e.containerHeight-e.railYMarginHeight,e.railYRatio=e.containerHeight/e.railYHeight,e.scrollbarYHeight=r(e,s.toInt(e.railYHeight*e.containerHeight/e.contentHeight)),e.scrollbarYTop=s.toInt(t.scrollTop*(e.railYHeight-e.scrollbarYHeight)/(e.contentHeight-e.containerHeight))):(e.scrollbarYActive=!1,e.scrollbarYHeight=0,e.scrollbarYTop=0,t.scrollTop=0),e.scrollbarXLeft>=e.railXWidth-e.scrollbarXWidth&&(e.scrollbarXLeft=e.railXWidth-e.scrollbarXWidth),e.scrollbarYTop>=e.railYHeight-e.scrollbarYHeight&&(e.scrollbarYTop=e.railYHeight-e.scrollbarYHeight),o(t,e),i[e.scrollbarXActive?"add":"remove"](t,"ps-active-x"),i[e.scrollbarYActive?"add":"remove"](t,"ps-active-y")}},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18}],20:[function(t,e,n){"use strict";var r=t("../lib/dom"),o=t("../lib/helper"),i=t("./instances"),l=t("./update-geometry");e.exports=function(t){var e=i.get(t);e&&(e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,r.css(e.scrollbarXRail,"display","block"),r.css(e.scrollbarYRail,"display","block"),e.railXMarginWidth=o.toInt(r.css(e.scrollbarXRail,"marginLeft"))+o.toInt(r.css(e.scrollbarXRail,"marginRight")),e.railYMarginHeight=o.toInt(r.css(e.scrollbarYRail,"marginTop"))+o.toInt(r.css(e.scrollbarYRail,"marginBottom")),r.css(e.scrollbarXRail,"display","none"),r.css(e.scrollbarYRail,"display","none"),l(t),r.css(e.scrollbarXRail,"display",""),r.css(e.scrollbarYRail,"display",""))}},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19}]},{},[1]); \ No newline at end of file
diff --git a/web/static/js/react-bootstrap-0.25.1.min.js b/web/static/js/react-bootstrap-0.25.1.min.js
index 06f9677d3..7c8c52a79 100644
--- a/web/static/js/react-bootstrap-0.25.1.min.js
+++ b/web/static/js/react-bootstrap-0.25.1.min.js
@@ -11,4 +11,3 @@ for(o=97;123>o;o++)n[String.fromCharCode(o)]=o-32;for(var o=48;58>o;o++)n[o-48]=
"use strict";var r=n(7)["default"],o=n(6)["default"],s=n(2)["default"],a=n(72)["default"];t.__esModule=!0;var i=n(1),l=s(i),u=n(34),p=s(u),d=n(54),f=a(d),c=n(12),h=s(c),m=function(e){function t(){o(this,t),e.apply(this,arguments)}return r(t,e),t.prototype.render=function(){return"static"===this.props.type?(h["default"]("Input type=static","StaticText"),l["default"].createElement(f.Static,this.props)):e.prototype.render.call(this)},t}(p["default"]);m.propTypes={type:l["default"].PropTypes.string},t["default"]=m,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(8),p=o(u),d=a["default"].createClass({displayName:"Jumbotron",propTypes:{componentClass:p["default"].elementType},getDefaultProps:function(){return{componentClass:"div"}},render:function(){var e=this.props.componentClass;return a["default"].createElement(e,r({},this.props,{className:l["default"](this.props.className,"jumbotron")}),this.props.children)}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=a["default"].createClass({displayName:"Label",mixins:[p["default"]],getDefaultProps:function(){return{bsClass:"label",bsStyle:"default"}},render:function(){var e=this.getBsClassSet();return a["default"].createElement("span",r({},this.props,{className:l["default"](this.props.className,e)}),this.props.children)}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(7)["default"],o=n(6)["default"],s=n(3)["default"],a=n(2)["default"];t.__esModule=!0;var i=n(1),l=a(i),u=n(4),p=a(u),d=n(9),f=a(d),c=function(e){function t(){o(this,t),e.apply(this,arguments)}return r(t,e),t.prototype.render=function(){var e=this,t=f["default"].map(this.props.children,function(e,t){return i.cloneElement(e,{key:e.key?e.key:t})}),n=!1;return this.props.children?l["default"].Children.forEach(this.props.children,function(t){e.isAnchorOrButton(t.props)&&(n=!0)}):n=!0,n?this.renderDiv(t):this.renderUL(t)},t.prototype.isAnchorOrButton=function(e){return e.href||e.onClick},t.prototype.renderUL=function(e){var t=f["default"].map(e,function(e,t){return i.cloneElement(e,{listItem:!0})});return l["default"].createElement("ul",s({},this.props,{className:p["default"](this.props.className,"list-group")}),t)},t.prototype.renderDiv=function(e){return l["default"].createElement("div",s({},this.props,{className:p["default"](this.props.className,"list-group")}),e)},t}(l["default"].Component);c.propTypes={className:l["default"].PropTypes.string,id:l["default"].PropTypes.oneOfType([l["default"].PropTypes.string,l["default"].PropTypes.number])},t["default"]=c,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(5),l=o(i),u=n(4),p=o(u),d=a["default"].createClass({displayName:"ListGroupItem",mixins:[l["default"]],propTypes:{bsStyle:a["default"].PropTypes.oneOf(["danger","info","success","warning"]),className:a["default"].PropTypes.string,active:a["default"].PropTypes.any,disabled:a["default"].PropTypes.any,header:a["default"].PropTypes.node,listItem:a["default"].PropTypes.bool,onClick:a["default"].PropTypes.func,eventKey:a["default"].PropTypes.any,href:a["default"].PropTypes.string,target:a["default"].PropTypes.string},getDefaultProps:function(){return{bsClass:"list-group-item",listItem:!1}},render:function(){var e=this.getBsClassSet();return e.active=this.props.active,e.disabled=this.props.disabled,this.props.href?this.renderAnchor(e):this.props.onClick?this.renderButton(e):this.props.listItem?this.renderLi(e):this.renderSpan(e)},renderLi:function(e){return a["default"].createElement("li",r({},this.props,{className:p["default"](this.props.className,e)}),this.props.header?this.renderStructuredContent():this.props.children)},renderAnchor:function(e){return a["default"].createElement("a",r({},this.props,{className:p["default"](this.props.className,e)}),this.props.header?this.renderStructuredContent():this.props.children)},renderButton:function(e){return a["default"].createElement("button",r({type:"button"},this.props,{className:p["default"](this.props.className,e)}),this.props.children)},renderSpan:function(e){return a["default"].createElement("span",r({},this.props,{className:p["default"](this.props.className,e)}),this.props.header?this.renderStructuredContent():this.props.children)},renderStructuredContent:function(){var e=void 0;e=a["default"].isValidElement(this.props.header)?s.cloneElement(this.props.header,{key:"header",className:p["default"](this.props.header.props.className,"list-group-item-heading")}):a["default"].createElement("h4",{key:"header",className:"list-group-item-heading"},this.props.header);var t=a["default"].createElement("p",{key:"content",className:"list-group-item-text"},this.props.children);return[e,t]}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(7)["default"],o=n(6)["default"],s=n(2)["default"];t.__esModule=!0;var a=n(1),i=s(a),l=n(4),u=s(l),p=n(8),d=s(p),f=n(14),c=s(f),h=function(e){function t(n){o(this,t),e.call(this,n),this.handleClick=this.handleClick.bind(this)}return r(t,e),t.prototype.handleClick=function(e){(!this.props.href||this.props.disabled)&&e.preventDefault(),this.props.disabled||this.props.onSelect&&this.props.onSelect(e,this.props.eventKey)},t.prototype.render=function(){if(this.props.divider)return i["default"].createElement("li",{role:"separator",className:"divider"});if(this.props.header)return i["default"].createElement("li",{role:"heading",className:"dropdown-header"},this.props.children);var e={disabled:this.props.disabled};return i["default"].createElement("li",{role:"presentation",className:u["default"](this.props.className,e),style:this.props.style},i["default"].createElement(c["default"],{role:"menuitem",tabIndex:"-1",id:this.props.id,target:this.props.target,title:this.props.title,href:this.props.href||"",onKeyDown:this.props.onKeyDown,onClick:this.handleClick},this.props.children))},t}(i["default"].Component);t["default"]=h,h.propTypes={disabled:i["default"].PropTypes.bool,divider:d["default"].all([i["default"].PropTypes.bool,function(e,t,n){return e.divider&&e.children?new Error("Children will not be rendered for dividers"):void 0}]),eventKey:i["default"].PropTypes.oneOfType([i["default"].PropTypes.number,i["default"].PropTypes.string]),header:i["default"].PropTypes.bool,href:i["default"].PropTypes.string,target:i["default"].PropTypes.string,title:i["default"].PropTypes.string,onKeyDown:i["default"].PropTypes.func,onSelect:i["default"].PropTypes.func,id:i["default"].PropTypes.oneOfType([i["default"].PropTypes.string,i["default"].PropTypes.number])},h.defaultProps={divider:!1,disabled:!1,header:!1},e.exports=t["default"]},function(e,t,n){"use strict";function r(e,t){var n=v["default"].ownerDocument(t);return e===n.body||e===n.documentElement?n.documentElement.clientHeight:e.clientHeight}function o(e){return e.props.container&&f["default"].findDOMNode(e.props.container)||v["default"].ownerDocument(e).body}function s(e,t){var n=v["default"].ownerDocument(e),r=!n.addEventListener,o=void 0;return B&&B.remove(),r?(document.attachEvent("onfocusin",t),o=function(){return document.detachEvent("onfocusin",t)}):(document.addEventListener("focus",t,!0),o=function(){return document.removeEventListener("focus",t,!0)}),B={remove:o}}var a=n(3)["default"],i=n(10)["default"],l=n(150)["default"],u=n(17)["default"],p=n(2)["default"];t.__esModule=!0;var d=n(1),f=p(d),c=n(4),h=p(c),m=n(23),v=p(m),y=n(183),g=p(y),b=n(71),T=p(b),P=n(11),x=p(P),E=n(8),C=p(E),_=n(47),N=p(_),O=n(32),w=p(O),S=n(123),k=p(S),M=n(58),D=p(M),A=n(60),I=p(A),R=n(61),L=p(R),j=n(59),K=p(j),B=void 0,F=f["default"].createClass({displayName:"Modal",propTypes:a({},N["default"].propTypes,k["default"].propTypes,{backdrop:f["default"].PropTypes.oneOf(["static",!0,!1]),keyboard:f["default"].PropTypes.bool,animation:f["default"].PropTypes.bool,dialogComponent:C["default"].elementType,autoFocus:f["default"].PropTypes.bool,enforceFocus:f["default"].PropTypes.bool,bsStyle:f["default"].PropTypes.string,show:f["default"].PropTypes.bool}),getDefaultProps:function(){return{bsClass:"modal",dialogComponent:k["default"],show:!1,animation:!0,backdrop:!0,keyboard:!0,autoFocus:!0,enforceFocus:!0}},getInitialState:function(){return{exited:!this.props.show}},render:function(){var e=this.props,t=(e.children,e.animation),n=e.backdrop,r=i(e,["children","animation","backdrop"]),o=r.onExit,s=r.onExiting,l=r.onEnter,u=r.onEntering,p=r.onEntered,d=!!r.show,c=r.dialogComponent,m=d||t&&!this.state.exited;if(!m)return null;var v=f["default"].createElement(c,a({},r,{ref:this._setDialogRef,className:h["default"](this.props.className,{"in":d&&!t}),onClick:n===!0?this.handleBackdropClick:null}),this.renderContent());return t&&(v=f["default"].createElement(w["default"],{transitionAppear:!0,unmountOnExit:!0,"in":d,timeout:F.TRANSITION_DURATION,onExit:o,onExiting:s,onExited:this.handleHidden,onEnter:l,onEntering:u,onEntered:p},v)),n&&(v=this.renderBackdrop(v)),f["default"].createElement(N["default"],{container:r.container},v)},renderContent:function(){var e=this;return f["default"].Children.map(this.props.children,function(t){return t&&t.type&&t.type.__isModalHeader?d.cloneElement(t,{onHide:x["default"](e.props.onHide,t.props.onHide)}):t})},renderBackdrop:function(e){var t=this.props,n=t.animation,r=t.bsClass,o=F.BACKDROP_TRANSITION_DURATION,s=this.props.backdrop===!0?this.handleBackdropClick:null,a=f["default"].createElement("div",{ref:"backdrop",className:h["default"](r+"-backdrop",{"in":this.props.show&&!n}),onClick:s});return f["default"].createElement("div",{ref:"modal"},n?f["default"].createElement(w["default"],{transitionAppear:!0,"in":this.props.show,timeout:o},a):a,e)},_setDialogRef:function(e){l(this.refs)&&!u(this.refs).length&&(this.refs={}),this.refs.dialog=e,this.props.backdrop||(this.refs.modal=e)},componentWillReceiveProps:function(e){e.show?this.setState({exited:!1}):e.animation||this.setState({exited:!0})},componentWillUpdate:function(e){e.show&&this.checkForFocus()},componentDidMount:function(){this.props.show&&this.onShow()},componentDidUpdate:function(e){var t=this.props.animation;!e.show||this.props.show||t?!e.show&&this.props.show&&this.onShow():this.onHide()},componentWillUnmount:function(){this.props.show&&this.onHide()},onShow:function(){var e=this,t=v["default"].ownerDocument(this),n=v["default"].ownerWindow(this);this._onDocumentKeyupListener=T["default"].listen(t,"keyup",this.handleDocumentKeyUp),this._onWindowResizeListener=T["default"].listen(n,"resize",this.handleWindowResize),this.props.enforceFocus&&(this._onFocusinListener=s(this,this.enforceFocus));var a=o(this);a.className+=a.className.length?" modal-open":"modal-open",this._containerIsOverflowing=a.scrollHeight>r(a,this),this._originalPadding=a.style.paddingRight,this._containerIsOverflowing&&(a.style.paddingRight=parseInt(this._originalPadding||0,10)+g["default"]()+"px"),this.props.backdrop&&this.iosClickHack(),this.setState(this._getStyles(),function(){return e.focusModalContent()})},onHide:function(){this._onDocumentKeyupListener.remove(),this._onWindowResizeListener.remove(),this._onFocusinListener&&this._onFocusinListener.remove();var e=o(this);e.style.paddingRight=this._originalPadding,e.className=e.className.replace(/ ?modal-open/,""),this.restoreLastFocus()},handleHidden:function(){if(this.setState({exited:!0}),this.onHide(),this.props.onExited){var e;(e=this.props).onExited.apply(e,arguments)}},handleBackdropClick:function(e){e.target===e.currentTarget&&this.props.onHide()},handleDocumentKeyUp:function(e){this.props.keyboard&&27===e.keyCode&&this.props.onHide()},handleWindowResize:function(){this.setState(this._getStyles())},checkForFocus:function(){v["default"].canUseDom&&(this.lastFocus=v["default"].activeElement(document))},focusModalContent:function(){var e=f["default"].findDOMNode(this.refs.dialog),t=v["default"].activeElement(v["default"].ownerDocument(this)),n=t&&v["default"].contains(e,t);e&&this.props.autoFocus&&!n&&(this.lastFocus=t,e.focus())},restoreLastFocus:function(){this.lastFocus&&this.lastFocus.focus&&(this.lastFocus.focus(),this.lastFocus=null)},enforceFocus:function(){if(this.isMounted()){var e=v["default"].activeElement(v["default"].ownerDocument(this)),t=f["default"].findDOMNode(this.refs.dialog);t&&t!==e&&!v["default"].contains(t,e)&&t.focus()}},iosClickHack:function(){f["default"].findDOMNode(this.refs.modal).onclick=function(){},f["default"].findDOMNode(this.refs.backdrop).onclick=function(){}},_getStyles:function(){if(!v["default"].canUseDom)return{};var e=f["default"].findDOMNode(this.refs.modal),t=e.scrollHeight,n=o(this),s=this._containerIsOverflowing,a=t>r(n,this);return{dialogStyles:{paddingRight:s&&!a?g["default"]():void 0,paddingLeft:!s&&a?g["default"]():void 0}}}});F.Body=D["default"],F.Header=I["default"],F.Title=L["default"],F.Footer=K["default"],F.Dialog=k["default"],F.TRANSITION_DURATION=300,F.BACKDROP_TRANSITION_DURATION=150,t["default"]=F,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=a["default"].createClass({displayName:"ModalDialog",mixins:[p["default"]],propTypes:{onHide:a["default"].PropTypes.func.isRequired,dialogClassName:a["default"].PropTypes.string},getDefaultProps:function(){return{bsClass:"modal",closeButton:!0}},render:function(){var e=r({display:"block"},this.props.style),t=this.props.bsClass,n=this.getBsClassSet();return delete n.modal,n[t+"-dialog"]=!0,a["default"].createElement("div",r({},this.props,{title:null,tabIndex:"-1",role:"dialog",style:e,className:l["default"](this.props.className,t)}),a["default"].createElement("div",{className:l["default"](this.props.dialogClassName,n)},a["default"].createElement("div",{className:t+"-content",role:"document"},this.props.children)))}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(5),l=o(i),u=n(4),p=o(u),d=n(9),f=o(d),c=n(11),h=o(c),m=n(8),v=o(m),y=a["default"].createClass({displayName:"Navbar",mixins:[l["default"]],propTypes:{fixedTop:a["default"].PropTypes.bool,fixedBottom:a["default"].PropTypes.bool,staticTop:a["default"].PropTypes.bool,inverse:a["default"].PropTypes.bool,fluid:a["default"].PropTypes.bool,role:a["default"].PropTypes.string,componentClass:v["default"].elementType,brand:a["default"].PropTypes.node,toggleButton:a["default"].PropTypes.node,toggleNavKey:a["default"].PropTypes.oneOfType([a["default"].PropTypes.string,a["default"].PropTypes.number]),onToggle:a["default"].PropTypes.func,navExpanded:a["default"].PropTypes.bool,defaultNavExpanded:a["default"].PropTypes.bool},getDefaultProps:function(){return{bsClass:"navbar",bsStyle:"default",role:"navigation",componentClass:"nav",fixedTop:!1,fixedBottom:!1,staticTop:!1,inverse:!1,fluid:!1,defaultNavExpanded:!1}},getInitialState:function(){return{navExpanded:this.props.defaultNavExpanded}},shouldComponentUpdate:function(){return!this._isChanging},handleToggle:function(){this.props.onToggle&&(this._isChanging=!0,this.props.onToggle(),this._isChanging=!1),this.setState({navExpanded:!this.state.navExpanded})},isNavExpanded:function(){return null!=this.props.navExpanded?this.props.navExpanded:this.state.navExpanded},render:function(){var e=this.getBsClassSet(),t=this.props.componentClass;return e["navbar-fixed-top"]=this.props.fixedTop,e["navbar-fixed-bottom"]=this.props.fixedBottom,e["navbar-static-top"]=this.props.staticTop,e["navbar-inverse"]=this.props.inverse,a["default"].createElement(t,r({},this.props,{className:p["default"](this.props.className,e)}),a["default"].createElement("div",{className:this.props.fluid?"container-fluid":"container"},this.props.brand||this.props.toggleButton||null!=this.props.toggleNavKey?this.renderHeader():null,f["default"].map(this.props.children,this.renderChild)))},renderChild:function(e,t){return s.cloneElement(e,{navbar:!0,collapsible:null!=this.props.toggleNavKey&&this.props.toggleNavKey===e.props.eventKey,expanded:null!=this.props.toggleNavKey&&this.props.toggleNavKey===e.props.eventKey&&this.isNavExpanded(),key:e.key?e.key:t})},renderHeader:function(){var e=void 0;return this.props.brand&&(e=a["default"].isValidElement(this.props.brand)?s.cloneElement(this.props.brand,{className:p["default"](this.props.brand.props.className,"navbar-brand")}):a["default"].createElement("span",{className:"navbar-brand"},this.props.brand)),a["default"].createElement("div",{className:"navbar-header"},e,this.props.toggleButton||null!=this.props.toggleNavKey?this.renderToggleButton():null)},renderToggleButton:function(){var e=void 0;return a["default"].isValidElement(this.props.toggleButton)?s.cloneElement(this.props.toggleButton,{className:p["default"](this.props.toggleButton.props.className,"navbar-toggle"),onClick:h["default"](this.handleToggle,this.props.toggleButton.props.onClick)}):(e=null!=this.props.toggleButton?this.props.toggleButton:[a["default"].createElement("span",{className:"sr-only",key:0},"Toggle navigation"),a["default"].createElement("span",{className:"icon-bar",key:1}),a["default"].createElement("span",{className:"icon-bar",key:2}),a["default"].createElement("span",{className:"icon-bar",key:3})],a["default"].createElement("button",{className:"navbar-toggle",type:"button",onClick:this.handleToggle},e))}});t["default"]=y,e.exports=t["default"]},function(e,t,n){"use strict";function r(e,t){return Array.isArray(t)?t.indexOf(e)>=0:e===t}var o=n(3)["default"],s=n(17)["default"],a=n(2)["default"];t.__esModule=!0;var i=n(1),l=a(i),u=n(11),p=a(u),d=n(145),f=a(d),c=n(65),h=a(c),m=n(48),v=a(m),y=n(221),g=a(y),b=l["default"].createClass({displayName:"OverlayTrigger",propTypes:o({},h["default"].propTypes,{trigger:l["default"].PropTypes.oneOfType([l["default"].PropTypes.oneOf(["click","hover","focus"]),l["default"].PropTypes.arrayOf(l["default"].PropTypes.oneOf(["click","hover","focus"]))]),delay:l["default"].PropTypes.number,delayShow:l["default"].PropTypes.number,delayHide:l["default"].PropTypes.number,defaultOverlayShown:l["default"].PropTypes.bool,overlay:l["default"].PropTypes.node.isRequired,onBlur:l["default"].PropTypes.func,onClick:l["default"].PropTypes.func,onFocus:l["default"].PropTypes.func,onMouseEnter:l["default"].PropTypes.func,onMouseLeave:l["default"].PropTypes.func,target:function(){},onHide:function(){},show:function(){}}),getDefaultProps:function(){return{defaultOverlayShown:!1,trigger:["hover","focus"]}},getInitialState:function(){return{isOverlayShown:this.props.defaultOverlayShown}},show:function(){this.setState({isOverlayShown:!0})},hide:function(){this.setState({isOverlayShown:!1})},toggle:function(){this.state.isOverlayShown?this.hide():this.show()},componentDidMount:function(){this._mountNode=document.createElement("div"),l["default"].render(this._overlay,this._mountNode)},componentWillUnmount:function(){l["default"].unmountComponentAtNode(this._mountNode),this._mountNode=null,clearTimeout(this._hoverDelay)},componentDidUpdate:function(){this._mountNode&&l["default"].render(this._overlay,this._mountNode)},getOverlayTarget:function(){return l["default"].findDOMNode(this)},getOverlay:function(){var e=o({},g["default"](this.props,s(h["default"].propTypes)),{show:this.state.isOverlayShown,onHide:this.hide,target:this.getOverlayTarget,onExit:this.props.onExit,onExiting:this.props.onExiting,onExited:this.props.onExited,onEnter:this.props.onEnter,onEntering:this.props.onEntering,onEntered:this.props.onEntered}),t=i.cloneElement(this.props.overlay,{placement:e.placement,container:e.container});return l["default"].createElement(h["default"],e,t)},render:function(){var e=l["default"].Children.only(this.props.children),t={"aria-describedby":this.props.overlay.props.id};return this._overlay=this.getOverlay(),t.onClick=p["default"](e.props.onClick,this.props.onClick),r("click",this.props.trigger)&&(t.onClick=p["default"](this.toggle,t.onClick)),r("hover",this.props.trigger)&&(v["default"](!("hover"===this.props.trigger),'[react-bootstrap] Specifying only the `"hover"` trigger limits the visibilty of the overlay to just mouse users. Consider also including the `"focus"` trigger so that touch and keyboard only users can see the overlay as well.'),t.onMouseOver=p["default"](this.handleDelayedShow,this.props.onMouseOver),t.onMouseOut=p["default"](this.handleDelayedHide,this.props.onMouseOut)),r("focus",this.props.trigger)&&(t.onFocus=p["default"](this.handleDelayedShow,this.props.onFocus),t.onBlur=p["default"](this.handleDelayedHide,this.props.onBlur)),i.cloneElement(e,t)},handleDelayedShow:function(){var e=this;if(null!=this._hoverDelay)return clearTimeout(this._hoverDelay),void(this._hoverDelay=null);var t=null!=this.props.delayShow?this.props.delayShow:this.props.delay;return t?void(this._hoverDelay=setTimeout(function(){e._hoverDelay=null,e.show()},t)):void this.show()},handleDelayedHide:function(){var e=this;if(null!=this._hoverDelay)return clearTimeout(this._hoverDelay),void(this._hoverDelay=null);var t=null!=this.props.delayHide?this.props.delayHide:this.props.delay;return t?void(this._hoverDelay=setTimeout(function(){e._hoverDelay=null,e.hide()},t)):void this.hide()}});b.withContext=f["default"](b,"overlay"),t["default"]=b,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=a["default"].createClass({displayName:"PageHeader",render:function(){return a["default"].createElement("div",r({},this.props,{className:l["default"](this.props.className,"page-header")}),a["default"].createElement("h1",null,this.props.children))}});t["default"]=u,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(14),p=o(u),d=a["default"].createClass({displayName:"PageItem",propTypes:{href:a["default"].PropTypes.string,target:a["default"].PropTypes.string,title:a["default"].PropTypes.string,disabled:a["default"].PropTypes.bool,previous:a["default"].PropTypes.bool,next:a["default"].PropTypes.bool,onSelect:a["default"].PropTypes.func,eventKey:a["default"].PropTypes.any},getDefaultProps:function(){return{disabled:!1,previous:!1,next:!1}},render:function(){var e={disabled:this.props.disabled,previous:this.props.previous,next:this.props.next};return a["default"].createElement("li",r({},this.props,{className:l["default"](this.props.className,e)}),a["default"].createElement(p["default"],{href:this.props.href,title:this.props.title,target:this.props.target,onClick:this.handleSelect},this.props.children))},handleSelect:function(e){(this.props.onSelect||this.props.disabled)&&(e.preventDefault(),this.props.disabled||this.props.onSelect(this.props.eventKey,this.props.href,this.props.target))}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(9),p=o(u),d=n(11),f=o(d),c=a["default"].createClass({displayName:"Pager",propTypes:{onSelect:a["default"].PropTypes.func},render:function(){return a["default"].createElement("ul",r({},this.props,{className:l["default"](this.props.className,"pager")}),p["default"].map(this.props.children,this.renderPageItem))},renderPageItem:function(e,t){return s.cloneElement(e,{onSelect:f["default"](e.props.onSelect,this.props.onSelect),key:e.key?e.key:t})}});t["default"]=c,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=n(130),f=o(d),c=n(8),h=o(c),m=n(14),v=o(m),y=a["default"].createClass({displayName:"Pagination",mixins:[p["default"]],propTypes:{activePage:a["default"].PropTypes.number,items:a["default"].PropTypes.number,maxButtons:a["default"].PropTypes.number,ellipsis:a["default"].PropTypes.bool,first:a["default"].PropTypes.bool,last:a["default"].PropTypes.bool,prev:a["default"].PropTypes.bool,next:a["default"].PropTypes.bool,onSelect:a["default"].PropTypes.func,buttonComponentClass:h["default"].elementType},getDefaultProps:function(){return{activePage:1,items:1,maxButtons:0,first:!1,last:!1,prev:!1,next:!1,ellipsis:!0,buttonComponentClass:v["default"],bsClass:"pagination"}},renderPageButtons:function(){var e=[],t=void 0,n=void 0,r=void 0,o=this.props,s=o.maxButtons,i=o.activePage,l=o.items,u=o.onSelect,p=o.ellipsis,d=o.buttonComponentClass;if(s){var c=i-parseInt(s/2,10);t=c>1?c:1,r=l>=t+s,r?n=t+s-1:(n=l,t=l-s+1,1>t&&(t=1))}else t=1,n=l;for(var h=t;n>=h;h++)e.push(a["default"].createElement(f["default"],{key:h,eventKey:h,active:h===i,onSelect:u,buttonComponentClass:d},h));return s&&r&&p&&e.push(a["default"].createElement(f["default"],{key:"ellipsis",disabled:!0,buttonComponentClass:d},a["default"].createElement("span",{"aria-label":"More"},"..."))),e},renderPrev:function(){return this.props.prev?a["default"].createElement(f["default"],{key:"prev",eventKey:this.props.activePage-1,disabled:1===this.props.activePage,onSelect:this.props.onSelect,buttonComponentClass:this.props.buttonComponentClass},a["default"].createElement("span",{"aria-label":"Previous"},"‹")):null},renderNext:function(){return this.props.next?a["default"].createElement(f["default"],{key:"next",eventKey:this.props.activePage+1,disabled:this.props.activePage>=this.props.items,onSelect:this.props.onSelect,buttonComponentClass:this.props.buttonComponentClass},a["default"].createElement("span",{"aria-label":"Next"},"›")):null},renderFirst:function(){return this.props.first?a["default"].createElement(f["default"],{key:"first",eventKey:1,disabled:1===this.props.activePage,onSelect:this.props.onSelect,buttonComponentClass:this.props.buttonComponentClass},a["default"].createElement("span",{"aria-label":"First"},"«")):null},renderLast:function(){return this.props.last?a["default"].createElement(f["default"],{key:"last",eventKey:this.props.items,disabled:this.props.activePage>=this.props.items,onSelect:this.props.onSelect,buttonComponentClass:this.props.buttonComponentClass},a["default"].createElement("span",{"aria-label":"Last"},"»")):null},render:function(){return a["default"].createElement("ul",r({},this.props,{className:l["default"](this.props.className,this.getBsClassSet())}),this.renderFirst(),this.renderPrev(),this.renderPageButtons(),this.renderNext(),this.renderLast())}});t["default"]=y,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(10)["default"],s=n(2)["default"];t.__esModule=!0;var a=n(1),i=s(a),l=n(4),u=s(l),p=n(5),d=s(p),f=n(146),c=s(f),h=n(8),m=s(h),v=i["default"].createClass({displayName:"PaginationButton",mixins:[d["default"]],propTypes:{className:i["default"].PropTypes.string,eventKey:i["default"].PropTypes.oneOfType([i["default"].PropTypes.string,i["default"].PropTypes.number]),onSelect:i["default"].PropTypes.func,disabled:i["default"].PropTypes.bool,active:i["default"].PropTypes.bool,buttonComponentClass:m["default"].elementType},getDefaultProps:function(){return{active:!1,disabled:!1}},handleClick:function(e){if(!this.props.disabled&&this.props.onSelect){var t=c["default"](this.props.eventKey);this.props.onSelect(e,t)}},render:function(){var e=r({active:this.props.active,disabled:this.props.disabled},this.getBsClassSet()),t=this.props,n=t.className,s=o(t,["className"]),a=this.props.buttonComponentClass;return i["default"].createElement("li",{className:u["default"](n,e)},i["default"].createElement(a,r({},s,{onClick:this.handleClick})))}});t["default"]=v,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(10)["default"],o=n(3)["default"],s=n(2)["default"];t.__esModule=!0;var a=n(1),i=s(a),l=n(4),u=s(l),p=n(5),d=s(p),f=n(26),c=s(f),h=i["default"].createClass({displayName:"Panel",mixins:[d["default"]],propTypes:{collapsible:i["default"].PropTypes.bool,onSelect:i["default"].PropTypes.func,header:i["default"].PropTypes.node,id:i["default"].PropTypes.oneOfType([i["default"].PropTypes.string,i["default"].PropTypes.number]),footer:i["default"].PropTypes.node,defaultExpanded:i["default"].PropTypes.bool,expanded:i["default"].PropTypes.bool,eventKey:i["default"].PropTypes.any,headerRole:i["default"].PropTypes.string,panelRole:i["default"].PropTypes.string},getDefaultProps:function(){return{bsClass:"panel",bsStyle:"default",defaultExpanded:!1}},getInitialState:function(){return{expanded:this.props.defaultExpanded}},handleSelect:function(e){e.selected=!0,this.props.onSelect?this.props.onSelect(e,this.props.eventKey):e.preventDefault(),e.selected&&this.handleToggle()},handleToggle:function(){this.setState({expanded:!this.state.expanded})},isExpanded:function(){return null!=this.props.expanded?this.props.expanded:this.state.expanded},render:function(){var e=this.props,t=e.headerRole,n=e.panelRole,s=r(e,["headerRole","panelRole"]);return i["default"].createElement("div",o({},s,{className:u["default"](this.props.className,this.getBsClassSet()),id:this.props.collapsible?null:this.props.id,onSelect:null}),this.renderHeading(t),this.props.collapsible?this.renderCollapsibleBody(n):this.renderBody(),this.renderFooter())},renderCollapsibleBody:function(e){var t={className:this.prefixClass("collapse"),id:this.props.id,ref:"panel","aria-hidden":!this.isExpanded()};return e&&(t.role=e),i["default"].createElement(c["default"],{"in":this.isExpanded()},i["default"].createElement("div",t,this.renderBody()))},renderBody:function(){function e(){return{key:l.length}}function t(t){l.push(a.cloneElement(t,e()))}function n(t){l.push(i["default"].createElement("div",o({className:p},e()),t))}function r(){0!==u.length&&(n(u),u=[])}var s=this.props.children,l=[],u=[],p=this.prefixClass("body");return Array.isArray(s)&&0!==s.length?(s.forEach(function(e){this.shouldRenderFill(e)?(r(),t(e)):u.push(e)}.bind(this)),r()):this.shouldRenderFill(s)?t(s):n(s),l},shouldRenderFill:function(e){return i["default"].isValidElement(e)&&null!=e.props.fill},renderHeading:function(e){var t=this.props.header;if(!t)return null;if(!i["default"].isValidElement(t)||Array.isArray(t))t=this.props.collapsible?this.renderCollapsibleTitle(t,e):t;else{var n=u["default"](this.prefixClass("title"),t.props.className);t=this.props.collapsible?a.cloneElement(t,{className:n,children:this.renderAnchor(t.props.children,e)}):a.cloneElement(t,{className:n})}return i["default"].createElement("div",{className:this.prefixClass("heading")},t)},renderAnchor:function(e,t){return i["default"].createElement("a",{href:"#"+(this.props.id||""),"aria-controls":this.props.collapsible?this.props.id:null,className:this.isExpanded()?null:"collapsed","aria-expanded":this.isExpanded(),"aria-selected":this.isExpanded(),onClick:this.handleSelect,role:t},e)},renderCollapsibleTitle:function(e,t){return i["default"].createElement("h4",{className:this.prefixClass("title"),role:"presentation"},this.renderAnchor(e,t))},renderFooter:function(){return this.props.footer?i["default"].createElement("div",{className:this.prefixClass("footer")},this.props.footer):null}});t["default"]=h,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=n(8),f=o(d),c=a["default"].createClass({displayName:"Popover",mixins:[p["default"]],propTypes:{id:f["default"].isRequiredForA11y(a["default"].PropTypes.oneOfType([a["default"].PropTypes.string,a["default"].PropTypes.number])),placement:a["default"].PropTypes.oneOf(["top","right","bottom","left"]),positionLeft:a["default"].PropTypes.number,positionTop:a["default"].PropTypes.number,arrowOffsetLeft:a["default"].PropTypes.oneOfType([a["default"].PropTypes.number,a["default"].PropTypes.string]),arrowOffsetTop:a["default"].PropTypes.oneOfType([a["default"].PropTypes.number,a["default"].PropTypes.string]),title:a["default"].PropTypes.node},getDefaultProps:function(){
return{placement:"right"}},render:function(){var e,t=(e={popover:!0},e[this.props.placement]=!0,e),n=r({left:this.props.positionLeft,top:this.props.positionTop,display:"block"},this.props.style),o={left:this.props.arrowOffsetLeft,top:this.props.arrowOffsetTop};return a["default"].createElement("div",r({role:"tooltip"},this.props,{className:l["default"](this.props.className,t),style:n,title:null}),a["default"].createElement("div",{className:"arrow",style:o}),this.props.title?this.renderTitle():null,a["default"].createElement("div",{className:"popover-content"},this.props.children))},renderTitle:function(){return a["default"].createElement("h3",{className:"popover-title"},this.props.title)}});t["default"]=c,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(2)["default"];t.__esModule=!0;var o=n(12),s=r(o),a=n(47),i=r(a);t["default"]=s["default"].wrapper(i["default"],{message:"The Portal component is deprecated in react-bootstrap. It has been moved to a more generic library: react-overlays. You can read more at: http://react-bootstrap.github.io/react-overlays/examples/#portal and https://github.com/react-bootstrap/react-bootstrap/issues/1084"}),e.exports=t["default"]},function(e,t,n){"use strict";var r=n(2)["default"];t.__esModule=!0;var o=n(12),s=r(o),a=n(97),i=r(a);t["default"]=s["default"].wrapper(i["default"],{message:"The Position component is deprecated in react-bootstrap. It has been moved to a more generic library: react-overlays. You can read more at: http://react-bootstrap.github.io/react-overlays/examples/#position and https://github.com/react-bootstrap/react-bootstrap/issues/1084"}),e.exports=t["default"]},function(e,t,n){"use strict";function r(e,t,n){if(e[t]){var r=function(){var r=void 0,o=void 0;return l["default"].Children.forEach(e[t],function(e){e.type!==y&&(o=e.type.displayName?e.type.displayName:e.type,r=new Error("Children of "+n+" can contain only ProgressBar components. Found "+o))}),{v:r}}();if("object"==typeof r)return r.v}}var o=n(3)["default"],s=n(10)["default"],a=n(2)["default"];t.__esModule=!0;var i=n(1),l=a(i),u=n(57),p=a(u),d=n(5),f=a(d),c=n(4),h=a(c),m=n(9),v=a(m),y=l["default"].createClass({displayName:"ProgressBar",propTypes:{min:i.PropTypes.number,now:i.PropTypes.number,max:i.PropTypes.number,label:i.PropTypes.node,srOnly:i.PropTypes.bool,striped:i.PropTypes.bool,active:i.PropTypes.bool,children:r,className:l["default"].PropTypes.string,interpolateClass:i.PropTypes.node,isChild:i.PropTypes.bool},mixins:[f["default"]],getDefaultProps:function(){return{bsClass:"progress-bar",min:0,max:100,active:!1,isChild:!1,srOnly:!1,striped:!1}},getPercentage:function(e,t,n){var r=1e3;return Math.round((e-t)/(n-t)*100*r)/r},render:function(){if(this.props.isChild)return this.renderProgressBar();var e=void 0;return e=this.props.children?v["default"].map(this.props.children,this.renderChildBar):this.renderProgressBar(),l["default"].createElement("div",o({},this.props,{className:h["default"](this.props.className,"progress"),min:null,max:null,label:null,"aria-valuetext":null}),e)},renderChildBar:function(e,t){return i.cloneElement(e,{isChild:!0,key:e.key?e.key:t})},renderProgressBar:function(){var e=this.props,t=e.className,n=e.label,r=e.now,a=e.min,i=e.max,u=s(e,["className","label","now","min","max"]),p=this.getPercentage(r,a,i);"string"==typeof n&&(n=this.renderLabel(p)),this.props.srOnly&&(n=l["default"].createElement("span",{className:"sr-only"},n));var d=h["default"](t,this.getBsClassSet(),{active:this.props.active,"progress-bar-striped":this.props.active||this.props.striped});return l["default"].createElement("div",o({},u,{className:d,role:"progressbar",style:{width:p+"%"},"aria-valuenow":this.props.now,"aria-valuemin":this.props.min,"aria-valuemax":this.props.max}),n)},renderLabel:function(e){var t=this.props.interpolateClass||p["default"];return l["default"].createElement(t,{now:this.props.now,min:this.props.min,max:this.props.max,percent:e,bsStyle:this.props.bsStyle},this.props.label)}});t["default"]=y,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(7)["default"],o=n(6)["default"],s=n(3)["default"],a=n(10)["default"],i=n(2)["default"];t.__esModule=!0;var l=n(1),u=i(l),p=n(5),d=i(p),f=n(25),c=i(f),h=n(27),m=i(h),v=n(137),y=i(v),g=function(e){function t(){o(this,t),e.apply(this,arguments)}return r(t,e),t.prototype.render=function(){var e=this.props,t=e.children,n=e.title,r=e.onClick,o=e.target,s=e.href,i=e.bsStyle,l=a(e,["children","title","onClick","target","href","bsStyle"]),p=l.disabled,d=u["default"].createElement(c["default"],{onClick:r,bsStyle:i,disabled:p,target:o,href:s},n);return u["default"].createElement(m["default"],l,d,u["default"].createElement(y["default"],{"aria-label":n,bsStyle:i,disabled:p}),u["default"].createElement(m["default"].Menu,null,t))},t}(u["default"].Component);g.propTypes=s({},m["default"].propTypes,d["default"].propTypes,{onClick:function(){},target:u["default"].PropTypes.string,href:u["default"].PropTypes.string,title:u["default"].PropTypes.node.isRequired}),g.defaultProps={disabled:!1,dropup:!1,pullRight:!1},g.Toggle=y["default"],t["default"]=g,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(7)["default"],o=n(6)["default"],s=n(3)["default"],a=n(2)["default"];t.__esModule=!0;var i=n(1),l=a(i),u=n(53),p=a(u),d=function(e){function t(){o(this,t),e.apply(this,arguments)}return r(t,e),t.prototype.render=function(){return l["default"].createElement(p["default"],s({},this.props,{useAnchor:!1,noCaret:!1}))},t}(l["default"].Component);t["default"]=d,d.defaultProps=p["default"].defaultProps,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(9),p=o(u),d=n(11),f=o(d),c=n(5),h=o(c),m=n(14),v=o(m),y=a["default"].createClass({displayName:"SubNav",mixins:[h["default"]],propTypes:{onSelect:a["default"].PropTypes.func,active:a["default"].PropTypes.bool,activeHref:a["default"].PropTypes.string,activeKey:a["default"].PropTypes.any,disabled:a["default"].PropTypes.bool,eventKey:a["default"].PropTypes.any,href:a["default"].PropTypes.string,title:a["default"].PropTypes.string,text:a["default"].PropTypes.node,target:a["default"].PropTypes.string},getDefaultProps:function(){return{bsClass:"nav",active:!1,disabled:!1}},handleClick:function(e){this.props.onSelect&&(e.preventDefault(),this.props.disabled||this.props.onSelect(this.props.eventKey,this.props.href,this.props.target))},isActive:function(){return this.isChildActive(this)},isChildActive:function(e){if(e.props.active)return!0;if(null!=this.props.activeKey&&this.props.activeKey===e.props.eventKey)return!0;if(null!=this.props.activeHref&&this.props.activeHref===e.props.href)return!0;if(e.props.children){var t=!1;return p["default"].forEach(e.props.children,function(e){this.isChildActive(e)&&(t=!0)},this),t}return!1},getChildActiveProp:function(e){return e.props.active?!0:null!=this.props.activeKey&&e.props.eventKey===this.props.activeKey?!0:null!=this.props.activeHref&&e.props.href===this.props.activeHref?!0:e.props.active},render:function(){var e={active:this.isActive(),disabled:this.props.disabled};return a["default"].createElement("li",r({},this.props,{className:l["default"](this.props.className,e)}),a["default"].createElement(v["default"],{href:this.props.href,title:this.props.title,target:this.props.target,onClick:this.handleClick},this.props.text),a["default"].createElement("ul",{className:"nav"},p["default"].map(this.props.children,this.renderNavItem)))},renderNavItem:function(e,t){return s.cloneElement(e,{active:this.getChildActiveProp(e),onSelect:f["default"](e.props.onSelect,this.props.onSelect),key:e.key?e.key:t})}});t["default"]=y,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(10)["default"],o=n(3)["default"],s=n(2)["default"];t.__esModule=!0;var a=n(1),i=s(a),l=n(70),u=s(l),p=n(69),d=s(p),f=n(9),c=s(f),h=n(12),m=s(h),v=i["default"].createClass({displayName:"TabbedArea",componentWillMount:function(){m["default"]("TabbedArea","Tabs","https://github.com/react-bootstrap/react-bootstrap/pull/1091")},render:function(){var e=this.props,t=e.children,n=r(e,["children"]),s=c["default"].map(t,function(e){var t=e.props,n=t.tab,s=r(t,["tab"]);return i["default"].createElement(d["default"],o({title:n},s))});return i["default"].createElement(u["default"],n,s)}});t["default"]=v,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=a["default"].createClass({displayName:"Table",propTypes:{striped:a["default"].PropTypes.bool,bordered:a["default"].PropTypes.bool,condensed:a["default"].PropTypes.bool,hover:a["default"].PropTypes.bool,responsive:a["default"].PropTypes.bool},getDefaultProps:function(){return{bordered:!1,condensed:!1,hover:!1,responsive:!1,striped:!1}},render:function(){var e={table:!0,"table-striped":this.props.striped,"table-bordered":this.props.bordered,"table-condensed":this.props.condensed,"table-hover":this.props.hover},t=a["default"].createElement("table",r({},this.props,{className:l["default"](this.props.className,e)}),this.props.children);return this.props.responsive?a["default"].createElement("div",{className:"table-responsive"},t):t}});t["default"]=u,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=n(14),f=o(d),c=a["default"].createClass({displayName:"Thumbnail",mixins:[p["default"]],propTypes:{alt:a["default"].PropTypes.string,href:a["default"].PropTypes.string,src:a["default"].PropTypes.string},getDefaultProps:function(){return{bsClass:"thumbnail"}},render:function(){var e=this.getBsClassSet();return this.props.href?a["default"].createElement(f["default"],r({},this.props,{href:this.props.href,className:l["default"](this.props.className,e)}),a["default"].createElement("img",{src:this.props.src,alt:this.props.alt})):this.props.children?a["default"].createElement("div",r({},this.props,{className:l["default"](this.props.className,e)}),a["default"].createElement("img",{src:this.props.src,alt:this.props.alt}),a["default"].createElement("div",{className:"caption"},this.props.children)):a["default"].createElement("div",r({},this.props,{className:l["default"](this.props.className,e)}),a["default"].createElement("img",{src:this.props.src,alt:this.props.alt}))}});t["default"]=c,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=n(8),f=o(d),c=a["default"].createClass({displayName:"Tooltip",mixins:[p["default"]],propTypes:{id:f["default"].isRequiredForA11y(a["default"].PropTypes.oneOfType([a["default"].PropTypes.string,a["default"].PropTypes.number])),placement:a["default"].PropTypes.oneOf(["top","right","bottom","left"]),positionLeft:a["default"].PropTypes.number,positionTop:a["default"].PropTypes.number,arrowOffsetLeft:a["default"].PropTypes.oneOfType([a["default"].PropTypes.number,a["default"].PropTypes.string]),arrowOffsetTop:a["default"].PropTypes.oneOfType([a["default"].PropTypes.number,a["default"].PropTypes.string]),title:a["default"].PropTypes.node},getDefaultProps:function(){return{placement:"right"}},render:function(){var e,t=(e={tooltip:!0},e[this.props.placement]=!0,e),n=r({left:this.props.positionLeft,top:this.props.positionTop},this.props.style),o={left:this.props.arrowOffsetLeft,top:this.props.arrowOffsetTop};return a["default"].createElement("div",r({role:"tooltip"},this.props,{className:l["default"](this.props.className,t),style:n}),a["default"].createElement("div",{className:"tooltip-arrow",style:o}),a["default"].createElement("div",{className:"tooltip-inner"},this.props.children))}});t["default"]=c,e.exports=t["default"]},function(e,t,n){"use strict";var r=n(3)["default"],o=n(2)["default"];t.__esModule=!0;var s=n(1),a=o(s),i=n(4),l=o(i),u=n(5),p=o(u),d=a["default"].createClass({displayName:"Well",mixins:[p["default"]],getDefaultProps:function(){return{bsClass:"well"}},render:function(){var e=this.getBsClassSet();return a["default"].createElement("div",r({},this.props,{className:l["default"](this.props.className,e)}),this.props.children)}});t["default"]=d,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){var t=[];return void 0===e?t:(a["default"].forEach(e,function(e){t.push(e)}),t)}var o=n(2)["default"];t.__esModule=!0,t["default"]=r;var s=n(9),a=o(s);e.exports=t["default"]},function(e,t,n){"use strict";function r(e,t){return function(n){var r=function(e){function t(){s(this,t),e.apply(this,arguments)}return o(t,e),t.prototype.getChildContext=function(){return this.props.context},t.prototype.render=function(){var e=this.props,t=e.wrapped,n=(e.context,i(e,["wrapped","context"]));return p["default"].cloneElement(t,n)},t}(p["default"].Component);r.childContextTypes=n;var l=function(){function n(){s(this,n)}return n.prototype.render=function(){var n=a({},this.props);return n[t]=this.getWrappedOverlay(),p["default"].createElement(e,n,this.props.children)},n.prototype.getWrappedOverlay=function(){return p["default"].createElement(r,{context:this.context,wrapped:this.props[t]})},n}();return l.contextTypes=n,l}}var o=n(7)["default"],s=n(6)["default"],a=n(3)["default"],i=n(10)["default"],l=n(2)["default"];t.__esModule=!0,t["default"]=r;var u=n(1),p=l(u);e.exports=t["default"]},function(e,t){"use strict";function n(e){var t=!1;return{eventKey:e,preventSelection:function(){t=!0},isSelectionPrevented:function(){return t}}}t.__esModule=!0,t["default"]=n,e.exports=t["default"]},function(e,t,n){e.exports={"default":n(152),__esModule:!0}},function(e,t,n){e.exports={"default":n(153),__esModule:!0}},function(e,t,n){e.exports={"default":n(154),__esModule:!0}},function(e,t,n){e.exports={"default":n(155),__esModule:!0}},function(e,t,n){e.exports={"default":n(157),__esModule:!0}},function(e,t,n){n(169),e.exports=n(18).Object.assign},function(e,t,n){var r=n(29);e.exports=function(e,t){return r.create(e,t)}},function(e,t,n){var r=n(29);e.exports=function(e,t,n){return r.setDesc(e,t,n)}},function(e,t,n){n(170),e.exports=n(18).Object.isFrozen},function(e,t,n){n(171),e.exports=n(18).Object.keys},function(e,t,n){n(172),e.exports=n(18).Object.setPrototypeOf},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(38);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(74),o=n(167),s=n(164);e.exports=Object.assign||function(e,t){for(var n=r(e),a=arguments.length,i=1;a>i;)for(var l,u=o(arguments[i++]),p=s(u),d=p.length,f=0;d>f;)n[l=p[f++]]=u[l];return n}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){var r=n(158);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(29);e.exports=function(e){var t=r.getKeys(e),n=r.getSymbols;if(n)for(var o,s=n(e),a=r.isEnum,i=0;s.length>i;)a.call(e,o=s[i++])&&t.push(o);return t}},function(e,t){e.exports=function(e){try{return!!e()}catch(t){return!0}}},function(e,t){var n="undefined"!=typeof self&&self.Math==Math?self:Function("return this")();e.exports=n,"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(161);e.exports=0 in Object("z")?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},function(e,t,n){var r=n(29).getDesc,o=n(38),s=n(159),a=function(e,t){if(s(e),!o(t)&&null!==t)throw TypeError(t+": can't set as prototype!")};e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,t){try{t=n(162)(Function.call,r(Object.prototype,"__proto__").set,2),t({},[])}catch(o){e=!0}return function(n,r){return a(n,r),e?n.__proto__=r:t(n,r),n}}():void 0),check:a}},function(e,t,n){var r=n(37);r(r.S,"Object",{assign:n(160)})},function(e,t,n){var r=n(38);n(73)("isFrozen",function(e){return function(t){return r(t)?e?e(t):!1:!0}})},function(e,t,n){var r=n(74);n(73)("keys",function(e){return function(t){return e(r(t))}})},function(e,t,n){var r=n(37);r(r.S,"Object",{setPrototypeOf:n(168).set})},function(e,t,n){"use strict";function r(){var e=void 0===arguments[0]?document:arguments[0];try{return e.activeElement}catch(t){}}var o=n(24);t.__esModule=!0,t["default"]=r;var s=n(19);o.interopRequireDefault(s);e.exports=t["default"]},function(e,t,n){"use strict";var r=n(20),o=function(){};r&&(o=function(){return document.addEventListener?function(e,t,n,r){return e.removeEventListener(t,n,r||!1)}:document.attachEvent?function(e,t,n){return e.detachEvent("on"+t,n)}:void 0}()),e.exports=o},function(e,t,n){"use strict";function r(e){var t=a["default"](e);return t&&t.defaultView||t.parentWindow}var o=n(24);t.__esModule=!0,t["default"]=r;var s=n(19),a=o.interopRequireDefault(s);e.exports=t["default"]},function(e,t,n){"use strict";var r=n(39);e.exports=function(e,t){var n=r(e);return void 0===t?n?"pageXOffset"in n?n.pageXOffset:n.document.documentElement.scrollLeft:e.scrollLeft:void(n?n.scrollTo(t,"pageYOffset"in n?n.pageYOffset:n.document.documentElement.scrollTop):e.scrollLeft=t)}},function(e,t,n){"use strict";var r=n(24),o=n(80),s=r.interopRequireDefault(o),a=/^(top|right|bottom|left)$/,i=/^([+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|))(?!px)[a-z%]+$/i;e.exports=function(e){if(!e)throw new TypeError("No Element passed to `getComputedStyle()`");var t=e.ownerDocument;return"defaultView"in t?t.defaultView.opener?e.ownerDocument.defaultView.getComputedStyle(e,null):window.getComputedStyle(e,null):{getPropertyValue:function(t){var n=e.style;t=s["default"](t),"float"==t&&(t="styleFloat");var r=e.currentStyle[t]||null;if(null==r&&n&&n[t]&&(r=n[t]),i.test(r)&&!a.test(t)){var o=n.left,l=e.runtimeStyle,u=l&&l.left;u&&(l.left=e.currentStyle.left),n.left="fontSize"===t?"1em":r,r=n.pixelLeft+"px",n.left=o,u&&(l.left=u)}return r}}}},function(e,t){"use strict";e.exports=function(e,t){return"removeProperty"in e.style?e.style.removeProperty(t):e.style.removeAttribute(t)}},function(e,t,n){"use strict";function r(){var e,t="",n={O:"otransitionend",Moz:"transitionend",Webkit:"webkitTransitionEnd",ms:"MSTransitionEnd"},r=document.createElement("div");for(var o in n)if(u.call(n,o)&&void 0!==r.style[o+"TransitionProperty"]){t="-"+o.toLowerCase()+"-",e=n[o];break}return e||void 0===r.style.transitionProperty||(e="transitionend"),{end:e,prefix:t}}var o,s,a,i,l=n(20),u=Object.prototype.hasOwnProperty,p="transform",d={};l&&(d=r(),p=d.prefix+p,a=d.prefix+"transition-property",s=d.prefix+"transition-duration",i=d.prefix+"transition-delay",o=d.prefix+"transition-timing-function"),e.exports={transform:p,end:d.end,property:a,timing:o,delay:i,duration:s}},function(e,t){"use strict";var n=/-(.)/g;e.exports=function(e){return e.replace(n,function(e,t){return t.toUpperCase()})}},function(e,t){"use strict";var n=/([A-Z])/g;e.exports=function(e){return e.replace(n,"-$1").toLowerCase()}},function(e,t,n){"use strict";var r=n(181),o=/^ms-/;e.exports=function(e){return r(e).replace(o,"-ms-")}},function(e,t,n){"use strict";var r,o=n(20);e.exports=function(e){if((!r||e)&&o){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),r=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return r}},function(e,t){function n(e){var t=e?e.length:0;return t?e[t-1]:void 0}e.exports=n},function(e,t,n){var r=n(192),o=n(210),s=o(r);e.exports=s},function(e,t,n){(function(t){function r(e){var t=e?e.length:0;for(this.data={hash:i(null),set:new a};t--;)this.push(e[t])}var o=n(206),s=n(30),a=s(t,"Set"),i=s(Object,"create");r.prototype.push=o,e.exports=r}).call(t,function(){return this}())},function(e,t){function n(e,t){for(var n=-1,r=e.length,o=Array(r);++n<r;)o[n]=t(e[n],n,e);return o}e.exports=n},function(e,t){function n(e,t){for(var n=-1,r=t.length,o=e.length;++n<r;)e[o+n]=t[n];return e}e.exports=n},function(e,t){function n(e,t){for(var n=-1,r=e.length;++n<r;)if(t(e[n],n,e))return!0;return!1}e.exports=n},function(e,t,n){function r(e,t,n){var r=typeof e;return"function"==r?void 0===t?e:a(e,t,n):null==e?i:"object"==r?o(e):void 0===t?l(e):s(e,t)}var o=n(200),s=n(201),a=n(42),i=n(96),l=n(222);e.exports=r},function(e,t,n){function r(e,t){var n=e?e.length:0,r=[];if(!n)return r;var l=-1,u=o,p=!0,d=p&&t.length>=i?a(t):null,f=t.length;d&&(u=s,p=!1,t=d);e:for(;++l<n;){var c=e[l];if(p&&c===c){for(var h=f;h--;)if(t[h]===c)continue e;r.push(c)}else u(t,c,0)<0&&r.push(c)}return r}var o=n(197),s=n(205),a=n(209),i=200;e.exports=r},function(e,t,n){var r=n(196),o=n(207),s=o(r);e.exports=s},function(e,t){function n(e,t,n,r){var o;return n(e,function(e,n,s){return t(e,n,s)?(o=r?n:e,!1):void 0}),o}e.exports=n},function(e,t){function n(e,t,n){for(var r=e.length,o=n?r:-1;n?o--:++o<r;)if(t(e[o],o,e))return o;return-1}e.exports=n},function(e,t,n){function r(e,t){return o(e,t,s)}var o=n(84),s=n(46);e.exports=r},function(e,t,n){function r(e,t){return o(e,t,s)}var o=n(84),s=n(45);e.exports=r},function(e,t,n){function r(e,t,n){if(t!==t)return o(e,n);for(var r=n-1,s=e.length;++r<s;)if(e[r]===t)return r;return-1}var o=n(215);e.exports=r},function(e,t,n){function r(e,t,n,r,f,m,v){var y=i(e),g=i(t),b=p,T=p;y||(b=h.call(e),b==u?b=d:b!=d&&(y=l(e))),g||(T=h.call(t),T==u?T=d:T!=d&&(g=l(t)));var P=b==d,x=T==d,E=b==T;if(E&&!y&&!P)return s(e,t,b);if(!f){var C=P&&c.call(e,"__wrapped__"),_=x&&c.call(t,"__wrapped__");if(C||_)return n(C?e.value():e,_?t.value():t,r,f,m,v)}if(!E)return!1;m||(m=[]),v||(v=[]);for(var N=m.length;N--;)if(m[N]==e)return v[N]==t;m.push(e),v.push(t);var O=(y?o:a)(e,t,n,r,f,m,v);return m.pop(),v.pop(),O}var o=n(211),s=n(212),a=n(213),i=n(15),l=n(219),u="[object Arguments]",p="[object Array]",d="[object Object]",f=Object.prototype,c=f.hasOwnProperty,h=f.toString;e.exports=r},function(e,t,n){function r(e,t,n){var r=t.length,a=r,i=!n;if(null==e)return!a;for(e=s(e);r--;){var l=t[r];if(i&&l[2]?l[1]!==e[l[0]]:!(l[0]in e))return!1}for(;++r<a;){l=t[r];var u=l[0],p=e[u],d=l[1];if(i&&l[2]){if(void 0===p&&!(u in e))return!1}else{var f=n?n(p,d,u):void 0;if(!(void 0===f?o(d,p,n,!0):f))return!1}}return!0}var o=n(86),s=n(13);e.exports=r},function(e,t,n){function r(e){var t=s(e);if(1==t.length&&t[0][2]){var n=t[0][0],r=t[0][1];return function(e){return null==e?!1:e[n]===r&&(void 0!==r||n in a(e))}}return function(e){return o(e,t)}}var o=n(199),s=n(214),a=n(13);e.exports=r},function(e,t,n){function r(e,t){var n=i(e),r=l(e)&&u(t),c=e+"";return e=f(e),function(i){if(null==i)return!1;var l=c;if(i=d(i),(n||!r)&&!(l in i)){if(i=1==e.length?i:o(i,a(e,0,-1)),null==i)return!1;l=p(e),i=d(i)}return i[l]===t?void 0!==t||l in i:s(t,i[l],void 0,!0)}}var o=n(85),s=n(86),a=n(203),i=n(15),l=n(90),u=n(91),p=n(184),d=n(13),f=n(94);e.exports=r},function(e,t,n){function r(e){var t=e+"";return e=s(e),function(n){return o(n,e,t)}}var o=n(85),s=n(94);e.exports=r},function(e,t){function n(e,t,n){var r=-1,o=e.length;t=null==t?0:+t||0,0>t&&(t=-t>o?0:o+t),n=void 0===n||n>o?o:+n||0,0>n&&(n+=o),o=t>n?0:n-t>>>0,t>>>=0;for(var s=Array(o);++r<o;)s[r]=e[r+t];return s}e.exports=n},function(e,t){function n(e){return null==e?"":e+""}e.exports=n},function(e,t,n){function r(e,t){var n=e.data,r="string"==typeof t||o(t)?n.set.has(t):n.hash[t];return r?0:-1}var o=n(16);e.exports=r},function(e,t,n){function r(e){var t=this.data;"string"==typeof e||o(e)?t.set.add(e):t.hash[e]=!0}var o=n(16);e.exports=r},function(e,t,n){function r(e,t){return function(n,r){var i=n?o(n):0;if(!s(i))return e(n,r);for(var l=t?i:-1,u=a(n);(t?l--:++l<i)&&r(u[l],l,u)!==!1;);return n}}var o=n(88),s=n(21),a=n(13);e.exports=r},function(e,t,n){function r(e){return function(t,n,r){for(var s=o(t),a=r(t),i=a.length,l=e?i:-1;e?l--:++l<i;){var u=a[l];if(n(s[u],u,s)===!1)break}return t}}var o=n(13);e.exports=r},function(e,t,n){(function(t){function r(e){return i&&a?new o(e):null}var o=n(186),s=n(30),a=s(t,"Set"),i=s(Object,"create");e.exports=r}).call(t,function(){return this}())},function(e,t,n){function r(e,t){return function(n,r,l){if(r=o(r,l,3),i(n)){var u=a(n,r,t);return u>-1?n[u]:void 0}return s(n,r,e)}}var o=n(190),s=n(193),a=n(194),i=n(15);e.exports=r},function(e,t,n){function r(e,t,n,r,s,a,i){var l=-1,u=e.length,p=t.length;if(u!=p&&!(s&&p>u))return!1;for(;++l<u;){var d=e[l],f=t[l],c=r?r(s?f:d,s?d:f,l):void 0;if(void 0!==c){if(c)continue;return!1}if(s){if(!o(t,function(e){return d===e||n(d,e,r,s,a,i)}))return!1}else if(d!==f&&!n(d,f,r,s,a,i))return!1}return!0}var o=n(189);e.exports=r},function(e,t){function n(e,t,n){switch(n){case r:case o:return+e==+t;case s:return e.name==t.name&&e.message==t.message;case a:return e!=+e?t!=+t:e==+t;case i:case l:return e==t+""}return!1}var r="[object Boolean]",o="[object Date]",s="[object Error]",a="[object Number]",i="[object RegExp]",l="[object String]";e.exports=n},function(e,t,n){function r(e,t,n,r,s,i,l){var u=o(e),p=u.length,d=o(t),f=d.length;if(p!=f&&!s)return!1;for(var c=p;c--;){var h=u[c];if(!(s?h in t:a.call(t,h)))return!1}for(var m=s;++c<p;){h=u[c];var v=e[h],y=t[h],g=r?r(s?y:v,s?v:y,h):void 0;if(!(void 0===g?n(v,y,r,s,i,l):g))return!1;m||(m="constructor"==h)}if(!m){var b=e.constructor,T=t.constructor;if(b!=T&&"constructor"in e&&"constructor"in t&&!("function"==typeof b&&b instanceof b&&"function"==typeof T&&T instanceof T))return!1}return!0}var o=n(45),s=Object.prototype,a=s.hasOwnProperty;e.exports=r},function(e,t,n){function r(e){for(var t=s(e),n=t.length;n--;)t[n][2]=o(t[n][1]);return t}var o=n(91),s=n(220);e.exports=r},function(e,t){function n(e,t,n){for(var r=e.length,o=t+(n?0:-1);n?o--:++o<r;){var s=e[o];if(s!==s)return o}return-1}e.exports=n},function(e,t,n){function r(e){for(var t=l(e),n=t.length,r=n&&e.length,u=!!r&&i(r)&&(s(e)||o(e)),d=-1,f=[];++d<n;){var c=t[d];(u&&a(c,r)||p.call(e,c))&&f.push(c)}return f}var o=n(44),s=n(15),a=n(89),i=n(21),l=n(46),u=Object.prototype,p=u.hasOwnProperty;e.exports=r},function(e,t,n){function r(e){return o(e)&&i.call(e)==s}var o=n(16),s="[object Function]",a=Object.prototype,i=a.toString;e.exports=r},function(e,t,n){function r(e){return null==e?!1:o(e)?p.test(l.call(e)):s(e)&&a.test(e)}var o=n(217),s=n(22),a=/^\[object .+?Constructor\]$/,i=Object.prototype,l=Function.prototype.toString,u=i.hasOwnProperty,p=RegExp("^"+l.call(u).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");e.exports=r},function(e,t,n){function r(e){return s(e)&&o(e.length)&&!!S[M.call(e)]}var o=n(21),s=n(22),a="[object Arguments]",i="[object Array]",l="[object Boolean]",u="[object Date]",p="[object Error]",d="[object Function]",f="[object Map]",c="[object Number]",h="[object Object]",m="[object RegExp]",v="[object Set]",y="[object String]",g="[object WeakMap]",b="[object ArrayBuffer]",T="[object Float32Array]",P="[object Float64Array]",x="[object Int8Array]",E="[object Int16Array]",C="[object Int32Array]",_="[object Uint8Array]",N="[object Uint8ClampedArray]",O="[object Uint16Array]",w="[object Uint32Array]",S={};S[T]=S[P]=S[x]=S[E]=S[C]=S[_]=S[N]=S[O]=S[w]=!0,S[a]=S[i]=S[b]=S[l]=S[u]=S[p]=S[d]=S[f]=S[c]=S[h]=S[m]=S[v]=S[y]=S[g]=!1;var k=Object.prototype,M=k.toString;e.exports=r},function(e,t,n){function r(e){e=s(e);for(var t=-1,n=o(e),r=n.length,a=Array(r);++t<r;){var i=n[t];a[t]=[i,e[i]]}return a}var o=n(45),s=n(13);e.exports=r},function(e,t,n){var r=n(83),o=n(42),s=n(92),a=n(93),i=n(82),l=i(function(e,t){return null==e?{}:"function"==typeof t[0]?a(e,o(t[0],t[1],3)):s(e,r(t))});e.exports=l},function(e,t,n){function r(e){return a(e)?o(e):s(e)}var o=n(87),s=n(202),a=n(90);e.exports=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(e.__proto__=t)}t.__esModule=!0;var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},l=n(1),u=r(l),p=n(47),d=r(p),f=n(97),c=r(f),h=n(98),m=r(h),v=n(227),y=r(v),g=function(e){function t(n,r){s(this,t),e.call(this,n,r),this.state={exited:!n.show},this.onHiddenListener=this.handleHidden.bind(this)}return a(t,e),t.prototype.componentWillReceiveProps=function(e){e.show?this.setState({exited:!1}):e.transition||this.setState({exited:!0})},t.prototype.render=function(){var e=this.props,t=e.container,n=e.containerPadding,r=e.target,s=e.placement,a=e.rootClose,i=e.children,l=e.transition,p=o(e,["container","containerPadding","target","placement","rootClose","children","transition"]),f=p.show||l&&!this.state.exited;if(!f)return null;var h=i;if(h=u["default"].createElement(c["default"],{container:t,containerPadding:n,target:r,placement:s},h),l){var v=p.onExit,y=p.onExiting,g=p.onEnter,b=p.onEntering,T=p.onEntered;h=u["default"].createElement(l,{"in":p.show,transitionAppear:!0,onExit:v,onExiting:y,onExited:this.onHiddenListener,onEnter:g,onEntering:b,onEntered:T},h)}return a&&(h=u["default"].createElement(m["default"],{onRootClose:p.onHide},h)),u["default"].createElement(d["default"],{container:t},h)},t.prototype.handleHidden=function(){if(this.setState({exited:!0}),this.props.onExited){var e;(e=this.props).onExited.apply(e,arguments)}},t}(u["default"].Component);g.propTypes=i({},d["default"].propTypes,c["default"].propTypes,{show:u["default"].PropTypes.bool,rootClose:u["default"].PropTypes.bool,onHide:u["default"].PropTypes.func,transition:y["default"],onEnter:u["default"].PropTypes.func,onEntering:u["default"].PropTypes.func,onEntered:u["default"].PropTypes.func,onExit:u["default"].PropTypes.func,onExiting:u["default"].PropTypes.func,onExited:u["default"].PropTypes.func}),t["default"]=g,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}t.__esModule=!0;var o=n(75),s=r(o),a=n(174),i=r(a);t["default"]=function(e,t,n){return s["default"](e,t,n),{remove:function(){i["default"](e,t,n)}}},e.exports=t["default"]},function(e,t){"use strict";function n(){for(var e=arguments.length,t=Array(e),n=0;e>n;n++)t[n]=arguments[n];return t.filter(function(e){return null!=e}).reduce(function(e,t){if("function"!=typeof t)throw new Error("Invalid Argument Type, must only provide functions, undefined, or null.");return null===e?t:function(){for(var n=arguments.length,r=Array(n),o=0;n>o;o++)r[o]=arguments[o];e.apply(this,r),t.apply(this,r)}},null)}t.__esModule=!0,t["default"]=n,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,n,r){var o=h.getContainerDimensions(n),s=o.scroll,a=o.height,i=e-r-s,l=e+r-s+t;return 0>i?-i:l>a?a-l:0}function s(e,t,n,r){var o=h.getContainerDimensions(n),s=o.width,a=e-r,i=e+r+t;return 0>a?-a:i>s?s-i:0}t.__esModule=!0;var a=n(31),i=r(a),l=n(40),u=r(l),p=n(78),d=r(p),f=n(79),c=r(f),h={getContainerDimensions:function(e){var t=void 0,n=void 0,r=void 0;if("BODY"===e.tagName)t=window.innerWidth,n=window.innerHeight,r=c["default"](i["default"](e).documentElement)||c["default"](e);else{var o=u["default"](e);t=o.width,n=o.height,r=c["default"](e)}return{width:t,height:n,scroll:r}},getPosition:function(e,t){var n="BODY"===t.tagName?u["default"](e):d["default"](e,t);return n},calcOverlayPosition:function(e,t,n,r,a){var i=h.getPosition(n,r),l=u["default"](t),p=l.height,d=l.width,f=void 0,c=void 0,m=void 0,v=void 0;
if("left"===e||"right"===e){c=i.top+(i.height-p)/2,f="left"===e?i.left-d:i.left+i.width;var y=o(c,p,r,a);c+=y,v=50*(1-2*y/p)+"%",m=void 0}else{if("top"!==e&&"bottom"!==e)throw new Error('calcOverlayPosition(): No such placement of "'+e+'" found.');f=i.left+(i.width-d)/2,c="top"===e?i.top-p:i.top+i.height;var g=s(f,d,r,a);f+=g,m=50*(1-2*g/d)+"%",v=void 0}return{positionLeft:f,positionTop:c,arrowOffsetLeft:m,arrowOffsetTop:v}}};t["default"]=h,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,n){var r=i.errMsg(e,t,n,". Expected an Element `type`");if("function"!=typeof e[t]){if(a["default"].isValidElement(e[t]))return new Error(r+", not an actual Element");if("string"!=typeof e[t])return new Error(r+" such as a tag name or return value of React.createClass(...)")}}t.__esModule=!0;var s=n(1),a=r(s),i=n(101);t["default"]=i.createChainableTypeChecker(o),e.exports=t["default"]},function(e,t){function n(e){return function(){return e}}function r(){}r.thatReturns=n,r.thatReturnsFalse=n(!1),r.thatReturnsTrue=n(!0),r.thatReturnsNull=n(null),r.thatReturnsThis=function(){return this},r.thatReturnsArgument=function(e){return e},e.exports=r},function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function o(e){return e&&e.__esModule?e:{"default":e}}function s(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function a(e,t){function n(n,r){function o(e,n){var o=d.getLinkName(e),s=this.props[r[e]];o&&a(this.props,o)&&!s&&(s=this.props[o].requestChange);for(var i=arguments.length,l=Array(i>2?i-2:0),u=2;i>u;u++)l[u-2]=arguments[u];t(this,e,s,n,l)}function a(e,t){return void 0!==e[t]}var l,p=arguments.length<=2||void 0===arguments[2]?[]:arguments[2],f=n.displayName||n.name||"Component",c=d.getType(n).propTypes;l=d.uncontrolledPropTypes(r,c,f);var h=d.transform(p,function(e,t){e[t]=function(){var e=this.refs.controlled;return e[t].apply(e,arguments)}},{}),m=u["default"].createClass(i({displayName:"Uncontrolled("+f+")",mixins:e,propTypes:l},h,{componentWillMount:function(){var e=this.props,t=Object.keys(r);this._values=d.transform(t,function(t,n){t[n]=e[d.defaultKey(n)]},{})},render:function(){var e=this,t={},l=this.props,p=(l.valueLink,l.checkedLink,s(l,["valueLink","checkedLink"]));return d.each(r,function(n,r){var s=d.getLinkName(r),i=e.props[r];s&&!a(e.props,r)&&a(e.props,s)&&(i=e.props[s].value),t[r]=void 0!==i?i:e._values[r],t[n]=o.bind(e,r)}),t=i({ref:"controlled"},p,t),u["default"].createElement(n,t)}}));return m.ControlledComponent=n,m}return n}t.__esModule=!0;var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e};t["default"]=a;var l=n(1),u=o(l),p=n(232),d=r(p);e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,n,r,o){n&&(e._notifying=!0,n.call.apply(n,[e,r].concat(o)),e._notifying=!1),e._values[t]=r,e.forceUpdate()}t.__esModule=!0;var s=n(229),a=r(s),i={shouldComponentUpdate:function(){return!this._notifying}};t["default"]=a["default"]([i],o),e.exports=t["default"]},function(e,t,n){"use strict";var r=function(e,t,n,r,o,s,a,i){if(!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var u=[n,r,o,s,a,i],p=0;l=new Error("Invariant Violation: "+t.replace(/%s/g,function(){return u[p++]}))}throw l.framesToPop=1,l}};e.exports=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,n){return function(r,o,s){return void 0!==r[o]?r[e]?t&&t(r,o,n):new Error("You have provided a `"+o+"` prop to `"+n+"` without an `"+e+"` handler. This will render a read-only field. If the field should be mutable use `"+l(o)+"`. Otherwise, set `"+e+"`"):void 0}}function s(e,t,n){var r={};return r}function a(e){return 0===v[0]&&v[1]>=13?e:e.type}function i(e){return"value"===e?"valueLink":"checked"===e?"checkedLink":null}function l(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function u(e,t,n){return function(){for(var r=arguments.length,o=Array(r),s=0;r>s;s++)o[s]=arguments[s];t&&t.call.apply(t,[e].concat(o)),n&&n.call.apply(n,[e].concat(o))}}function p(e,t,n){return d(e,t.bind(null,n=n||(Array.isArray(e)?[]:{}))),n}function d(e,t,n){if(Array.isArray(e))return e.forEach(t,n);for(var r in e)f(e,r)&&t.call(n,e[r],r,e)}function f(e,t){return e?Object.prototype.hasOwnProperty.call(e,t):!1}t.__esModule=!0,t.customPropType=o,t.uncontrolledPropTypes=s,t.getType=a,t.getLinkName=i,t.defaultKey=l,t.chain=u,t.transform=p,t.each=d,t.has=f;var c=n(1),h=r(c),m=n(231),v=(r(m),h["default"].version.split(".").map(parseFloat));t.version=v}])});
-//# sourceMappingURL=react-bootstrap.min.js.map \ No newline at end of file
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 55c729476..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">
@@ -31,13 +36,11 @@
<link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
- <script src="/static/js/perfect-scrollbar-0.6.3.jquery.js"></script>
+ <script src="/static/js/perfect-scrollbar-0.6.5.jquery.js"></script>
<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 9cb81226b..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
@@ -643,12 +642,199 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request)
func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !c.IsSystemAdmin() {
- c.Err = model.NewAppError("adminConsole", "You do not have permission to access the admin console.", "")
- c.Err.StatusCode = http.StatusForbidden
+ if !c.HasSystemAdminPermissions("adminConsole") {
+ return
+ }
+
+ 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 {
- page := NewHtmlTemplatePage("admin_console", "Admin Console")
- page.Render(c, w)
+ 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