diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-03-14 00:56:37 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-03-14 00:56:37 -0400 |
commit | 975159b52ac6bf0ee6b2d5b73d2391b2e0c6e165 (patch) | |
tree | a0852f9faf6de9962b5478ed3f1bbaccd71062c0 | |
parent | 160e1a8bea6d8280ef3b41a4a9f80674b75f3eb0 (diff) | |
parent | d7cdcf082fab6c0cb7c2fe4bed821bd1a8000e69 (diff) | |
download | chat-975159b52ac6bf0ee6b2d5b73d2391b2e0c6e165.tar.gz chat-975159b52ac6bf0ee6b2d5b73d2391b2e0c6e165.tar.bz2 chat-975159b52ac6bf0ee6b2d5b73d2391b2e0c6e165.zip |
Merge pull request #2331 from mattermost/plt-1680
PLT-1680 Converting client to react-router. Stage 1
183 files changed, 3426 insertions, 5084 deletions
@@ -127,10 +127,9 @@ package: cp -RL web/static/help $(DIST_PATH)/web/static cp -RL web/static/images $(DIST_PATH)/web/static cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/ - cp -RL web/templates $(DIST_PATH)/web + cp -RL templates $(DIST_PATH) mkdir -p $(DIST_PATH)/api - cp -RL api/templates $(DIST_PATH)/api cp -RL i18n $(DIST_PATH) cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH) @@ -140,17 +139,17 @@ package: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - rm $(DIST_PATH)/web/templates/*.bak + sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + rm $(DIST_PATH)/templates/*.bak sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv' diff --git a/api/api.go b/api/api.go index 4fecd3dd4..20f77e558 100644 --- a/api/api.go +++ b/api/api.go @@ -4,47 +4,15 @@ package api import ( - "bytes" - l4g "github.com/alecthomas/log4go" + "net/http" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "html/template" - "net/http" _ "github.com/cloudfoundry/jibber_jabber" _ "github.com/nicksnyder/go-i18n/i18n" ) -var ServerTemplates *template.Template - -type ServerTemplatePage Page - -func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage { - return &ServerTemplatePage{ - TemplateName: templateName, - Props: make(map[string]string), - Extra: make(map[string]string), - Html: make(map[string]template.HTML), - ClientCfg: utils.ClientCfg, - Locale: locale, - } -} - -func (me *ServerTemplatePage) Render() string { - var text bytes.Buffer - - T := utils.GetUserTranslations(me.Locale) - me.Props["Footer"] = T("api.templates.email_footer") - me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", - map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]})) - - if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { - l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err) - } - - return text.String() -} - func InitApi() { r := Srv.Router.PathPrefix("/api/v1").Subrouter() InitUser(r) @@ -60,12 +28,7 @@ func InitApi() { InitPreference(r) InitLicense(r) - templatesDir := utils.FindDir("api/templates") - l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir) - var err error - if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("api.api.init.parsing_templates.error"), err) - } + utils.InitHTML() } func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool { diff --git a/api/context.go b/api/context.go index edcdcbfef..eed035daf 100644 --- a/api/context.go +++ b/api/context.go @@ -5,11 +5,9 @@ package api import ( "fmt" - "html/template" "net" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -31,33 +29,16 @@ var allowedMethods []string = []string{ } type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string - SessionTokenIndex int64 - T goi18n.TranslateFunc - Locale string -} - -type Page struct { - TemplateName string - Props map[string]string - Extra map[string]string - Html map[string]template.HTML - ClientCfg map[string]string - ClientLicense map[string]string - User *model.User - Team *model.Team - Channel *model.Channel - Preferences *model.Preferences - PostID string - SessionTokenIndex int64 - Locale string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + T goi18n.TranslateFunc + Locale string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - tokens := GetMultiSessionCookieTokens(r) - if len(tokens) > 0 { - // If there is only 1 token in the cookie then just use it like normal - if len(tokens) == 1 { - token = tokens[0] - } else { - // If it is a multi-session token then find the correct session - sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX) - sessionTokenIndex := int64(-1) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } else { - sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } - } - - if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) { - token = tokens[sessionTokenIndex] - c.SessionTokenIndex = sessionTokenIndex - } else { - c.SessionTokenIndex = -1 - } - } - } else { - c.SessionTokenIndex = -1 + if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + token = cookie.Value } } @@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w, r) - c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) - c.Err.StatusCode = http.StatusUnauthorized + if h.requireUser || h.requireSystemAdmin { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } } else if !session.IsOAuth && isTokenFromQueryString { c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token) c.Err.StatusCode = http.StatusUnauthorized @@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool { } func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - - // multiToken := "" - // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - // multiToken = oldMultiCookie.Value - // } - - // multiCookie := &http.Cookie{ - // Name: model.SESSION_COOKIE_TOKEN, - // Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), - // Path: "/", - // MaxAge: model.SESSION_TIME_WEB_IN_SECS, - // HttpOnly: true, - // } - - //http.SetCookie(w, multiCookie) - cookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, Value: "", @@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool { } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { - props := make(map[string]string) - props["Message"] = err.Message - props["Details"] = err.DetailedError + T, locale := utils.GetTranslationsAndLocale(w, r) + page := utils.NewHTMLTemplate("error", locale) + page.Props["Message"] = err.Message + page.Props["Details"] = err.DetailedError pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) > 1 { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] } else { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host } - T, _ := utils.GetTranslationsAndLocale(w, r) - props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - props["Link"] = T("api.templates.error.link") + page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + page.Props["Link"] = T("api.templates.error.link") w.WriteHeader(err.StatusCode) - ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) + if rErr := page.RenderToWriter(w); rErr != nil { + l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error()) + } } func Handle404(w http.ResponseWriter, r *http.Request) { @@ -588,29 +528,6 @@ func GetSession(token string) *model.Session { return session } -func GetMultiSessionCookieTokens(r *http.Request) []string { - if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - multiToken := multiCookie.Value - - if len(multiToken) > 0 { - return strings.Split(multiToken, " ") - } - } - - return []string{} -} - -func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) { - for index, token := range GetMultiSessionCookieTokens(r) { - s := GetSession(token) - if s != nil && !s.IsExpired() && s.TeamId == teamId { - return int64(index), s - } - } - - return -1, nil -} - func AddSessionToCache(session *model.Session) { sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/api/license.go b/api/license.go index 23e7946c8..542b45e26 100644 --- a/api/license.go +++ b/api/license.go @@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) { sr := r.PathPrefix("/license").Subrouter() sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST") sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST") + sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) { rdata["status"] = "ok" w.Write([]byte(model.MapToJson(rdata))) } + +func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) { + config := utils.ClientLicense + + var etag string + if config["IsLicensed"] == "false" { + etag = model.Etag(config["IsLicensed"]) + } else { + etag = model.Etag(config["IsLicensed"], config["IssuedAt"]) + } + + if HandleEtag(etag, w, r) { + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + + w.Write([]byte(model.MapToJson(config))) +} diff --git a/api/license_test.go b/api/license_test.go new file mode 100644 index 000000000..b34aeb7a6 --- /dev/null +++ b/api/license_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" +) + +func TestGetLicenceConfig(t *testing.T) { + Setup() + + if result, err := Client.GetClientLicenceConfig(); err != nil { + t.Fatal(err) + } else { + cfg := result.Data.(map[string]string) + + if _, ok := cfg["IsLicensed"]; !ok { + t.Fatal(cfg) + } + } +} diff --git a/api/oauth.go b/api/oauth.go index 1ae3dbf78..9b7f3699d 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -5,12 +5,15 @@ package api import ( "fmt" + "net/http" + "net/url" + "strconv" + "strings" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" - "net/url" ) func InitOAuth(r *mux.Router) { @@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) { sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + + // Also handle this a the old routes remove soon apiv2? + mr := Srv.Router + mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData { return result.Data.(*model.AuthData) } } + +func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" + + if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_LOGIN: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + } + } +} + +func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + 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.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := utils.NewHTMLTemplate("authorize", c.Locale) + page.Props["Title"] = c.T("web.authorize_oauth.title") + 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 + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) + } +} + +func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") + 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.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := Srv.Store.OAuth().GetApp(clientId) + tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + uchan := Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") + 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.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") + 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 := RevokeAccessToken(accessData.Token); err != nil { + l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) + } + + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") + return + } else { + session = result.Data.(*model.Session) + AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} + + 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())) +} + +func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + loginHint := r.URL.Query().Get("login_hint") + teamName := r.URL.Query().Get("team") + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} + +func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := r.URL.Query().Get("team") + + if !utils.Cfg.TeamSettings.EnableUserCreation { + c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + hash := r.URL.Query().Get("h") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if IsVerifyHashRequired(nil, team, hash) { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) + return + } + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} diff --git a/api/post.go b/api/post.go index cd78b16f0..d0ec5826a 100644 --- a/api/post.go +++ b/api/post.go @@ -419,7 +419,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team // copy the context and create a mock session for posting the message mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false} - newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { @@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * year := fmt.Sprintf("%d", tm.Year()) zone, _ := tm.Zone() - subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale) + subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale) subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, "Month": month[:3], "Day": day, "Year": year}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale) + bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name diff --git a/api/team.go b/api/team.go index 2f680dc76..255982522 100644 --- a/api/team.go +++ b/api/team.go @@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) { sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST") sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") - sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET") + sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") - sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") - sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") // These should be moved to the global admain console sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET") @@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signup_team_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title") bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button") @@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { } if !utils.Cfg.EmailSettings.RequireEmailVerification { - m["follow_link"] = bodyPage.Props["Link"] + m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash)) } w.Header().Set("Access-Control-Allow-Origin", " *") @@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name} w.Write([]byte(model.MapToJson(data))) } @@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool { } func getAll(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasSystemAdminPermissions("getLogs") { - return - } - if result := <-Srv.Store.Team().GetAll(); result.Err != nil { c.Err = result.Err return @@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { m := make(map[string]*model.Team) for _, v := range teams { m[v.Id] = v + if !c.IsSystemAdmin() { + m[v.Id].SanitizeForNotLoggedIn() + } } w.Write([]byte(model.TeamMapToJson(m))) @@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool { return false } -func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - m := make(map[string]*model.Team) - for _, v := range teams { - v.Sanitize() - m[v.Id] = v - } - - w.Write([]byte(model.TeamMapToJson(m))) - } -} - -func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - siteURL := c.GetSiteURL() - subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - - bodyPage := NewServerTemplatePage("find_teams_body", c.Locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title") - bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found") - bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found") - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - } else { - teams := result.Data.([]*model.Team) - - // the template expects Props to be a map with team names as the keys and the team url as the value - props := make(map[string]string) - for _, team := range teams { - props[team.Name] = c.GetTeamURLFromTeam(team) - } - bodyPage.Extra = props - - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.team.email_teams.sending.error"), err) - } - - w.Write([]byte(model.MapToJson(m))) - } -} - func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) if len(invites.Invites) == 0 { @@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = c.T("api.team.invite_members.member") } - subjectPage := NewServerTemplatePage("invite_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("invite_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.invite_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info", @@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { + m := model.MapFromJson(r.Body) + inviteId := m["invite_id"] + + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team := result.Data.(*model.Team) + if !(team.Type == model.TEAM_OPEN) { + c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId) + return + } + + result := map[string]string{} + result["display_name"] = team.DisplayName + result["name"] = team.Name + result["id"] = team.Id + w.Write([]byte(model.MapToJson(result))) + } +} diff --git a/api/team_test.go b/api/team_test.go index c942e2e1f..bbbc8385d 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -108,49 +108,36 @@ func TestCreateTeam(t *testing.T) { } } -func TestFindTeamByEmail(t *testing.T) { +func TestGetAllTeams(t *testing.T) { Setup() - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - if r1, err := Client.FindTeams(user.Email); err != nil { + Client.LoginByEmail(team.Name, user.Email, "pwd") + + enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing + defer func() { + *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks + }() + *utils.Cfg.TeamSettings.EnableTeamListing = true + + if r1, err := Client.GetAllTeams(); err != nil { t.Fatal(err) } else { teams := r1.Data.(map[string]*model.Team) if teams[team.Id].Name != team.Name { t.Fatal() } - if teams[team.Id].DisplayName != team.DisplayName { - t.Fatal() + if teams[team.Id].Email != "" { + t.Fatal("Non admin users shoudn't get full listings") } } - if _, err := Client.FindTeams("missing"); err != nil { - t.Fatal(err) - } -} - -func TestGetAllTeams(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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.GetAllTeams(); err == nil { - t.Fatal("you shouldn't have permissions") - } - c := &Context{} c.RequestId = model.NewId() c.IpAddress = "cmd_line" @@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) { if teams[team.Id].Name != team.Name { t.Fatal() } + if teams[team.Id].Email != team.Email { + t.Fatal() + } } } @@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) { Client.ClearOAuthToken() } -/* - -XXXXXX investigate and fix failing test - -func TestFindTeamByDomain(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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if val { - t.Fatal("shouldn't be a valid domain") - } - } -} - -*/ - -func TestFindTeamByEmailSend(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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.FindTeamsSendEmail(user.Email); err != nil { - t.Fatal(err) - } else { - } - - if _, err := Client.FindTeamsSendEmail("missing"); err != nil { - - // It should actually succeed at sending the email since it doesn't exist - if !strings.Contains(err.DetailedError, "Failed to add to email address") { - t.Fatal(err) - } - } -} - func TestInviteMembers(t *testing.T) { Setup() diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html deleted file mode 100644 index afabc2191..000000000 --- a/api/templates/email_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html deleted file mode 100644 index 4fc4f4846..000000000 --- a/api/templates/email_change_verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/error.html b/api/templates/error.html deleted file mode 100644 index 2f588aead..000000000 --- a/api/templates/error.html +++ /dev/null @@ -1,37 +0,0 @@ -<html> -<head> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <title><span class='fa fa-chevron-left'></span>Back - 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/favicon-16x16.png" type="image/x-icon"> - <link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" 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>{{.Props.Title}}</h2> - <p>{{ .Props.Message }}</p> - <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a> - </div> - </div> -</body> -<script> - var details = "{{ .Details }}"; - if (details.length > 0) { - console.log("error details: " + details); - } -</script> -</html> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html deleted file mode 100644 index 1324091aa..000000000 --- a/api/templates/find_teams_body.html +++ /dev/null @@ -1,52 +0,0 @@ -{{define "find_teams_body"}} - -<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> - <tr> - <td> - <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> - <tr> - <td style="border: 1px solid #ddd;"> - <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="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt=""> - </td> - </tr> - <tr> - <td> - <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;">{{.Props.Title}}</h2> - <p>{{ if .Extra }} - {{.Props.Found}}<br> - {{range $index, $element := .Extra}} - <a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br> - {{ end }} - {{ else }} - {{.Props.NotFound}} - {{ end }} - </p> - </td> - </tr> - <tr> - {{template "email_info" . }} - </tr> - </table> - </td> - </tr> - <tr> - {{template "email_footer" . }} - </tr> - </table> - </td> - </tr> - </table> - </td> - </tr> -</table> - -{{end}} - - - diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html deleted file mode 100644 index ebc339562..000000000 --- a/api/templates/find_teams_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "find_teams_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html deleted file mode 100644 index 60daaa432..000000000 --- a/api/templates/post_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/user.go b/api/user.go index b7e6220d8..0841c38aa 100644 --- a/api/user.go +++ b/api/user.go @@ -53,10 +53,13 @@ func InitUser(r *mux.Router) { sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") + sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") @@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) - bodyPage := NewServerTemplatePage("welcome_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") @@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN bodyPage.Props["TeamURL"] = teamURL if !verified { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) bodyPage.Props["VerifyUrl"] = link } @@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) { func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") @@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, w.Header().Set(model.HEADER_TOKEN, session.Token) - tokens := GetMultiSessionCookieTokens(r) - multiToken := "" - seen := make(map[string]string) - seen[session.TeamId] = session.TeamId - for _, token := range tokens { - s := GetSession(token) - if s != nil && !s.IsExpired() && seen[s.TeamId] == "" { - multiToken += " " + token - seen[s.TeamId] = s.TeamId - } - } - - multiToken = strings.TrimSpace(multiToken + " " + session.Token) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) - - multiSessionCookie := &http.Cookie{ + sessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, - Value: multiToken, + Value: session.Token, Path: "/", MaxAge: maxAge, Expires: expiresAt, HttpOnly: true, } - http.SetCookie(w, multiSessionCookie) + http.SetCookie(w, sessionCookie) c.Session = *session c.LogAuditWithUserId(user.Id, "success") @@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) { + data := make(map[string]string) + data["logged_in"] = "false" + data["team_name"] = "" + + if len(c.Session.UserId) != 0 { + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + } + data["logged_in"] = "true" + data["team_name"] = team.Name + } + w.Write([]byte(model.MapToJson(data))) +} + func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] @@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) - link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) + link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") - bodyPage := NewServerTemplatePage("reset_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.reset_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info")) @@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("password_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", @@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", @@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) - subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title") bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info", @@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string props["team"] = teamName state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) + redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signin_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", @@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, }() } + +func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["uid"] + if len(userId) != 26 { + c.SetInvalidParam("verifyEmail", "uid") + return + } + + hashedId := props["hid"] + if len(hashedId) == 0 { + c.SetInvalidParam("verifyEmail", "hid") + return + } + + if model.ComparePassword(hashedId, userId) { + if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { + return + } else { + c.LogAudit("Email Verified") + return + } + } + + c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden +} + +func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("resendVerification", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("resendVerification", "email") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) + + if user.LastActivityAt > 0 { + SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } else { + SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } + } +} diff --git a/api/user_test.go b/api/user_test.go index 1a1cf9634..27f00829f 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) { t.Fatal("should have failed - wrong user") } } + +func TestMeLoggedIn(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()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.AuthToken = "invalid" + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" { + t.Fatal("Got: " + val) + } + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" { + t.Fatal("Got: " + val) + } + } +} diff --git a/i18n/en.json b/i18n/en.json index bc33fc019..d16de7dbb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1624,6 +1624,10 @@ "translation": "Couldn't upload profile image" }, { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Bad verify email link." + }, + { "id": "api.web_conn.new_web_conn.last_activity.error", "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" }, @@ -3400,22 +3404,6 @@ "translation": "Find Team" }, { - "id": "web.footer.about", - "translation": "About" - }, - { - "id": "web.footer.help", - "translation": "Help" - }, - { - "id": "web.footer.privacy", - "translation": "Privacy" - }, - { - "id": "web.footer.terms", - "translation": "Terms" - }, - { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: Bad client_id" }, @@ -3548,10 +3536,6 @@ "translation": "Home" }, { - "id": "web.root.singup_info", - "translation": "All team communication in one place, searchable and accessible anywhere" - }, - { "id": "web.root.singup_title", "translation": "Signup" }, @@ -3606,5 +3590,9 @@ { "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Invite is invalid because this is not an open team." } ] diff --git a/i18n/es.json b/i18n/es.json index 4c0c1fd03..93ffb2341 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -3400,22 +3400,6 @@ "translation": "Encontrar Equipo" }, { - "id": "web.footer.about", - "translation": "Acerca" - }, - { - "id": "web.footer.help", - "translation": "Ayuda" - }, - { - "id": "web.footer.privacy", - "translation": "Privacidad" - }, - { - "id": "web.footer.terms", - "translation": "Términos" - }, - { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: client_id malo" }, @@ -3548,10 +3532,6 @@ "translation": "Inicio" }, { - "id": "web.root.singup_info", - "translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" - }, - { "id": "web.root.singup_title", "translation": "Registrar" }, diff --git a/model/client.go b/model/client.go index 560e47b76..3adcb980d 100644 --- a/model/client.go +++ b/model/client.go @@ -16,19 +16,17 @@ import ( ) const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" - SESSION_TOKEN_INDEX = "session_token_index" - API_URL_SUFFIX = "/api/v1" + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -179,29 +177,6 @@ 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.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { - return nil, err - } else { - - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); 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) InviteMembers(invites *Invites) (*Result, *AppError) { if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err @@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (* } 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 { + if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER } + +func (c *Client) GetClientLicenceConfig() (*Result, *AppError) { + if r, err := c.DoApiGet("/license/client_config", "", ""); 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) GetMeLoggedIn() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} diff --git a/model/session.go b/model/session.go index 5d9424d64..bf0d9531e 100644 --- a/model/session.go +++ b/model/session.go @@ -9,7 +9,7 @@ import ( ) const ( - SESSION_COOKIE_TOKEN = "MMTOKEN" + SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" SESSION_PROP_OS = "os" diff --git a/model/team.go b/model/team.go index 9e9eaa25f..bed7bbd8d 100644 --- a/model/team.go +++ b/model/team.go @@ -232,3 +232,10 @@ func (o *Team) Sanitize() { o.Email = "" o.AllowedDomains = "" } + +func (o *Team) SanitizeForNotLoggedIn() { + o.Email = "" + o.AllowedDomains = "" + o.CompanyName = "" + o.InviteId = "" +} diff --git a/web/templates/authorize.html b/templates/authorize.html index 0fa36b0ab..0fa36b0ab 100644 --- a/web/templates/authorize.html +++ b/templates/authorize.html diff --git a/api/templates/email_change_body.html b/templates/email_change_body.html index 41b1bcd7d..41b1bcd7d 100644 --- a/api/templates/email_change_body.html +++ b/templates/email_change_body.html diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html new file mode 100644 index 000000000..540bc6eab --- /dev/null +++ b/templates/email_change_subject.html @@ -0,0 +1 @@ +{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 0d0c0aaba..0d0c0aaba 100644 --- a/api/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html new file mode 100644 index 000000000..04da7593c --- /dev/null +++ b/templates/email_change_verify_subject.html @@ -0,0 +1 @@ +{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_footer.html b/templates/email_footer.html index 6dc7fa483..6dc7fa483 100644 --- a/api/templates/email_footer.html +++ b/templates/email_footer.html diff --git a/api/templates/email_info.html b/templates/email_info.html index 0a34f18a0..0a34f18a0 100644 --- a/api/templates/email_info.html +++ b/templates/email_info.html diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 000000000..b86039ca3 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,24 @@ +{{define "error"}} +<!DOCTYPE html> +<html> +{{template "head" . }} +<body class="white error"> + <div class="container-fluid"> + <div class="error__container"> + <div class="error__icon"> + <i class="fa fa-exclamation-triangle"/> + </div> + <h2>{{.Props.Title}}</h2> + <p>{{ .Props.Message }}</p> + <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a> + </div> + </div> +</body> +<script> +var details = {{ .Props.Details }}; +if (details.length > 0) { + console.log("error details: " + details); +} +</script> +</html> +{{end}} diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 000000000..a7eacc85f --- /dev/null +++ b/templates/head.html @@ -0,0 +1,92 @@ +{{define "head"}} +<head> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> + <meta name="robots" content="noindex, nofollow"> + <meta name="referrer" content="no-referrer"> + + <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="{{ .Props.Title }}"> + <meta name="application-name" content="{{ .Props.Title }}"> + <meta name="format-detection" content="telephone=no"> + <!-- iOS add to homescreen --> + + <!-- Android add to homescreen --> + <link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png"> + <link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png"> + <link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png"> + <link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png"> + <link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png"> + <link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png"> + <link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png"> + <link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png"> + <link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png"> + <link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png"> + <link rel="manifest" href="/static/config/manifest.json"> + <!-- Android add to homescreen --> + + <!-- CSS Should always go first --> + <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> + <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css"> + <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css"> + <link rel="stylesheet" href="/static/css/styles.css"> + <link rel="stylesheet" href="/static/css/google-fonts.css"> + <link rel="stylesheet" href="/static/css/katex.min.css"> + <link rel="stylesheet" class="code_theme" href=""> + + <script src="/static/js/intl-1.0.0/Intl.js"></script> + <script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script> + <script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script> + <script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script> + + <script src="/static/js/react-0.14.3.js"></script> + <script src="/static/js/react-dom-0.14.3.js"></script> + <script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script> + <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script> + <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script> + <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script> + <script src="/static/js/jquery-2.1.4.js"></script> + <script src="/static/js/bootstrap-3.3.5.js"></script> + <script src="/static/js/bootstrap-colorpicker.min.js"></script> + <script src="/static/js/react-bootstrap-0.28.1.js"></script> + <script src="/static/js/velocity.min.js"></script> + <script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script> + <script src="/static/js/jquery-dragster/jquery.dragster.js"></script> + <script src="/static/js/babel-polyfill-6.1.18.min.js"></script> + <script src="/static/js/katex.min.js"></script> + <script src="/static/js/Chart.min.js"></script> + + <style id="antiClickjack">body{display:none !important;}</style> + + <script> + if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) { + Object.keys(ReactIntlLocaleData).forEach(function(lang) { + ReactIntl.addLocaleData(ReactIntlLocaleData[lang]); + }); + } + + $(window).on('beforeunload', function(){ + if (window.SocketStore) { + SocketStore.close(); + } + }); + </script> + + <script src="/static/js/libs.min.js"></script> + <script src="/static/js/bundle.js"></script> + + <script type="text/javascript"> + if (self === top) { + var blocker = document.getElementById("antiClickjack"); + blocker.parentNode.removeChild(blocker); + } + </script> +</head> +{{end}} diff --git a/api/templates/invite_body.html b/templates/invite_body.html index 2b6bde6d3..2b6bde6d3 100644 --- a/api/templates/invite_body.html +++ b/templates/invite_body.html diff --git a/api/templates/invite_subject.html b/templates/invite_subject.html index 504915d50..504915d50 100644 --- a/api/templates/invite_subject.html +++ b/templates/invite_subject.html diff --git a/api/templates/password_change_body.html b/templates/password_change_body.html index 2c4ba10ca..2c4ba10ca 100644 --- a/api/templates/password_change_body.html +++ b/templates/password_change_body.html diff --git a/api/templates/password_change_subject.html b/templates/password_change_subject.html index 897f1210d..897f1210d 100644 --- a/api/templates/password_change_subject.html +++ b/templates/password_change_subject.html diff --git a/api/templates/post_body.html b/templates/post_body.html index 54f34d1dd..54f34d1dd 100644 --- a/api/templates/post_body.html +++ b/templates/post_body.html diff --git a/templates/post_subject.html b/templates/post_subject.html new file mode 100644 index 000000000..9789d4142 --- /dev/null +++ b/templates/post_subject.html @@ -0,0 +1 @@ +{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/reset_body.html b/templates/reset_body.html index 69cd44957..69cd44957 100644 --- a/api/templates/reset_body.html +++ b/templates/reset_body.html diff --git a/api/templates/reset_subject.html b/templates/reset_subject.html index a2852d332..a2852d332 100644 --- a/api/templates/reset_subject.html +++ b/templates/reset_subject.html diff --git a/templates/root.html b/templates/root.html new file mode 100644 index 000000000..560c7a4b0 --- /dev/null +++ b/templates/root.html @@ -0,0 +1,12 @@ +{{define "root"}} +<!DOCTYPE html> +<html> +{{template "head" . }} +<body> + <div id='root'/> + <script> + window.setup_root(); + </script> +</body> +</html> +{{end}} diff --git a/api/templates/signin_change_body.html b/templates/signin_change_body.html index af8577f0f..af8577f0f 100644 --- a/api/templates/signin_change_body.html +++ b/templates/signin_change_body.html diff --git a/api/templates/signin_change_subject.html b/templates/signin_change_subject.html index 606dc4df3..606dc4df3 100644 --- a/api/templates/signin_change_subject.html +++ b/templates/signin_change_subject.html diff --git a/api/templates/signup_team_body.html b/templates/signup_team_body.html index 683a9891e..683a9891e 100644 --- a/api/templates/signup_team_body.html +++ b/templates/signup_team_body.html diff --git a/api/templates/signup_team_subject.html b/templates/signup_team_subject.html index 413a5c8c1..413a5c8c1 100644 --- a/api/templates/signup_team_subject.html +++ b/templates/signup_team_subject.html diff --git a/api/templates/verify_body.html b/templates/verify_body.html index 2b0d25f94..2b0d25f94 100644 --- a/api/templates/verify_body.html +++ b/templates/verify_body.html diff --git a/api/templates/verify_subject.html b/templates/verify_subject.html index ad7fc2aaa..ad7fc2aaa 100644 --- a/api/templates/verify_subject.html +++ b/templates/verify_subject.html diff --git a/api/templates/welcome_body.html b/templates/welcome_body.html index b5ca9beb3..b5ca9beb3 100644 --- a/api/templates/welcome_body.html +++ b/templates/welcome_body.html diff --git a/api/templates/welcome_subject.html b/templates/welcome_subject.html index 95189b900..95189b900 100644 --- a/api/templates/welcome_subject.html +++ b/templates/welcome_subject.html diff --git a/utils/html.go b/utils/html.go new file mode 100644 index 000000000..4203160d5 --- /dev/null +++ b/utils/html.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "bytes" + "html/template" + "net/http" + + l4g "github.com/alecthomas/log4go" + "gopkg.in/fsnotify.v1" +) + +// Global storage for templates +var htmlTemplates *template.Template + +type HTMLTemplate struct { + TemplateName string + Props map[string]string + Html map[string]template.HTML + Locale string +} + +func InitHTML() { + templatesDir := FindDir("templates") + l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) + var err error + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("api.api.init.parsing_templates.error"), err) + } + + // Watch the templates folder for changes. + watcher, err := fsnotify.NewWatcher() + if err != nil { + l4g.Error(T("web.create_dir.error"), err) + } + + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + l4g.Info(T("web.reparse_templates.info"), event.Name) + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("web.parsing_templates.error"), err) + } + } + case err := <-watcher.Errors: + l4g.Error(T("web.dir_fail.error"), err) + } + } + }() + + err = watcher.Add(templatesDir) + if err != nil { + l4g.Error(T("web.watcher_fail.error"), err) + } +} + +func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate { + return &HTMLTemplate{ + TemplateName: templateName, + Props: make(map[string]string), + Html: make(map[string]template.HTML), + Locale: locale, + } +} + +func (t *HTMLTemplate) addDefaultProps() { + T := GetUserTranslations(t.Locale) + t.Props["Footer"] = T("api.templates.email_footer") + t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", + map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName})) +} + +func (t *HTMLTemplate) Render() string { + t.addDefaultProps() + + var text bytes.Buffer + + if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + } + + return text.String() +} + +func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { + t.addDefaultProps() + + if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + return err + } + return nil +} diff --git a/utils/license.go b/utils/license.go index 5c975aec2..b1f15ad92 100644 --- a/utils/license.go +++ b/utils/license.go @@ -20,7 +20,7 @@ import ( var IsLicensed bool = false var License *model.License = &model.License{} -var ClientLicense map[string]string = make(map[string]string) +var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/action_creators/global_actions.jsx index 367347d4b..4375d6c87 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/action_creators/global_actions.jsx @@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) { emitPostRecievedEvent(post); } + +export function loadTeamRequiredPage() { + AsyncClient.getAllTeams(); +} + +export function newLocalizationSelected(locale) { + Client.getTranslations( + locale, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_LOCALE, + locale, + translations: data + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTranslations'); + } + ); +} + +export function viewLoggedIn() { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.getMyTeam(); + AsyncClient.getMe(); + + // Clear pending posts (shouldn't have pending posts if we are loading) + PostStore.clearPendingPosts(); +} diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 95b4caa12..db366f8ed 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal; import LoadingScreen from './loading_screen.jsx'; import * as Utils from '../utils/utils.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; export default class ActivityLogModal extends React.Component { constructor(props) { @@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.firstTime' defaultMessage='First time active: {date}, {time}' values={{ - date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + <FormattedDate + value={firstAccessTime} + day='2-digit' + month='long' + year='numeric' + /> + ), + time: ( + <FormattedTime + value={firstAccessTime} + hour='2-digit' + minute='2-digit' + /> + ) }} /> </div> @@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.lastActivity' defaultMessage='Last activity: {date}, {time}' values={{ - date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + <FormattedDate + value={lastAccessTime} + day='2-digit' + month='long' + year='numeric' + /> + ), + time: ( + <FormattedTime + value={lastAccessTime} + hour='2-digit' + minute='2-digit' + /> + ) }} /> </div> diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 32ed70a99..4c4f21f08 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx'; import TeamStore from '../../stores/team_store.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import LoadingScreen from '../loading_screen.jsx'; -import * as Utils from '../../utils/utils.jsx'; import EmailSettingsTab from './email_settings.jsx'; import LogSettingsTab from './log_settings.jsx'; @@ -50,11 +49,6 @@ export default class AdminController extends React.Component { selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; - - if (!props.tab) { - var tokenIndex = Utils.getUrlParameter('session_token_index'); - history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`); - } } componentDidMount() { @@ -63,6 +57,9 @@ export default class AdminController extends React.Component { AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); AsyncClient.getAllTeams(); + + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover(); } componentWillUnmount() { @@ -175,7 +172,7 @@ export default class AdminController extends React.Component { } return ( - <div> + <div id='admin_controller'> <div className='sidebar--menu' id='sidebar-menu' diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx index dc0b3c4cb..ae95f5a3a 100644 --- a/web/react/components/admin_console/admin_navbar_dropdown.jsx +++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx @@ -2,13 +2,14 @@ // See License.txt for license information. import * as Utils from '../../utils/utils.jsx'; -import * as Client from '../../utils/client.jsx'; import TeamStore from '../../stores/team_store.jsx'; import Constants from '../../utils/constants.jsx'; import {FormattedMessage} from 'mm-intl'; +import {Link} from 'react-router'; + function getStateFromStores() { return {currentTeam: TeamStore.getCurrent()}; } @@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component { super(props); this.blockToggle = false; - this.handleLogoutClick = this.handleLogoutClick.bind(this); - this.state = getStateFromStores(); } - handleLogoutClick(e) { - e.preventDefault(); - Client.logout(); - } - componentDidMount() { $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { this.blockToggle = true; @@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component { </a> </li> <li> - <a - href='#' - onClick={this.handleLogoutClick} - > + <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}> <FormattedMessage id='admin.nav.logout' defaultMessage='Logout' /> - </a> + </Link> </li> <li className='divider'></li> <li> @@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component { </ul> ); } -}
\ No newline at end of file +} diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 6621e5743..c2f31f569 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -3,7 +3,6 @@ import AdminSidebarHeader from './admin_sidebar_header.jsx'; import SelectTeamModal from './select_team_modal.jsx'; -import * as Utils from '../../utils/utils.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component { handleClick(name, teamId, e) { e.preventDefault(); this.props.selectTab(name, teamId); - var tokenIndex = Utils.getUrlParameter('session_token_index'); - history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`); } isSelected(name, teamId) { @@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component { } teamSelectedModal(teamId) { - this.props.selectedTeams[teamId] = 'true'; this.setState({showSelectModal: false}); this.props.addSelectedTeam(teamId); this.forceUpdate(); diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx index 8c9f74934..f1281c6ee 100644 --- a/web/react/components/admin_console/admin_sidebar_header.jsx +++ b/web/react/components/admin_console/admin_sidebar_header.jsx @@ -3,7 +3,6 @@ import AdminNavbarDropdown from './admin_navbar_dropdown.jsx'; import UserStore from '../../stores/user_store.jsx'; -import * as Utils from '../../utils/utils.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} /> ); } @@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component { </div> ); } -}
\ No newline at end of file +} diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 4af350bcd..7d6cfb5c3 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -366,7 +366,7 @@ export default class UserItem extends React.Component { <td className='row member-div padding--equal'> <img className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} height='36' width='36' /> diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx index 47eee6d3f..917093840 100644 --- a/web/react/components/audit_table.jsx +++ b/web/react/components/audit_table.jsx @@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl'; const holders = defineMessages({ sessionRevoked: { @@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) { } const date = new Date(audit.create_at); - let auditInfo = {}; - auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}); + const auditInfo = {}; + auditInfo.timestamp = ( + <div> + <FormattedDate + value={date} + day='2-digit' + month='short' + year='numeric' + /> + {' - '} + <FormattedTime + value={date} + hour='2-digit' + minute='2-digit' + /> + </div> + ); auditInfo.userId = audit.user_id; auditInfo.desc = auditDesc; auditInfo.ip = audit.ip_address; diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index 2422588cf..2ea840c1e 100644 --- a/web/react/components/center_panel.jsx +++ b/web/react/components/center_panel.jsx @@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component { constructor(props) { super(props); - this.onPreferenceChange = this.onPreferenceChange.bind(this); - this.onChannelChange = this.onChannelChange.bind(this); - this.onUserChange = this.onUserChange.bind(this); + this.getStateFromStores = this.getStateFromStores.bind(this); + this.validState = this.validState.bind(this); + this.onStoresChange = this.onStoresChange.bind(this); + this.state = this.getStateFromStores(); + } + getStateFromStores() { const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); - this.state = { - showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS, + return { + showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS, showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS, user: UserStore.getCurrentUser(), + channel: ChannelStore.getCurrent(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles())) }; } - componentDidMount() { - PreferenceStore.addChangeListener(this.onPreferenceChange); - ChannelStore.addChangeListener(this.onChannelChange); - UserStore.addChangeListener(this.onUserChange); - } - componentWillUnmount() { - PreferenceStore.removeChangeListener(this.onPreferenceChange); - ChannelStore.removeChangeListener(this.onChannelChange); - UserStore.removeChangeListener(this.onUserChange); + validState() { + return this.state.user && this.state.channel && this.state.profiles; } - onPreferenceChange() { - const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); - this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS}); + onStoresChange() { + this.setState(this.getStateFromStores()); } - onChannelChange() { - this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS}); + componentDidMount() { + PreferenceStore.addChangeListener(this.onStoresChange); + ChannelStore.addChangeListener(this.onStoresChange); + UserStore.addChangeListener(this.onStoresChange); } - onUserChange() { - this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))}); + componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onStoresChange); + ChannelStore.removeChangeListener(this.onStoresChange); + UserStore.removeChangeListener(this.onStoresChange); } render() { - const channel = ChannelStore.getCurrent(); + if (!this.validState()) { + return null; + } + const channel = this.state.channel; var handleClick = null; let postsContainer; let createPost; diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 51be13dcf..882c575f0 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component { memberChannel: ChannelStore.getCurrentMember(), users: extraInfo.members, userCount: extraInfo.member_count, - searchVisible: SearchStore.getSearchResults() !== null + searchVisible: SearchStore.getSearchResults() !== null, + currentUser: UserStore.getCurrentUser() }; } + validState() { + if (!this.state.channel || + !this.state.memberChannel || + !this.state.users || + !this.state.userCount || + !this.state.currentUser) { + return false; + } + return true; + } componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addExtraInfoChangeListener(this.onListenerChange); SearchStore.addSearchChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); SearchStore.removeSearchChangeListener(this.onListenerChange); PreferenceStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); } onListenerChange() { const newState = this.getStateFromStores(); @@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component { searchMentions(e) { e.preventDefault(); - const user = this.props.user; + const user = this.state.currentUser; let terms = ''; if (user.notify_props && user.notify_props.mention_keys) { @@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component { }); } render() { - if (this.state.channel === null) { + if (!this.validState()) { return null; } @@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component { </Popover> ); let channelTitle = channel.display_name; - const currentId = this.props.user.id; - const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles); + const currentId = this.state.currentUser.id; + const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles); const isDirect = (this.state.channel.type === 'D'); if (isDirect) { @@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component { <ToggleModalButton role='menuitem' dialogType={ChannelInviteModal} - dialogProps={{channel}} + dialogProps={{channel, currentUser: this.state.currentUser}} > <FormattedMessage id='chanel_header.addMembers' @@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component { <ToggleModalButton role='menuitem' dialogType={ChannelNotificationsModal} - dialogProps={{channel}} + dialogProps={{ + channel, + channelMember: this.state.memberChannel, + currentUser: this.state.currentUser + }} > <FormattedMessage id='channel_header.notificationPreferences' @@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component { } ChannelHeader.propTypes = { - user: React.PropTypes.object.isRequired }; diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index 6c8d51abb..4157812a9 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -4,8 +4,8 @@ import FilteredUserList from './filtered_user_list.jsx'; import LoadingScreen from './loading_screen.jsx'; -import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; @@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl'; const Modal = ReactBootstrap.Modal; export default class ChannelInviteModal extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.onListenerChange = this.onListenerChange.bind(this); this.handleInvite = this.handleInvite.bind(this); - + this.getStateFromStores = this.getStateFromStores.bind(this); this.createInviteButton = this.createInviteButton.bind(this); - // the state gets populated when the modal is shown - this.state = { - loading: true - }; + this.state = this.getStateFromStores(); } shouldComponentUpdate(nextProps, nextState) { if (!this.props.show && !nextProps.show) { @@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component { }; } + const currentUser = UserStore.getCurrentUser(); + if (!currentUser) { + return { + loading: true + }; + } + + const currentMember = ChannelStore.getCurrentMember(); + if (!currentMember) { + return { + loading: true + }; + } + const memberIds = extraInfo.members.map((user) => user.id); var nonmembers = []; @@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component { return { nonmembers, - loading: false + loading: false, + currentUser, + currentMember }; } componentWillReceiveProps(nextProps) { @@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component { UserStore.removeChangeListener(this.onListenerChange); } } + componentWillUnmount() { + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } onListenerChange() { var newState = this.getStateFromStores(); if (!Utils.areObjectsEqual(this.state, newState)) { @@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component { if (Utils.windowHeight() <= 1200) { maxHeight = Utils.windowHeight() - 300; } - content = ( <FilteredUserList style={{maxHeight}} diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx deleted file mode 100644 index e47f2aa50..000000000 --- a/web/react/components/channel_loader.jsx +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -/* This is a special React control with the sole purpose of making all the AsyncClient calls - to the server on page load. This is to prevent other React controls from spamming - AsyncClient with requests. */ - -import * as AsyncClient from '../utils/async_client.jsx'; -import * as Client from '../utils/client.jsx'; -import SocketStore from '../stores/socket_store.jsx'; -import ChannelStore from '../stores/channel_store.jsx'; -import PostStore from '../stores/post_store.jsx'; -import UserStore from '../stores/user_store.jsx'; -import PreferenceStore from '../stores/preference_store.jsx'; - -import * as Utils from '../utils/utils.jsx'; -import Constants from '../utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages} from 'mm-intl'; - -const holders = defineMessages({ - socketError: { - id: 'channel_loader.socketError', - defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.' - }, - someone: { - id: 'channel_loader.someone', - defaultMessage: 'Someone' - }, - posted: { - id: 'channel_loader.posted', - defaultMessage: 'Posted' - }, - uploadedImage: { - id: 'channel_loader.uploadedImage', - defaultMessage: ' uploaded an image' - }, - uploadedFile: { - id: 'channel_loader.uploadedFile', - defaultMessage: ' uploaded a file' - }, - something: { - id: 'channel_loader.something', - defaultMessage: ' did something new' - }, - wrote: { - id: 'channel_loader.wrote', - defaultMessage: ' wrote: ' - }, - connectionError: { - id: 'channel_loader.connection_error', - defaultMessage: 'There appears to be a problem with your internet connection.' - }, - unknownError: { - id: 'channel_loader.unknown_error', - defaultMessage: 'We received an unexpected status code from the server.' - } -}); - -class ChannelLoader extends React.Component { - constructor(props) { - super(props); - - this.intervalId = null; - - this.onSocketChange = this.onSocketChange.bind(this); - - const {formatMessage} = this.props.intl; - SocketStore.setTranslations({ - socketError: formatMessage(holders.socketError), - someone: formatMessage(holders.someone), - posted: formatMessage(holders.posted), - uploadedImage: formatMessage(holders.uploadedImage), - uploadedFile: formatMessage(holders.uploadedFile), - something: formatMessage(holders.something), - wrote: formatMessage(holders.wrote) - }); - - Client.setTranslations({ - connectionError: formatMessage(holders.connectionError), - unknownError: formatMessage(holders.unknownError) - }); - - this.state = {}; - } - componentDidMount() { - /* Initial aysnc loads */ - AsyncClient.getPosts(ChannelStore.getCurrentId()); - AsyncClient.getChannels(); - AsyncClient.getChannelExtraInfo(); - AsyncClient.findTeams(); - AsyncClient.getMyTeam(); - setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit - - /* Perform pending post clean-up */ - PostStore.clearPendingPosts(); - - /* Set up interval functions */ - this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000); - - /* Device tracking setup */ - var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); - if (iOS) { - $('body').addClass('ios'); - } - - /* Set up tracking for whether the window is active */ - window.isActive = true; - - $(window).on('focus', function windowFocus() { - AsyncClient.updateLastViewedAt(); - ChannelStore.resetCounts(ChannelStore.getCurrentId()); - ChannelStore.emitChange(); - window.isActive = true; - }); - - $(window).on('blur', function windowBlur() { - window.isActive = false; - }); - - /* Start global change listeners setup */ - SocketStore.addChangeListener(this.onSocketChange); - - /* Update CSS classes to match user theme */ - var user = UserStore.getCurrentUser(); - - if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { - Utils.applyTheme(user.theme_props); - } else { - Utils.applyTheme(Constants.THEMES.default); - } - - // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx - const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT); - Utils.applyFont(selectedFont); - - $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { - if (ev.type === 'mouseenter') { - $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); - $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); - } else { - $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); - $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); - } - }); - - $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { - if (ev.type === 'mouseenter') { - $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); - $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); - } else { - $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); - $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); - } - }); - - $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { - if (ev.type === 'mouseenter') { - $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); - $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); - } else { - $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); - $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); - } - }); - - /* Prevent backspace from navigating back a page */ - $(window).on('keydown.preventBackspace', (e) => { - if (e.which === 8 && !$(e.target).is('input, textarea')) { - e.preventDefault(); - } - }); - } - componentWillUnmount() { - clearInterval(this.intervalId); - - $(window).off('focus'); - $(window).off('blur'); - - SocketStore.removeChangeListener(this.onSocketChange); - - $('body').off('click.userpopover'); - $('body').off('mouseenter mouseleave', '.post'); - $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); - - $('.modal').off('show.bs.modal'); - - $(window).off('keydown.preventBackspace'); - } - onSocketChange(msg) { - if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { - UserStore.setStatus(msg.user_id, 'online'); - } - } - render() { - return <div/>; - } -} - -ChannelLoader.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(ChannelLoader); diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx index 7048434f8..acefaf024 100644 --- a/web/react/components/channel_notifications_modal.jsx +++ b/web/react/components/channel_notifications_modal.jsx @@ -6,7 +6,6 @@ import SettingItemMin from './setting_item_min.jsx'; import SettingItemMax from './setting_item_max.jsx'; import * as Client from '../utils/client.jsx'; -import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component { constructor(props) { super(props); - this.onListenerChange = this.onListenerChange.bind(this); this.updateSection = this.updateSection.bind(this); this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this); @@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component { this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this); this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this); - const member = ChannelStore.getMember(props.channel.id); this.state = { - notifyLevel: member.notify_props.desktop, - markUnreadLevel: member.notify_props.mark_unread, - channelId: ChannelStore.getCurrentId(), - activeSection: '' + activeSection: '', + notifyLevel: '', + unreadLevel: '' }; } + updateSection(section) { + this.setState({activeSection: section}); + } componentWillReceiveProps(nextProps) { if (!this.props.show && nextProps.show) { - this.onListenerChange(); - ChannelStore.addChangeListener(this.onListenerChange); - } else { - ChannelStore.removeChangeListener(this.onListenerChange); + this.setState({ + notifyLevel: nextProps.channelMember.notify_props.desktop, + unreadLevel: nextProps.channelMember.notify_props.mark_unread + }); } } - onListenerChange() { - const curChannelId = ChannelStore.getCurrentId(); - - if (!curChannelId) { - return; - } - - const newState = {channelId: curChannelId}; - const member = ChannelStore.getMember(curChannelId); - - if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) { - newState.notifyLevel = member.notify_props.desktop; - newState.markUnreadLevel = member.notify_props.mark_unread; - } - - this.setState(newState); - } - updateSection(section) { - this.setState({activeSection: section}); - } handleSubmitNotifyLevel() { - var channelId = this.state.channelId; + var channelId = this.props.channel.id; var notifyLevel = this.state.notifyLevel; - if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) { + if (this.props.channelMember.notify_props.desktop === notifyLevel) { this.updateSection(''); return; } var data = {}; data.channel_id = channelId; - data.user_id = UserStore.getCurrentId(); + data.user_id = this.props.currentUser.id; data.desktop = notifyLevel; + //TODO: This should be moved to event_helpers Client.updateNotifyProps(data, () => { + // YUCK var member = ChannelStore.getMember(channelId); member.notify_props.desktop = notifyLevel; ChannelStore.setChannelMember(member); @@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component { this.setState({notifyLevel}); } createNotifyLevelSection(serverError) { - var handleUpdateSection; - - const user = UserStore.getCurrentUser(); - const globalNotifyLevel = user.notify_props.desktop; - + // Get glabal user setting for notifications + const globalNotifyLevel = this.props.currentUser.notify_props.desktop; let globalNotifyLevelName; if (globalNotifyLevel === 'all') { globalNotifyLevelName = ( @@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component { /> ); + const notificationLevel = this.state.notifyLevel; + if (this.state.activeSection === 'desktop') { - var notifyActive = [false, false, false, false]; - if (this.state.notifyLevel === 'default') { + const notifyActive = [false, false, false, false]; + if (notificationLevel === 'default') { notifyActive[0] = true; - } else if (this.state.notifyLevel === 'all') { + } else if (notificationLevel === 'all') { notifyActive[1] = true; - } else if (this.state.notifyLevel === 'mention') { + } else if (notificationLevel === 'mention') { notifyActive[2] = true; } else { notifyActive[3] = true; @@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component { </div> ); - handleUpdateSection = function updateSection(e) { + const handleUpdateSection = function updateSection(e) { this.updateSection(''); this.onListenerChange(); e.preventDefault(); @@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component { } var describe; - if (this.state.notifyLevel === 'default') { + if (notificationLevel === 'default') { describe = ( <FormattedMessage id='channel_notifications.globalDefault' @@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component { }} /> ); - } else if (this.state.notifyLevel === 'mention') { + } else if (notificationLevel === 'mention') { describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>); - } else if (this.state.notifyLevel === 'all') { + } else if (notificationLevel === 'all') { describe = (<FormattedMessage id='channel_notifications.allActivity'/>); } else { describe = (<FormattedMessage id='channel_notifications.never'/>); } - handleUpdateSection = function updateSection(e) { - this.updateSection('desktop'); - e.preventDefault(); - }.bind(this); - return ( <SettingItemMin title={sendDesktop} describe={describe} - updateSection={handleUpdateSection} + updateSection={() => { + this.updateSection('desktop'); + }} /> ); } handleSubmitMarkUnreadLevel() { - const channelId = this.state.channelId; - const markUnreadLevel = this.state.markUnreadLevel; + const channelId = this.props.channel.id; + const markUnreadLevel = this.state.unreadLevel; - if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) { + if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) { this.updateSection(''); return; } const data = { channel_id: channelId, - user_id: UserStore.getCurrentId(), + user_id: this.props.currentUser.id, mark_unread: markUnreadLevel }; + //TODO: This should be fixed, moved to event_helpers Client.updateNotifyProps(data, () => { + // Yuck... var member = ChannelStore.getMember(channelId); member.notify_props.mark_unread = markUnreadLevel; ChannelStore.setChannelMember(member); @@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component { ); } - handleUpdateMarkUnreadLevel(markUnreadLevel) { - this.setState({markUnreadLevel}); + handleUpdateMarkUnreadLevel(unreadLevel) { + this.setState({unreadLevel}); } createMarkUnreadLevelSection(serverError) { @@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component { <label> <input type='radio' - checked={this.state.markUnreadLevel === 'all'} + checked={this.state.unreadLevel === 'all'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')} /> <FormattedMessage @@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component { <label> <input type='radio' - checked={this.state.markUnreadLevel === 'mention'} + checked={this.state.unreadLevel === 'mention'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')} /> <FormattedMessage id='channel_notifications.onlyMentions'/> @@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component { } else { let describe; - if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') { + if (!this.state.unreadLevel || this.state.unreadLevel === 'all') { describe = ( <FormattedMessage id='channel_notifications.allUnread' @@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component { ChannelNotificationsModal.propTypes = { show: React.PropTypes.bool.isRequired, onHide: React.PropTypes.func.isRequired, - channel: React.PropTypes.object.isRequired + channel: React.PropTypes.object.isRequired, + channelMember: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired }; diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx index 9c4131292..76744d6d7 100644 --- a/web/react/components/channel_view.jsx +++ b/web/react/components/channel_view.jsx @@ -2,34 +2,11 @@ // See License.txt for license information. import CenterPanel from '../components/center_panel.jsx'; -import Sidebar from '../components/sidebar.jsx'; -import SidebarRight from '../components/sidebar_right.jsx'; -import SidebarRightMenu from '../components/sidebar_right_menu.jsx'; export default class ChannelView extends React.Component { render() { return ( - <div className='container-fluid'> - <div - className='sidebar--right' - id='sidebar-right' - > - <SidebarRight/> - </div> - <div - className='sidebar--menu' - id='sidebar-menu' - > - <SidebarRightMenu/> - </div> - <div - className='sidebar--left' - id='sidebar-left' - > - <Sidebar/> - </div> - <CenterPanel/> - </div> + <CenterPanel/> ); } } @@ -37,4 +14,5 @@ ChannelView.defaultProps = { }; ChannelView.propTypes = { + params: React.PropTypes.object }; diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx index 5b3b584ee..42fd8dafa 100644 --- a/web/react/components/claim/claim_account.jsx +++ b/web/react/components/claim/claim_account.jsx @@ -3,6 +3,7 @@ import EmailToSSO from './email_to_sso.jsx'; import SSOToEmail from './sso_to_email.jsx'; +import TeamStore from '../../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component { constructor(props) { super(props); + this.onTeamChange = this.onTeamChange.bind(this); + this.updateStateFromStores = this.updateStateFromStores.bind(this); + this.state = {}; } + componentWillMount() { + this.setState({ + email: this.props.location.query.email, + newType: this.props.location.query.new_type, + oldType: this.props.location.query.old_type, + teamName: this.props.params.team, + teamDisplayName: '' + }); + this.updateStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + updateStateFromStores() { + const team = TeamStore.getByName(this.state.teamName); + let displayName = ''; + if (team) { + displayName = team.displayName; + } + this.setState({ + teamDisplayName: displayName + }); + } + onTeamChange() { + this.updateStateFromStores(); + } render() { + if (this.state.teamDisplayName === '') { + return (<div/>); + } let content; - if (this.props.email === '') { + if (this.state.email === '') { content = ( <p> <FormattedMessage @@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component { /> </p> ); - } else if (this.props.currentType === '' && this.props.newType !== '') { + } else if (this.state.oldType === '' && this.state.newType !== '') { content = ( <EmailToSSO - email={this.props.email} - type={this.props.newType} - teamName={this.props.teamName} - teamDisplayName={this.props.teamDisplayName} + email={this.state.email} + type={this.state.newType} + teamName={this.state.teamName} + teamDisplayName={this.state.teamDisplayName} /> ); } else { content = ( <SSOToEmail - email={this.props.email} - currentType={this.props.currentType} - teamName={this.props.teamName} - teamDisplayName={this.props.teamDisplayName} + email={this.state.email} + currentType={this.state.oldType} + teamName={this.state.teamName} + teamDisplayName={this.state.teamDisplayName} /> ); } - return content; + return ( + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <div id='claim'> + {content} + </div> + </div> + </div> + </div> + ); } } ClaimAccount.defaultProps = { }; ClaimAccount.propTypes = { - currentType: React.PropTypes.string.isRequired, - newType: React.PropTypes.string.isRequired, - email: React.PropTypes.string.isRequired, - teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx index 74137082a..a16efb57b 100644 --- a/web/react/components/claim/sso_to_email.jsx +++ b/web/react/components/claim/sso_to_email.jsx @@ -159,7 +159,7 @@ SSOToEmail.propTypes = { currentType: React.PropTypes.string.isRequired, email: React.PropTypes.string.isRequired, teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + teamDisplayName: React.PropTypes.string }; export default injectIntl(SSOToEmail); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 62319b1a7..69cc74842 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import * as Utils from '../utils/utils.jsx'; @@ -165,7 +165,7 @@ class CreatePost extends React.Component { const channel = ChannelStore.get(this.state.channelId); - EventHelpers.emitUserPostedEvent(post); + GlobalActions.emitUserPostedEvent(post); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); Client.createPost(post, channel, @@ -177,7 +177,7 @@ class CreatePost extends React.Component { member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); - EventHelpers.emitPostRecievedEvent(data); + GlobalActions.emitPostRecievedEvent(data); }, (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index d9113bc9f..70e7a67a8 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; + export default class DeleteChannelModal extends React.Component { constructor(props) { super(props); @@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component { return; } + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); Client.deleteChannel( this.props.channel.id, () => { AsyncClient.getChannels(true); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; }, (err) => { AsyncClient.dispatchError(err, 'handleDelete'); diff --git a/web/react/components/do_verify_email.jsx b/web/react/components/do_verify_email.jsx new file mode 100644 index 000000000..df98bf463 --- /dev/null +++ b/web/react/components/do_verify_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; +import LoadingScreen from './loading_screen.jsx'; + +import {browserHistory} from 'react-router'; + +export default class DoVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.state = { + verifyStatus: 'pending', + serverError: '' + }; + } + componentWillMount() { + const uid = this.props.location.query.uid; + const hid = this.props.location.query.hid; + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + Client.verifyEmail( + () => { + browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email); + }, + (err) => { + this.setState({verifyStatus: 'failure', serverError: err.message}); + }, + uid, + hid + ); + } + render() { + if (this.state.verifyStatus !== 'failure') { + return (<LoadingScreen/>); + } + + return ( + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3> + <FormattedMessage + id='email_verify.almost' + defaultMessage='{siteName}: You are almost done' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </h3> + <div> + <p> + <FormattedMessage id='email_verify.verifyFailed'/> + </p> + <p className='alert alert-danger'> + <i className='fa fa-times'/> + {this.state.serverError} + </p> + </div> + </div> + </div> + </div> + ); + } +} + +DoVerifyEmail.defaultProps = { +}; +DoVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx deleted file mode 100644 index 6d3a109c2..000000000 --- a/web/react/components/docs.jsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as TextFormatting from '../utils/text_formatting.jsx'; -import UserStore from '../stores/user_store.jsx'; - -export default class Docs extends React.Component { - constructor(props) { - super(props); - UserStore.setCurrentUser(global.window.mm_user || {}); - - this.state = {text: ''}; - const errorState = {text: '## 404'}; - - if (props.site) { - $.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => { - this.setState({text: response}); - }, () => { - this.setState(errorState); - }); - } else { - this.setState(errorState); - } - } - - render() { - return ( - <div - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}} - > - </div> - ); - } -} - -Docs.defaultProps = { - site: '' -}; -Docs.propTypes = { - site: React.PropTypes.string -}; diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 380ca7bde..f02239fcf 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,7 +3,7 @@ import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Textbox from './textbox.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import PostStore from '../stores/post_store.jsx'; @@ -45,7 +45,7 @@ class EditPostModal extends React.Component { delete tempState.editText; BrowserStore.setItem('edit_state_transfer', tempState); $('#edit_post').modal('hide'); - EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); + GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); return; } diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx deleted file mode 100644 index 702a20eba..000000000 --- a/web/react/components/email_verify.jsx +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -export default class EmailVerify extends React.Component { - constructor(props) { - super(props); - - this.handleResend = this.handleResend.bind(this); - - this.state = {}; - } - handleResend() { - const newAddress = window.location.href.replace('&resend_success=true', ''); - window.location.href = newAddress + '&resend=true'; - } - render() { - var title = ''; - var body = ''; - var resend = ''; - var resendConfirm = ''; - if (this.props.isVerified === 'true') { - title = ( - <FormattedMessage - id='email_verify.verified' - defaultMessage='{siteName} Email Verified' - values={{ - siteName: global.window.mm_config.SiteName - }} - /> - ); - body = ( - <FormattedHTMLMessage - id='email_verify.verifiedBody' - defaultMessage='<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>' - values={{ - url: this.props.teamURL + '?email=' + this.props.userEmail - }} - /> - ); - } else { - title = ( - <FormattedMessage - id='email_verify.almost' - defaultMessage='{siteName}: You are almost done' - values={{ - siteName: global.window.mm_config.SiteName - }} - /> - ); - body = ( - <p> - <FormattedMessage - id='email_verify.notVerifiedBody' - defaultMessage='Please verify your email address. Check your inbox for an email.' - /> - </p> - ); - resend = ( - <button - onClick={this.handleResend} - className='btn btn-primary' - > - <FormattedMessage - id='email_verify.resend' - defaultMessage='Resend Email' - /> - </button> - ); - if (this.props.resendSuccess) { - resendConfirm = ( - <div><br/><p className='alert alert-success'><i className='fa fa-check'></i> - <FormattedMessage - id='email_verify.sent' - defaultMessage=' Verification email sent.' - /> - </p></div>); - } - } - - return ( - <div className='col-sm-12'> - <div className='signup-team__container'> - <h3>{title}</h3> - <div> - {body} - {resend} - {resendConfirm} - </div> - </div> - </div> - ); - } -} - -EmailVerify.defaultProps = { - isVerified: 'false', - teamURL: '', - userEmail: '', - resendSuccess: 'false' -}; -EmailVerify.propTypes = { - isVerified: React.PropTypes.string, - teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string, - resendSuccess: React.PropTypes.string -}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 2f6067b86..8abcac8c3 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -43,7 +43,7 @@ class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { + $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -114,7 +114,7 @@ class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); } } removeBackgroundImage(name) { diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx deleted file mode 100644 index 3ff9787ad..000000000 --- a/web/react/components/find_team.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -var holders = defineMessages({ - submitError: { - id: 'find_team.submitError', - defaultMessage: 'Please enter a valid email address' - }, - placeholder: { - id: 'find_team.placeholder', - defaultMessage: 'you@domain.com' - } -}); - -class FindTeam extends React.Component { - constructor(props) { - super(props); - this.state = {}; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - - var state = { }; - - var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!email || !utils.isEmail(email)) { - state.email_error = this.props.intl.formatMessage(holders.submitError); - this.setState(state); - return; - } - - state.email_error = ''; - - client.findTeamsSendEmail(email, - function success() { - state.sent = true; - this.setState(state); - }.bind(this), - function fail(err) { - state.email_error = err.message; - this.setState(state); - }.bind(this) - ); - } - - render() { - var emailError = null; - var emailErrorClass = 'form-group'; - - if (this.state.email_error) { - emailError = <label className='control-label'>{this.state.email_error}</label>; - emailErrorClass = 'form-group has-error'; - } - - if (this.state.sent) { - return ( - <div> - <h4> - <FormattedMessage - id='find_team.findTitle' - defaultMessage='Find Your Team' - /> - </h4> - <p> - <FormattedMessage - id='find_team.findDescription' - defaultMessage='An email was sent with links to any teams to which you are a member.' - /> - </p> - </div> - ); - } - - return ( - <div> - <h4> - <FormattedMessage - id='find_team.findTitle' - defaultMessage='Find Your Team' - /> - </h4> - <form onSubmit={this.handleSubmit}> - <p> - <FormattedMessage - id='find_team.getLinks' - defaultMessage='Get an email with links to any teams to which you are a member.' - /> - </p> - <div className='form-group'> - <label className='control-label'> - <FormattedMessage - id='find_team.email' - defaultMessage='Email' - /> - </label> - <div className={emailErrorClass}> - <input - type='text' - ref='email' - className='form-control' - placeholder={this.props.intl.formatMessage(holders.placeholder)} - maxLength='128' - spellCheck='false' - /> - {emailError} - </div> - </div> - <button - className='btn btn-md btn-primary' - type='submit' - > - <FormattedMessage - id='find_team.send' - defaultMessage='Send' - /> - </button> - </form> - </div> - ); - } -} - -FindTeam.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(FindTeam);
\ No newline at end of file diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 184ba1357..71cd5b8b6 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as Client from '../utils/client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ModalStore from '../stores/modal_store.jsx'; import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component { showGetTeamInviteLinkModal() { this.handleHide(false); - EventHelpers.showGetTeamInviteLinkModal(); + GlobalActions.showGetTeamInviteLinkModal(); } render() { diff --git a/web/react/components/logged_in.jsx b/web/react/components/logged_in.jsx new file mode 100644 index 000000000..1ed3694e9 --- /dev/null +++ b/web/react/components/logged_in.jsx @@ -0,0 +1,224 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as AsyncClient from '../utils/async_client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import UserStore from '../stores/user_store.jsx'; +import SocketStore from '../stores/socket_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; +import ErrorBar from '../components/error_bar.jsx'; + +import {browserHistory} from 'react-router'; + +import SidebarRight from '../components/sidebar_right.jsx'; +import SidebarRightMenu from '../components/sidebar_right_menu.jsx'; + +// Modals +import GetPostLinkModal from '../components/get_post_link_modal.jsx'; +import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; +import EditPostModal from '../components/edit_post_modal.jsx'; +import DeletePostModal from '../components/delete_post_modal.jsx'; +import MoreChannelsModal from '../components/more_channels.jsx'; +import TeamSettingsModal from '../components/team_settings_modal.jsx'; +import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; +import RegisterAppModal from '../components/register_app_modal.jsx'; +import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; +import InviteMemberModal from '../components/invite_member_modal.jsx'; +import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; + +const CLIENT_STATUS_INTERVAL = 30000; +const BACKSPACE_CHAR = 8; + +export default class LoggedIn extends React.Component { + constructor(params) { + super(params); + + this.onUserChanged = this.onUserChanged.bind(this); + } + onUserChanged() { + // Grab the current user + const user = UserStore.getCurrentUser(); + + // Update segment indentify + if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') { + global.window.analytics.identify(user.id, { + name: user.nickname, + email: user.email, + createdAt: user.create_at, + username: user.username, + team_id: user.team_id, + id: user.id + }); + } + + // Update CSS classes to match user theme + if (user) { + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + Utils.applyTheme(user.theme_props); + } else { + Utils.applyTheme(Constants.THEMES.default); + } + } + } + onSocketChange(msg) { + if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { + UserStore.setStatus(msg.user_id, 'online'); + } + } + componentWillMount() { + // Emit view action + GlobalActions.viewLoggedIn(); + + // Listen for user + UserStore.addChangeListener(this.onUserChanged); + + // Add listner for socker store + SocketStore.addChangeListener(this.onSocketChange); + + // Get all statuses regularally. (Soon to be switched to websocket) + this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL); + + // Force logout of all tabs if one tab is logged out + $(window).bind('storage', (e) => { + // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out + if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + return; + } + + console.log('detected logout from a different tab'); //eslint-disable-line no-console + browserHistory.push('/' + this.props.params.team); + } + + if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { + return; + } + + console.log('detected login from a different tab'); //eslint-disable-line no-console + location.reload(); + } + }); + + // Because current CSS requires the root tag to have specific stuff + $('#root').attr('class', 'channel-view'); + + // ??? + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); + } else { + $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); + } + }); + + // Device tracking setup + var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); + if (iOS) { + $('body').addClass('ios'); + } + + // Set up tracking for whether the window is active + window.isActive = true; + $(window).on('focus', () => { + AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); + window.isActive = true; + }); + $(window).on('blur', () => { + window.isActive = false; + }); + + // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx + const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT); + Utils.applyFont(selectedFont); + + // Pervent backspace from navigating back a page + $(window).on('keydown.preventBackspace', (e) => { + if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) { + e.preventDefault(); + } + }); + } + componentWillUnmount() { + $('#root').attr('class', ''); + clearInterval(this.intervalId); + + $(window).off('focus'); + $(window).off('blur'); + + SocketStore.removeChangeListener(this.onSocketChange); + UserStore.removeChangeListener(this.onUserChanged); + + $('body').off('click.userpopover'); + $('body').off('mouseenter mouseleave', '.post'); + $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); + + $('.modal').off('show.bs.modal'); + + $(window).off('keydown.preventBackspace'); + } + render() { + return ( + <div className='channel-view'> + <ErrorBar/> + <div className='container-fluid'> + <SidebarRight/> + <SidebarRightMenu/> + {this.props.sidebar} + {this.props.center} + + <GetPostLinkModal/> + <GetTeamInviteLinkModal/> + <InviteMemberModal/> + <ImportThemeModal/> + <TeamSettingsModal/> + <MoreChannelsModal/> + <EditPostModal/> + <DeletePostModal/> + <RemovedFromChannelModal/> + <RegisterAppModal/> + <SelectTeamModal/> + </div> + </div> + ); + } +} + +LoggedIn.defaultProps = { +}; + +LoggedIn.propTypes = { + children: React.PropTypes.object, + sidebar: React.PropTypes.object, + center: React.PropTypes.object, + params: React.PropTypes.object +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 581b8e0b5..30c8ffe4f 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,82 +6,118 @@ import LoginUsername from './login_username.jsx'; import LoginLdap from './login_ldap.jsx'; import * as Utils from '../utils/utils.jsx'; +import * as Client from '../utils/client.jsx'; import Constants from '../utils/constants.jsx'; +import TeamStore from '../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; export default class Login extends React.Component { constructor(props) { super(props); - this.state = {}; + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = this.getStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + Client.getMeLoggedIn((data) => { + if (data && data.logged_in !== 'false') { + browserHistory.push('/' + this.props.params.team + '/channels/town-square'); + } + }); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + getStateFromStores() { + return { + currentTeam: TeamStore.getByName(this.props.params.team) + }; + } + onTeamChange() { + this.setState(this.getStateFromStores()); } render() { - const teamDisplayName = this.props.teamDisplayName; - const teamName = this.props.teamName; + const currentTeam = this.state.currentTeam; + if (currentTeam == null) { + return <div/>; + } + + const teamDisplayName = currentTeam.display_name; + const teamName = currentTeam.name; let loginMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( - <a - className='btn btn-custom-login gitlab' - key='gitlab' - href={'/' + teamName + '/login/gitlab'} - > - <span className='icon'/> - <span> - <FormattedMessage - id='login.gitlab' - defaultMessage='with GitLab' - /> - </span> - </a> + <a + className='btn btn-custom-login gitlab' + key='gitlab' + href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='login.gitlab' + defaultMessage='with GitLab' + /> + </span> + </a> ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { loginMessage.push( - <a - className='btn btn-custom-login google' - key='google' - href={'/' + teamName + '/login/google'} - > - <span className='icon'/> - <span> - <FormattedMessage - id='login.google' - defaultMessage='with Google Apps' - /> - </span> - </a> - ); + <a + className='btn btn-custom-login google' + key='google' + href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='login.google' + defaultMessage='with Google Apps' + /> + </span> + </a> + ); } const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { - let msg; if (extraParam === Constants.SIGNIN_CHANGE) { - msg = ( - <FormattedMessage - id='login.changed' - defaultMessage=' Sign-in method changed successfully' - /> + extraBox = ( + <div className='alert alert-success'> + <i className='fa fa-check'/> + <FormattedMessage + id='login.changed' + defaultMessage=' Sign-in method changed successfully' + /> + </div> ); } else if (extraParam === Constants.SIGNIN_VERIFIED) { - msg = ( - <FormattedMessage - id='login.verified' - defaultMessage=' Email Verified' - /> - ); - } - - if (msg != null) { extraBox = ( <div className='alert alert-success'> <i className='fa fa-check'/> - {msg} + <FormattedMessage + id='login.verified' + defaultMessage=' Email Verified' + /> + </div> + ); + } else if (extraParam === Constants.SESSION_EXPIRED) { + extraBox = ( + <div className='alert alert-warning'> + <i className='fa fa-exclamation-triangle'/> + <FormattedMessage + id='login.session_expired' + defaultMessage=' Your session has expired. Please login again.' + /> </div> ); } @@ -91,7 +127,7 @@ export default class Login extends React.Component { if (global.window.mm_config.EnableSignInWithEmail === 'true') { emailSignup = ( <LoginEmail - teamName={this.props.teamName} + teamName={teamName} /> ); } @@ -125,7 +161,7 @@ export default class Login extends React.Component { } let userSignUp = null; - if (this.props.inviteId) { + if (currentTeam.allow_open_invite) { userSignUp = ( <div> <span> @@ -134,7 +170,7 @@ export default class Login extends React.Component { defaultMessage="Don't have an account? " /> <a - href={'/signup_user_complete/?id=' + this.props.inviteId} + href={'/signup_user_complete/?id=' + currentTeam.invite_id} className='signup-team-login' > <FormattedMessage @@ -168,73 +204,65 @@ export default class Login extends React.Component { if (global.window.mm_config.EnableLdap === 'true') { ldapLogin = ( <LoginLdap - teamName={this.props.teamName} + teamName={teamName} /> ); } - let findTeams = null; - if (!Utils.isMobileApp()) { - findTeams = ( - <div className='form-group margin--extra form-group--small'> - <span> - <a href='/find_team'> - <FormattedMessage - id='login.find' - defaultMessage='Find your other teams' - /> - </a></span> - </div> - ); - } - let usernameLogin = null; if (global.window.mm_config.EnableSignInWithUsername === 'true') { usernameLogin = ( <LoginUsername - teamName={this.props.teamName} + teamName={teamName} /> ); } return ( - <div className='signup-team__container'> - <h5 className='margin--less'> - <FormattedMessage - id='login.signTo' - defaultMessage='Sign in to:' - /> - </h5> - <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'> - <FormattedMessage - id='login.on' - defaultMessage='on {siteName}' - values={{ - siteName: global.window.mm_config.SiteName - }} - /> - </h2> - {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {findTeams} - {forgotPassword} - {teamSignUp} + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <h5 className='margin--less'> + <FormattedMessage + id='login.signTo' + defaultMessage='Sign in to:' + /> + </h5> + <h2 className='signup-team__name'>{teamDisplayName}</h2> + <h2 className='signup-team__subdomain'> + <FormattedMessage + id='login.on' + defaultMessage='on {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </h2> + {extraBox} + {loginMessage} + {emailSignup} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} + </div> + </div> </div> ); } } Login.defaultProps = { - teamName: '', - teamDisplayName: '' }; Login.propTypes = { - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - inviteId: React.PropTypes.string + params: React.PropTypes.object.isRequired }; diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx index cf1e1bc40..3e0d8919d 100644 --- a/web/react/components/login_email.jsx +++ b/web/react/components/login_email.jsx @@ -4,6 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; import UserStore from '../stores/user_store.jsx'; +import {browserHistory} from 'react-router'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; @@ -72,13 +73,7 @@ class LoginEmail extends React.Component { Client.loginByEmail(name, email, password, () => { UserStore.setLastEmail(email); - - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - window.location.href = decodeURIComponent(redirect); - } else { - window.location.href = '/' + name + '/channels/town-square'; - } + browserHistory.push('/' + name + '/channels/town-square'); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { @@ -167,4 +162,4 @@ LoginEmail.propTypes = { teamName: React.PropTypes.string.isRequired }; -export default injectIntl(LoginEmail);
\ No newline at end of file +export default injectIntl(LoginEmail); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 93fe6c05a..974f026d0 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -56,9 +56,13 @@ export default class Navbar extends React.Component { return { channel: ChannelStore.getCurrent(), member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members + users: ChannelStore.getCurrentExtraInfo().members, + currentUser: UserStore.getCurrentUser() }; } + stateValid() { + return this.state.channel && this.state.member && this.state.users && this.state.currentUser; + } componentDidMount() { ChannelStore.addChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange); @@ -201,7 +205,7 @@ export default class Navbar extends React.Component { <ToggleModalButton role='menuitem' dialogType={ChannelInviteModal} - dialogProps={{channel}} + dialogProps={{channel, currentUser: this.state.currentUser}} > <FormattedMessage id='navbar.addMembers' @@ -286,7 +290,11 @@ export default class Navbar extends React.Component { <ToggleModalButton role='menuitem' dialogType={ChannelNotificationsModal} - dialogProps={{channel}} + dialogProps={{ + channel, + channelMember: this.state.member, + currentUser: this.state.currentUser + }} > <FormattedMessage id='navbar.preferences' @@ -412,7 +420,11 @@ export default class Navbar extends React.Component { return buttons; } render() { - var currentId = UserStore.getCurrentId(); + if (!this.stateValid()) { + return null; + } + + var currentId = this.state.currentUser.id; var channel = this.state.channel; var channelTitle = this.props.teamDisplayName; var popoverContent; diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 0ddd6ff4f..12227fd13 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -2,10 +2,7 @@ // See License.txt for license information. import * as Utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; -import UserStore from '../stores/user_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AboutBuildModal from './about_build_modal.jsx'; import TeamMembersModal from './team_members_modal.jsx'; @@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import Constants from '../utils/constants.jsx'; import {FormattedMessage} from 'mm-intl'; - -function getStateFromStores() { - const teams = []; - const teamsObject = UserStore.getTeams(); - for (const teamId in teamsObject) { - if (teamsObject.hasOwnProperty(teamId)) { - teams.push(teamsObject[teamId]); - } - } - - teams.sort(Utils.sortByDisplayName); - return {teams}; -} +import {Link} from 'react-router'; export default class NavbarDropdown extends React.Component { constructor(props) { super(props); this.blockToggle = false; - this.handleLogoutClick = this.handleLogoutClick.bind(this); this.handleAboutModal = this.handleAboutModal.bind(this); - this.onListenerChange = this.onListenerChange.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); - const state = getStateFromStores(); - state.showUserSettingsModal = false; - state.showAboutModal = false; - this.state = state; - } - handleLogoutClick(e) { - e.preventDefault(); - client.logout(); + this.state = { + showUserSettingsModal: false, + showAboutModal: false + }; } handleAboutModal() { this.setState({showAboutModal: true}); @@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component { this.setState({showAboutModal: false}); } componentDidMount() { - UserStore.addTeamsChangeListener(this.onListenerChange); - TeamStore.addChangeListener(this.onListenerChange); - $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { $('.sidebar--left .dropdown-menu').scrollTop(0); this.blockToggle = true; @@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component { }); } componentWillUnmount() { - UserStore.removeTeamsChangeListener(this.onListenerChange); - TeamStore.removeChangeListener(this.onListenerChange); - $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); } - onListenerChange() { - var newState = getStateFromStores(); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } - } render() { var teamLink = ''; var inviteLink = ''; var manageLink = ''; var sysAdminLink = ''; var adminDivider = ''; - var currentUser = UserStore.getCurrentUser(); + var currentUser = this.props.currentUser; var isAdmin = false; var isSystemAdmin = false; var teamSettings = null; @@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - onClick={EventHelpers.showInviteMemberModal} + onClick={GlobalActions.showInviteMemberModal} > <FormattedMessage id='navbar_dropdown.inviteMember' @@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - onClick={EventHelpers.showGetTeamInviteLinkModal} + onClick={GlobalActions.showGetTeamInviteLinkModal} > <FormattedMessage id='navbar_dropdown.teamLink' @@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component { sysAdminLink = ( <li> <a - href={'/admin_console?' + Utils.getSessionIndex()} + href={'/admin_console'} > <FormattedMessage id='navbar_dropdown.console' @@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component { var teams = []; - if (this.state.teams.length > 1) { - teams.push( - <li - className='divider' - key='div' - > - </li> - ); - - this.state.teams.forEach((team) => { - if (team.name !== this.props.teamName) { - teams.push( - <li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}> - <FormattedMessage - id='navbar_dropdown.switchTeam' - defaultMessage='Switch to {team}' - values={{ - team: team.display_name - }} - /> - </a></li>); - } - }); - } - if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push( <li key='newTeam_li'> @@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component { {inviteLink} {teamLink} <li> - <a - href='#' - onClick={this.handleLogoutClick} - > + <Link to={'/' + this.props.teamName + '/logout'}> <FormattedMessage id='navbar_dropdown.logout' defaultMessage='Logout' /> - </a> + </Link> </li> {adminDivider} {teamSettings} @@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = { NavbarDropdown.propTypes = { teamType: React.PropTypes.string, teamDisplayName: React.PropTypes.string, - teamName: React.PropTypes.string + teamName: React.PropTypes.string, + currentUser: React.PropTypes.object }; diff --git a/web/react/components/needs_team.jsx b/web/react/components/needs_team.jsx new file mode 100644 index 000000000..33b9cd37e --- /dev/null +++ b/web/react/components/needs_team.jsx @@ -0,0 +1,20 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as GlobalActions from '../action_creators/global_actions.jsx'; + +export default class NeedsTeam extends React.Component { + componentWillMount() { + GlobalActions.loadTeamRequiredPage(); + } + render() { + return this.props.children; + } +} + +NeedsTeam.defaultProps = { +}; + +NeedsTeam.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/not_logged_in.jsx b/web/react/components/not_logged_in.jsx new file mode 100644 index 000000000..7af293e77 --- /dev/null +++ b/web/react/components/not_logged_in.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class NotLoggedIn extends React.Component { + componentDidMount() { + $('body').attr('class', 'white'); + $('#root').attr('class', 'container-fluid'); + } + componentWillUnmount() { + $('body').attr('class', ''); + $('#root').attr('class', ''); + } + render() { + return ( + <div className='inner__wrap'> + <div className='row content'> + {this.props.children} + <div className='footer-push'></div> + </div> + <div className='row footer'> + <div className='footer-pane col-xs-12'> + <div className='col-xs-12'> + <span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span> + </div> + <div className='col-xs-12'> + <span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span> + <a + id='help_link' + className='pull-right footer-link' + href={global.window.mm_config.HelpLink} + > + <FormattedMessage id='web.footer.help'/> + </a> + <a + id='terms_link' + className='pull-right footer-link' + href={global.window.mm_config.TermsOfServiceLink} + > + <FormattedMessage id='web.footer.terms'/> + </a> + <a + id='privacy_link' + className='pull-right footer-link' + href={global.window.mm_config.PrivacyPolicyLink} + > + <FormattedMessage id='web.footer.privacy'/> + </a> + <a + id='about_link' + className='pull-right footer-link' + href={global.window.mm_config.AboutLink} + > + <FormattedMessage id='web.footer.about'/> + </a> + </div> + </div> + </div> + </div> + ); + } +} + +NotLoggedIn.defaultProps = { +}; + +NotLoggedIn.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx deleted file mode 100644 index 4c9bb6310..000000000 --- a/web/react/components/password_reset.jsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordResetSendLink from './password_reset_send_link.jsx'; -import PasswordResetForm from './password_reset_form.jsx'; - -export default class PasswordReset extends React.Component { - constructor(props) { - super(props); - - this.state = {}; - } - render() { - if (this.props.isReset === 'false') { - return ( - <PasswordResetSendLink - teamDisplayName={this.props.teamDisplayName} - teamName={this.props.teamName} - /> - ); - } - - return ( - <PasswordResetForm - teamDisplayName={this.props.teamDisplayName} - teamName={this.props.teamName} - hash={this.props.hash} - data={this.props.data} - /> - ); - } -} - -PasswordReset.defaultProps = { - isReset: '', - teamName: '', - teamDisplayName: '', - hash: '', - data: '' -}; -PasswordReset.propTypes = { - isReset: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string -}; diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 380dbe973..cfd39e440 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -2,24 +2,11 @@ // See License.txt for license information. import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_form.error', - defaultMessage: 'Please enter at least {chars} characters.' - }, - update: { - id: 'password_form.update', - defaultMessage: 'Your password has been updated successfully.' - }, - pwd: { - id: 'password_form.pwd', - defaultMessage: 'Password' - } -}); +import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; class PasswordResetForm extends React.Component { constructor(props) { @@ -32,51 +19,50 @@ class PasswordResetForm extends React.Component { handlePasswordReset(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; - var state = {}; - - var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { - state.error = formatMessage(holders.error, {chars: Constants.MIN_PASSWORD_LENGTH}); - this.setState(state); + this.setState({ + error: ( + <FormattedMessage + id='password_form.error' + defaultMessage='Please enter at least {chars} characters.' + chars={Constants.MIN_PASSWORD_LENGTH} + /> + ) + }); return; } - state.error = null; - this.setState(state); + this.setState({ + error: null + }); - var data = {}; + const data = {}; data.new_password = password; - data.hash = this.props.hash; - data.data = this.props.data; - data.name = this.props.teamName; + data.hash = this.props.location.query.h; + data.data = this.props.location.query.d; + data.name = this.props.params.team; Client.resetPassword(data, - function resetSuccess() { - this.setState({error: null, updateText: formatMessage(holders.update)}); - }.bind(this), - function resetFailure(err) { - this.setState({error: err.message, updateText: null}); - }.bind(this) + () => { + this.setState({error: null}); + browserHistory.push('/' + this.props.params.team + '/login'); + }, + (err) => { + this.setState({error: err.message}); + } ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = (<div className='form-group'><br/><label className='control-label reset-form'>{this.state.updateText} - <FormattedHTMLMessage - id='password_form.click' - defaultMessage='Click <a href={url}>here</a> to log in.' - values={{ - url: '/' + this.props.teamName + '/login' - }} - /> - </label></div>); - } - var error = null; if (this.state.error) { - error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + error = ( + <div className='form-group has-error'> + <label className='control-label'> + {this.state.error} + </label> + </div> + ); } var formClass = 'form-group'; @@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( <div className='col-sm-12'> <div className='signup-team__container'> @@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component { <p> <FormattedMessage id='password_form.enter' - defaultMessage='Enter a new password for your {teamDisplayName} {siteName} account.' + defaultMessage='Enter a new password for your {siteName} account.' values={{ - teamDisplayName: this.props.teamDisplayName, siteName: global.window.mm_config.SiteName }} /> @@ -111,7 +95,10 @@ class PasswordResetForm extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage( + 'password_form.pwd', + 'Password' + )} spellCheck='false' /> </div> @@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component { defaultMessage='Change my password' /> </button> - {updateText} </form> </div> </div> @@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component { } PasswordResetForm.defaultProps = { - teamName: '', - teamDisplayName: '', - hash: '', - data: '' }; PasswordResetForm.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetForm);
\ No newline at end of file +export default PasswordResetForm; diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 8cc8a050d..ce6253e16 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -4,26 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as client from '../utils/client.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_send.error', - defaultMessage: 'Please enter a valid email address.' - }, - link: { - id: 'password_send.link', - defaultMessage: '<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>' - }, - checkInbox: { - id: 'password_send.checkInbox', - defaultMessage: 'Please check your inbox.' - }, - email: { - id: 'password_send.email', - defaultMessage: 'Email' - } -}); +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; class PasswordResetSendLink extends React.Component { constructor(props) { @@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component { this.handleSendLink = this.handleSendLink.bind(this); - this.state = {}; + this.state = { + error: '', + updateText: '' + }; } handleSendLink(e) { e.preventDefault(); - var state = {}; - const {formatMessage} = this.props.intl; var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email || !Utils.isEmail(email)) { - state.error = formatMessage(holders.error); - this.setState(state); + this.setState({ + error: ( + <FormattedMessage + id={'password_send.error'} + defaultMessage={'Please enter a valid email address.'} + /> + ) + }); return; } - state.error = null; - this.setState(state); + // End of error checking clear error + this.setState({ + error: '' + }); var data = {}; data.email = email; - data.name = this.props.teamName; - + data.name = this.props.params.team; client.sendPasswordReset(data, - function passwordResetSent() { - this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)}); - $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); - }.bind(this), - function passwordResetFailedToSend(err) { - this.setState({error: err.message, update_text: null, moreUpdateText: null}); - }.bind(this) - ); + () => { + this.setState({ + error: null, + updateText: ( + <div className='reset-form alert alert-success'> + <FormattedHTMLMessage + id='password_send.link' + defaultMessage='<p>A password reset link has been sent to <b>{email}</b></p>' + email={email} + /> + <FormattedMessage + id={'password_send.checkInbox'} + defaultMessage={'Please check your inbox.'} + /> + </div> + ) + }); + $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); + }, + (err) => { + this.setState({ + error: err.message, + update_text: null + }); + } + ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = ( - <div className='reset-form alert alert-success' - dangerouslySetInnerHTML={{__html: this.state.updateText + this.state.moreUpdateText}} - > - </div> - ); - } - var error = null; if (this.state.error) { error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; @@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( - <div className='col-sm-12'> - <div className='signup-team__container'> - <h3> + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> <FormattedMessage - id='password_send.title' - defaultMessage='Password Reset' + id='web.header.back' /> - </h3> - {updateText} - <form - onSubmit={this.handleSendLink} - ref='reset_form' - > - <p> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3> <FormattedMessage - id='password_send.description' - defaultMessage='To reset your password, enter the email address you used to sign up for {teamName}.' - values={{ - teamName: this.props.teamDisplayName - }} + id='password_send.title' + defaultMessage='Password Reset' /> - </p> - <div className={formClass}> - <input - type='email' - className='form-control' - name='email' - ref='email' - placeholder={formatMessage(holders.email)} - spellCheck='false' - /> - </div> - {error} - <button - type='submit' - className='btn btn-primary' + </h3> + {this.state.updateText} + <form + onSubmit={this.handleSendLink} + ref='reset_form' > - <FormattedMessage - id='password_send.reset' - defaultMessage='Reset my password' - /> - </button> - </form> + <p> + <FormattedMessage + id='password_send.description' + defaultMessage='To reset your password, enter the email address you used to sign up' + /> + </p> + <div className={formClass}> + <input + type='email' + className='form-control' + name='email' + ref='email' + placeholder={Utils.localizeMessage( + 'password_send.email', + 'Email' + )} + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='password_send.reset' + defaultMessage='Reset my password' + /> + </button> + </form> + </div> </div> </div> ); @@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component { } PasswordResetSendLink.defaultProps = { - teamName: '', - teamDisplayName: '' }; PasswordResetSendLink.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + params: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetSendLink);
\ No newline at end of file +export default PasswordResetSendLink; diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index afff78bae..1943fb409 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component { className='profile-img rounded pull-left' width='26px' height='26px' - src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${m.id}/image?time=${m.update_at}`} /> <div className='pull-left'> <div diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 57e919e45..3a855edf2 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -4,7 +4,6 @@ import PostHeader from './post_header.jsx'; import PostBody from './post_body.jsx'; -import UserStore from '../stores/user_store.jsx'; import PostStore from '../stores/post_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -128,7 +127,6 @@ export default class Post extends React.Component { const post = this.props.post; const parentPost = this.props.parentPost; const posts = this.props.posts; - const user = this.props.user || {}; if (!post.props) { post.props = {}; @@ -156,13 +154,15 @@ export default class Post extends React.Component { } let currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) { + if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) { currentUserCss = 'current--user'; } - let timestamp = user.update_at; - if (timestamp == null) { - timestamp = UserStore.getCurrentUser().update_at; + let timestamp = 0; + if (!this.props.user || this.props.user.update_at == null) { + timestamp = this.props.currentUser.update_at; + } else { + timestamp = this.props.user.update_at; } let sameUserClass = ''; @@ -182,7 +182,7 @@ export default class Post extends React.Component { let profilePic = null; if (!this.props.hideProfilePic) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex(); + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; @@ -218,6 +218,7 @@ export default class Post extends React.Component { isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} user={this.props.user} + currentUser={this.props.currentUser} /> <PostBody post={post} @@ -245,5 +246,7 @@ Post.propTypes = { hideProfilePic: React.PropTypes.bool, isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, - displayNameType: React.PropTypes.string + displayNameType: React.PropTypes.string, + hasProfiles: React.PropTypes.bool, + currentUser: React.PropTypes.object.isRequired }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 854cb095a..2fa4cebfe 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -80,12 +80,10 @@ class PostBody extends React.Component { username = parentPost.props.override_username; } - if (global.window.mm_locale === 'en') { - if (username.slice(-1) === 's') { - apostrophe = '\''; - } else { - apostrophe = '\'s'; - } + if (username.slice(-1) === 's') { + apostrophe = '\''; + } else { + apostrophe = '\'s'; } name = ( <a diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx index 44a0bae09..fd654f502 100644 --- a/web/react/components/post_focus_view.jsx +++ b/web/react/components/post_focus_view.jsx @@ -5,7 +5,8 @@ import PostsView from './posts_view.jsx'; import PostStore from '../stores/post_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import UserStore from '../stores/user_store.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -15,6 +16,7 @@ export default class PostFocusView extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); @@ -26,18 +28,21 @@ export default class PostFocusView extends React.Component { scrollPostId: focusedPostId, postList: PostStore.getVisiblePosts(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId), - atBottom: PostStore.getVisibilityAtBottom(focusedPostId) + atBottom: PostStore.getVisibilityAtBottom(focusedPostId), + currentUser: UserStore.getCurrentUser() }; } componentDidMount() { ChannelStore.addChangeListener(this.onChannelChange); PostStore.addChangeListener(this.onPostsChange); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); PostStore.removeChangeListener(this.onPostsChange); + UserStore.removeChangeListener(this.onUserChange); } onChannelChange() { @@ -46,6 +51,10 @@ export default class PostFocusView extends React.Component { }); } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + onPostsChange() { const focusedPostId = PostStore.getFocusedPostId(); if (focusedPostId == null) { @@ -65,11 +74,11 @@ export default class PostFocusView extends React.Component { } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsFocusedTopEvent(); + GlobalActions.emitLoadMorePostsFocusedTopEvent(); } loadMorePostsBottom() { - EventHelpers.emitLoadMorePostsFocusedBottomEvent(); + GlobalActions.emitLoadMorePostsFocusedBottomEvent(); } getIntroMessage() { @@ -89,6 +98,10 @@ export default class PostFocusView extends React.Component { const postsToHighlight = {}; postsToHighlight[this.state.scrollPostId] = true; + if (!this.state.currentUser || !this.state.postList) { + return null; + } + return ( <div id='post-list'> <PostsView @@ -106,6 +119,7 @@ export default class PostFocusView extends React.Component { messageSeparatorTime={0} postsToHighlight={postsToHighlight} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> </div> ); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 2803fe387..966775dad 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -14,16 +14,15 @@ export default class PostHeader extends React.Component { } render() { const post = this.props.post; - const user = this.props.user; - let userProfile = <UserProfile user={user}/>; + let userProfile = <UserProfile user={this.props.user}/>; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile - user={user} + user={this.props.user} overwriteName={post.props.override_username} disablePopover={true} /> @@ -54,6 +53,7 @@ export default class PostHeader extends React.Component { allowReply='true' isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} + currentUser={this.props.currentUser} /> </li> </ul> @@ -68,10 +68,11 @@ PostHeader.defaultProps = { sameUser: false }; PostHeader.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, user: React.PropTypes.object, - commentCount: React.PropTypes.number, - isLastComment: React.PropTypes.bool, - handleCommentClick: React.PropTypes.func, - sameUser: React.PropTypes.bool + currentUser: React.PropTypes.object.isRequired, + commentCount: React.PropTypes.number.isRequired, + isLastComment: React.PropTypes.bool.isRequired, + handleCommentClick: React.PropTypes.func.isRequired, + sameUser: React.PropTypes.bool.isRequired }; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ffac6eaef..d0a4c828e 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -1,10 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import TimeSince from './time_since.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -27,8 +26,8 @@ export default class PostInfo extends React.Component { } createDropdown() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() === post.user_id; - var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); + var isOwner = this.props.currentUser.id === post.user_id; + var isAdmin = Utils.isAdmin(this.props.currentUser.roles); if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; @@ -47,21 +46,21 @@ export default class PostInfo extends React.Component { if (this.props.allowReply === 'true') { dropdownContents.push( - <li - key='replyLink' - role='presentation' - > - <a - className='link__reply theme' - href='#' - onClick={this.props.handleCommentClick} - > - <FormattedMessage - id='post_info.reply' - defaultMessage='Reply' - /> - </a> - </li> + <li + key='replyLink' + role='presentation' + > + <a + className='link__reply theme' + href='#' + onClick={this.props.handleCommentClick} + > + <FormattedMessage + id='post_info.reply' + defaultMessage='Reply' + /> + </a> + </li> ); } @@ -93,7 +92,7 @@ export default class PostInfo extends React.Component { <a href='#' role='menuitem' - onClick={() => EventHelpers.showDeletePostModal(post, dataComments)} + onClick={() => GlobalActions.showDeletePostModal(post, dataComments)} > <FormattedMessage id='post_info.del' @@ -157,11 +156,11 @@ export default class PostInfo extends React.Component { handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } removePost() { - EventHelpers.emitRemovePost(this.props.post); + GlobalActions.emitRemovePost(this.props.post); } createRemovePostButton(post) { if (!Utils.isPostEphemeral(post)) { @@ -240,10 +239,11 @@ PostInfo.defaultProps = { sameUser: false }; PostInfo.propTypes = { - post: React.PropTypes.object, - commentCount: React.PropTypes.number, - isLastComment: React.PropTypes.bool, - allowReply: React.PropTypes.string, - handleCommentClick: React.PropTypes.func, - sameUser: React.PropTypes.bool + post: React.PropTypes.object.isRequired, + commentCount: React.PropTypes.number.isRequired, + isLastComment: React.PropTypes.bool.isRequired, + allowReply: React.PropTypes.string.isRequired, + handleCommentClick: React.PropTypes.func.isRequired, + sameUser: React.PropTypes.bool.isRequired, + currentUser: React.PropTypes.object.isRequired }; diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index c2c739e9a..0a9232850 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -1,9 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as Utils from '../utils/utils.jsx'; import Post from './post.jsx'; import Constants from '../utils/constants.jsx'; @@ -144,7 +143,7 @@ export default class PostsView extends React.Component { createPosts(posts, order) { const postCtls = []; let previousPostDay = new Date(0); - const userId = UserStore.getCurrentId(); + const userId = this.props.currentUser.id; const profiles = this.props.profiles || {}; let renderedLastViewed = false; @@ -230,8 +229,8 @@ export default class PostsView extends React.Component { const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id); let profile; - if (UserStore.getCurrentId() === post.user_id) { - profile = UserStore.getCurrentUser(); + if (this.props.currentUser.id === post.user_id) { + profile = this.props.currentUser; } else { profile = profiles[post.user_id]; } @@ -248,9 +247,10 @@ export default class PostsView extends React.Component { hideProfilePic={hideProfilePic} isLastComment={isLastComment} shouldHighlight={shouldHighlight} - onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} user={profile} + currentUser={this.props.currentUser} /> ); @@ -525,7 +525,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -535,7 +535,8 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired }; function FloatingTimestamp({isScrolling, post}) { diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 976e03fab..b361779d2 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + currentUser: UserStore.getCurrentUser() }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); + } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component { return PostStore.getVisiblePosts(id); } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsEvent(); + GlobalActions.emitLoadMorePostsEvent(); } handlePostsViewScroll(atBottom) { if (atBottom) { @@ -165,6 +173,10 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); + if (!this.state.currentUser || !channel) { + return null; + } + const postListCtls = []; for (let i = 0; i < channels.length; i++) { const isActive = (channels[i] === currentChannelId); @@ -185,6 +197,7 @@ export default class PostsViewContainer extends React.Component { introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9588809eb..9183b761f 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; @@ -70,7 +70,7 @@ class RhsComment extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -151,7 +151,7 @@ class RhsComment extends React.Component { <a href='#' role='menuitem' - onClick={() => EventHelpers.showDeletePostModal(post, 0)} + onClick={() => GlobalActions.showDeletePostModal(post, 0)} > <FormattedMessage id='rhs_comment.del' @@ -253,7 +253,7 @@ class RhsComment extends React.Component { <div className='post__content'> <div className='post__img'> <img - src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 023f3dd2d..fc1cd0b41 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component { <a href='#' role='menuitem' - onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)} + onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)} > <FormattedMessage id='rhs_root.del' @@ -211,7 +211,7 @@ export default class RhsRootPost extends React.Component { ); } - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex(); + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; diff --git a/web/react/components/root.jsx b/web/react/components/root.jsx new file mode 100644 index 000000000..70038203b --- /dev/null +++ b/web/react/components/root.jsx @@ -0,0 +1,90 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; + +var IntlProvider = ReactIntl.IntlProvider; + +export default class Root extends React.Component { + constructor(props) { + super(props); + this.state = { + locale: 'en', + translations: null + }; + + this.localizationChanged = this.localizationChanged.bind(this); + } + localizationChanged() { + this.setState({locale: LocalizationStore.getLocale(), translations: LocalizationStore.getTranslations()}); + } + componentWillMount() { + // Setup localization listener + LocalizationStore.addChangeListener(this.localizationChanged); + + // Browser store check version + BrowserStore.checkVersion(); + + window.onerror = (msg, url, line, column, stack) => { + var l = {}; + l.level = 'ERROR'; + l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; + + $.ajax({ + url: '/api/v1/admin/log_client', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(l) + }); + + if (window.mm_config.EnableDeveloper === 'true') { + window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'}); + window.ErrorStore.emitChange(); + } + }; + + // Ya.... + /*eslint-disable */ + if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { + !function(){var analytics=global.window.analytics=global.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.mm_config.SegmentDeveloperKey); + analytics.page(); + }}(); + } else { + global.window.analytics = {}; + global.window.analytics.page = function(){}; + global.window.analytics.track = function(){}; + } + /*eslint-enable */ + + // Get our localizaiton + GlobalActions.newLocalizationSelected('en'); + } + componentWillUnmount() { + LocalizationStore.removeChangeListener(this.localizationChanged); + } + render() { + if (this.state.translations == null) { + return <div/>; + } + + return ( + <IntlProvider + locale={this.state.locale} + messages={this.state.translations} + key={this.state.locale} + > + {this.props.children} + </IntlProvider> + ); + } +} +Root.defaultProps = { +}; + +Root.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 5ab864b7c..3a091bdd1 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -3,8 +3,7 @@ import UserStore from '../stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import Constants from '../utils/constants.jsx'; @@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component { handleClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusEvent(this.props.post.id); + GlobalActions.emitPostFocusEvent(this.props.post.id); if ($(window).width() < 768) { $('.sidebar--right').removeClass('move--left'); @@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component { handleFocusRHSClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); + GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } render() { @@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component { <div className='post__content'> <div className='post__img'> <img - src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()} + src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp} height='36' width='36' /> diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx new file mode 100644 index 000000000..c473fe366 --- /dev/null +++ b/web/react/components/should_verify_email.jsx @@ -0,0 +1,111 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; + +export default class ShouldVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.handleResend = this.handleResend.bind(this); + + this.state = { + resendStatus: 'none' + }; + } + handleResend() { + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + this.setState({resendStatus: 'sending'}); + + Client.resendVerification(() => { + this.setState({resendStatus: 'success'}); + }, + () => { + this.setState({resendStatus: 'failure'}); + }, + teamName, + email); + } + render() { + let resendConfirm = ''; + if (this.state.resendStatus === 'success') { + resendConfirm = ( + <div> + <br/> + <p className='alert alert-success'> + <i className='fa fa-check'/> + <FormattedMessage + id='email_verify.sent' + defaultMessage=' Verification email sent.' + /> + </p> + </div> + ); + } + + if (this.state.resendStatus === 'failure') { + resendConfirm = ( + <div> + <br/> + <p className='alert alert-danger'> + <i className='fa fa-times'/> + <FormattedMessage id='email_verify.failed'/> + </p> + </div> + ); + } + + return ( + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3> + <FormattedMessage + id='email_verify.almost' + defaultMessage='{siteName}: You are almost done' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </h3> + <div> + <p> + <FormattedMessage + id='email_verify.notVerifiedBody' + defaultMessage='Please verify your email address. Check your inbox for an email.' + /> + </p> + <button + onClick={this.handleResend} + className='btn btn-primary' + > + <FormattedMessage + id='email_verify.resend' + defaultMessage='Resend Email' + /> + </button> + {resendConfirm} + </div> + </div> + </div> + </div> + ); + } +} + +ShouldVerifyEmail.defaultProps = { +}; +ShouldVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index c7dba306b..5c682d64b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -129,7 +129,9 @@ export default class Sidebar extends React.Component { directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), - showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER + showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, + currentTeam: TeamStore.getCurrent(), + currentUser: UserStore.getCurrentUser() }; } @@ -179,7 +181,7 @@ export default class Sidebar extends React.Component { } updateTitle() { const channel = ChannelStore.getCurrent(); - if (channel) { + if (channel && this.state.currentTeam) { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { currentSiteName = global.window.mm_config.SiteName; @@ -196,7 +198,7 @@ export default class Sidebar extends React.Component { const unread = this.getTotalUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; - document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName; } } onScroll() { @@ -401,7 +403,6 @@ export default class Sidebar extends React.Component { // set up click handler to switch channels (or create a new channel for non-existant ones) var handleClick = null; var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { handleClick = function clickHandler(e) { @@ -413,7 +414,7 @@ export default class Sidebar extends React.Component { e.preventDefault(); }; - } else if (channel.fake && teamURL) { + } else if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = Utils.getUserIdFromChannelName(channel); @@ -434,7 +435,7 @@ export default class Sidebar extends React.Component { }, () => { this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + window.location.href = '/' + this.state.currentTeam.name; } ); } @@ -497,6 +498,11 @@ export default class Sidebar extends React.Component { ); } render() { + // Check if we have all info needed to render + if (this.state.currentTeam == null || this.state.currentUser == null) { + return (<div/>); + } + this.badgesActive = false; // keep track of the first and last unread channels so we can use them to set the unread indicators @@ -586,7 +592,10 @@ export default class Sidebar extends React.Component { ); return ( - <div> + <div + className='sidebar--left' + id='sidebar-left' + > <NewChannelFlow show={showChannelModal} channelType={this.state.newChannelModalType} @@ -598,9 +607,10 @@ export default class Sidebar extends React.Component { /> <SidebarHeader - teamDisplayName={TeamStore.getCurrent().display_name} - teamName={TeamStore.getCurrent().name} - teamType={TeamStore.getCurrent().type} + teamDisplayName={this.state.currentTeam.display_name} + teamName={this.state.currentTeam.name} + teamType={this.state.currentTeam.type} + currentUser={this.state.currentUser} /> <UnreadChannelIndicator diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 45b0a5fc4..00d30948a 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -4,10 +4,8 @@ import NavbarDropdown from './navbar_dropdown.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; -import UserStore from '../stores/user_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; -import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; import {FormattedHTMLMessage} from 'mm-intl'; @@ -34,7 +32,7 @@ export default class SidebarHeader extends React.Component { PreferenceStore.removeChangeListener(this.onPreferenceChange); } getStateFromStores() { - const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, this.props.currentUser.id, 999); return {showTutorialTip: tutorialStep === TutorialSteps.MENU_POPOVER}; } @@ -77,7 +75,7 @@ export default class SidebarHeader extends React.Component { ); } render() { - var me = UserStore.getCurrentUser(); + var me = this.props.currentUser; var profilePicture = null; if (!me) { @@ -88,7 +86,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} /> ); } @@ -124,6 +122,7 @@ export default class SidebarHeader extends React.Component { teamType={this.props.teamType} teamDisplayName={this.props.teamDisplayName} teamName={this.props.teamName} + currentUser={this.props.currentUser} /> </div> ); @@ -131,11 +130,12 @@ export default class SidebarHeader extends React.Component { } SidebarHeader.defaultProps = { - teamDisplayName: global.window.mm_config.SiteName, + teamDisplayName: '', teamType: '' }; SidebarHeader.propTypes = { teamDisplayName: React.PropTypes.string, teamName: React.PropTypes.string, - teamType: React.PropTypes.string + teamType: React.PropTypes.string, + currentUser: React.PropTypes.object }; diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx index b81c0d099..14853d3a3 100644 --- a/web/react/components/sidebar_right.jsx +++ b/web/react/components/sidebar_right.jsx @@ -127,8 +127,13 @@ export default class SidebarRight extends React.Component { } return ( - <div className='sidebar-right-container'> - {content} + <div + className='sidebar--right' + id='sidebar-right' + > + <div className='sidebar-right-container'> + {content} + </div> </div> ); } diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 4d714e9f1..c7c5bcfd6 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -5,11 +5,11 @@ import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import UserStore from '../stores/user_store.jsx'; -import * as client from '../utils/client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import * as Utils from '../utils/utils.jsx'; import {FormattedMessage} from 'mm-intl'; +import {Link} from 'react-router'; export default class SidebarRightMenu extends React.Component { componentDidMount() { @@ -19,18 +19,11 @@ export default class SidebarRightMenu extends React.Component { constructor(props) { super(props); - this.handleLogoutClick = this.handleLogoutClick.bind(this); - this.state = { showUserSettingsModal: false }; } - handleLogoutClick(e) { - e.preventDefault(); - client.logout(); - } - render() { var teamLink = ''; var inviteLink = ''; @@ -42,14 +35,14 @@ export default class SidebarRightMenu extends React.Component { var isSystemAdmin = false; if (currentUser != null) { - isAdmin = utils.isAdmin(currentUser.roles); - isSystemAdmin = utils.isSystemAdmin(currentUser.roles); + isAdmin = Utils.isAdmin(currentUser.roles); + isSystemAdmin = Utils.isSystemAdmin(currentUser.roles); inviteLink = ( <li> <a href='#' - onClick={EventHelpers.showInviteMemberModal} + onClick={GlobalActions.showInviteMemberModal} > <i className='fa fa-user'></i> <FormattedMessage @@ -65,7 +58,7 @@ export default class SidebarRightMenu extends React.Component { <li> <a href='#' - onClick={EventHelpers.showGetTeamInviteLinkModal} + onClick={GlobalActions.showGetTeamInviteLinkModal} > <i className='glyphicon glyphicon-link'></i> <FormattedMessage @@ -107,13 +100,13 @@ export default class SidebarRightMenu extends React.Component { ); } - if (isSystemAdmin && !utils.isMobile()) { + if (isSystemAdmin && !Utils.isMobile()) { consoleLink = ( <li> <a - href={'/admin_console?' + utils.getSessionIndex()} + href={'/admin_console'} > - <i className='fa fa-wrench'></i> + <i className='fa fa-wrench'></i> <FormattedMessage id='sidebar_right_menu.console' defaultMessage='System Console' @@ -168,7 +161,10 @@ export default class SidebarRightMenu extends React.Component { ); } return ( - <div> + <div + className='sidebar--menu' + id='sidebar-menu' + > <div className='team__header theme'> <a className='team__name' @@ -196,16 +192,13 @@ export default class SidebarRightMenu extends React.Component { {manageLink} {consoleLink} <li> - <a - href='#' - onClick={this.handleLogoutClick} - > + <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}> <i className='fa fa-sign-out'></i> <FormattedMessage id='sidebar_right_menu.logout' defaultMessage='Logout' /> - </a> + </Link> </li> <li className='divider'></li> {helpLink} diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 26c46dad0..2adf8d111 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -6,6 +6,8 @@ import EmailSignUpPage from './team_signup_with_email.jsx'; import SSOSignupPage from './team_signup_with_sso.jsx'; import LdapSignUpPage from './team_signup_with_ldap.jsx'; import Constants from '../utils/constants.jsx'; +import TeamStore from '../stores/team_store.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -14,6 +16,7 @@ export default class TeamSignUp extends React.Component { super(props); this.updatePage = this.updatePage.bind(this); + this.onTeamUpdate = this.onTeamUpdate.bind(this); var count = 0; @@ -46,11 +49,34 @@ export default class TeamSignUp extends React.Component { this.setState({page}); } + componentWillMount() { + if (global.window.mm_config.EnableTeamListing === 'true') { + AsyncClient.getAllTeams(); + this.onTeamUpdate(); + } + } + + componentDidMount() { + TeamStore.addChangeListener(this.onTeamUpdate); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamUpdate); + } + + onTeamUpdate() { + this.setState({ + teams: TeamStore.getAll() + }); + } + render() { - var teamListing = null; + let teamListing = null; if (global.window.mm_config.EnableTeamListing === 'true') { - if (this.props.teams.length === 0) { + if (this.state.teams == null) { + teamListing = (<div/>); + } else if (this.state.teams.length === 0) { if (global.window.mm_config.EnableTeamCreation !== 'true') { teamListing = ( <div> @@ -72,23 +98,26 @@ export default class TeamSignUp extends React.Component { </h4> <div className='signup-team-all'> { - this.props.teams.map((team) => { - return ( - <div - key={'team_' + team.name} - className='signup-team-dir' - > - <a - href={'/' + team.name} + Object.values(this.state.teams).map((team) => { + if (team.allow_team_listing) { + return ( + <div + key={'team_' + team.name} + className='signup-team-dir' > - <span className='signup-team-dir__name'>{team.display_name}</span> - <span - className='glyphicon glyphicon-menu-right right signup-team-dir__arrow' - aria-hidden='true' - /> - </a> - </div> - ); + <a + href={'/' + team.name} + > + <span className='signup-team-dir__name'>{team.display_name}</span> + <span + className='glyphicon glyphicon-menu-right right signup-team-dir__arrow' + aria-hidden='true' + /> + </a> + </div> + ); + } + return null; }) } </div> @@ -103,42 +132,26 @@ export default class TeamSignUp extends React.Component { } } + let signupMethod = null; + if (global.window.mm_config.EnableTeamCreation !== 'true') { if (teamListing == null) { - return ( - <div> - <FormattedMessage - id='signup_team.disabled' - defaultMessage='Team creation has been disabled. Please contact an administrator for access.' - /> - </div> + signupMethod = ( + <FormattedMessage + id='signup_team.disabled' + defaultMessage='Team creation has been disabled. Please contact an administrator for access.' + /> ); } - - return ( - <div> - {teamListing} - </div> - ); - } - - if (this.state.page === 'choose') { - return ( - <div> - {teamListing} - <ChoosePage - updatePage={this.updatePage} - /> - </div> + } else if (this.state.page === 'choose') { + signupMethod = ( + <ChoosePage + updatePage={this.updatePage} + /> ); - } - - if (this.state.page === 'email') { - return ( - <div> - {teamListing} - <EmailSignUpPage/> - </div> + } else if (this.state.page === 'email') { + signupMethod = ( + <EmailSignUpPage/> ); } else if (this.state.page === 'ldap') { return ( @@ -148,35 +161,45 @@ export default class TeamSignUp extends React.Component { </div> ); } else if (this.state.page === 'gitlab') { - return ( - <div> - {teamListing} - <SSOSignupPage service={Constants.GITLAB_SERVICE}/> - </div> + signupMethod = ( + <SSOSignupPage service={Constants.GITLAB_SERVICE}/> ); } else if (this.state.page === 'google') { - return ( - <div> - {teamListing} - <SSOSignupPage service={Constants.GOOGLE_SERVICE}/> - </div> + signupMethod = ( + <SSOSignupPage service={Constants.GOOGLE_SERVICE}/> ); } else if (this.state.page === 'none') { - return ( - <div> - <FormattedMessage - id='signup_team.none' - defaultMessage='No team creation method has been enabled. Please contact an administrator for access.' - /> - </div> + signupMethod = ( + <FormattedMessage + id='signup_team.none' + defaultMessage='No team creation method has been enabled. Please contact an administrator for access.' + /> ); } - return null; + return ( + <div className='col-sm-12'> + <div className='signup-team__container'> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h1>{global.window.mm_config.SiteName}</h1> + <h4 className='color--light'> + <FormattedMessage + id='web.root.singup_info' + /> + </h4> + <div id='signup-team'> + {teamListing} + {signupMethod} + </div> + </div> + </div> + ); } } TeamSignUp.propTypes = { - teams: React.PropTypes.array }; diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx deleted file mode 100644 index 16553daeb..000000000 --- a/web/react/components/signup_team_complete.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import WelcomePage from './team_signup_welcome_page.jsx'; -import TeamDisplayNamePage from './team_signup_display_name_page.jsx'; -import TeamURLPage from './team_signup_url_page.jsx'; -import SendInivtesPage from './team_signup_send_invites_page.jsx'; -import UsernamePage from './team_signup_username_page.jsx'; -import PasswordPage from './team_signup_password_page.jsx'; -import BrowserStore from '../stores/browser_store.jsx'; - -import {FormattedMessage} from 'mm-intl'; - -export default class SignupTeamComplete extends React.Component { - constructor(props) { - super(props); - - this.updateParent = this.updateParent.bind(this); - - var initialState = BrowserStore.getGlobalItem(props.hash); - - if (!initialState) { - initialState = {}; - initialState.wizard = 'welcome'; - initialState.team = {}; - initialState.team.email = this.props.email; - initialState.team.allowed_domains = ''; - initialState.invites = []; - initialState.invites.push(''); - initialState.invites.push(''); - initialState.invites.push(''); - initialState.user = {}; - initialState.hash = this.props.hash; - initialState.data = this.props.data; - } - - this.state = initialState; - } - updateParent(state, skipSet) { - BrowserStore.setGlobalItem(this.props.hash, state); - - if (!skipSet) { - this.setState(state); - } - } - render() { - if (this.state.wizard === 'welcome') { - return ( - <WelcomePage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - if (this.state.wizard === 'team_display_name') { - return ( - <TeamDisplayNamePage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - if (this.state.wizard === 'team_url') { - return ( - <TeamURLPage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - if (this.state.wizard === 'send_invites') { - return ( - <SendInivtesPage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - if (this.state.wizard === 'username') { - return ( - <UsernamePage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - if (this.state.wizard === 'password') { - return ( - <PasswordPage - state={this.state} - updateParent={this.updateParent} - /> - ); - } - - return ( - <div> - <FormattedMessage - id='signup_team_complete.completed' - defaultMessage="You've already completed the signup process for this invitation or this invitation has expired." - /> - </div> - ); - } -} - -SignupTeamComplete.defaultProps = { - hash: '', - email: '', - data: '' -}; -SignupTeamComplete.propTypes = { - hash: React.PropTypes.string, - email: React.PropTypes.string, - data: React.PropTypes.string -}; diff --git a/web/react/components/signup_team_complete/components/signup_team_complete.jsx b/web/react/components/signup_team_complete/components/signup_team_complete.jsx new file mode 100644 index 000000000..5ad21e941 --- /dev/null +++ b/web/react/components/signup_team_complete/components/signup_team_complete.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import BrowserStore from '../../../stores/browser_store.jsx'; + +import {FormattedMessage} from 'mm-intl'; + +import {browserHistory} from 'react-router'; + +export default class SignupTeamComplete extends React.Component { + constructor(props) { + super(props); + + this.updateParent = this.updateParent.bind(this); + } + componentWillMount() { + const data = JSON.parse(this.props.location.query.d); + this.hash = this.props.location.query.h; + + var initialState = BrowserStore.getGlobalItem(this.hash); + + if (!initialState) { + initialState = {}; + initialState.wizard = 'welcome'; + initialState.team = {}; + initialState.team.email = data.email; + initialState.team.allowed_domains = ''; + initialState.invites = []; + initialState.invites.push(''); + initialState.invites.push(''); + initialState.invites.push(''); + initialState.user = {}; + initialState.hash = this.hash; + initialState.data = this.props.location.query.d; + } + + this.setState(initialState); + } + componentDidMount() { + browserHistory.push('/signup_team_complete/welcome'); + } + updateParent(state, skipSet) { + BrowserStore.setGlobalItem(this.hash, state); + + if (!skipSet) { + this.setState(state); + browserHistory.push('/signup_team_complete/' + state.wizard); + } + } + render() { + return ( + <div> + <div className='signup-header'> + <a href='/'> + <span classNameName='fa fa-chevron-left'/> + <FormattedMessage id='web.header.back'/> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <div id='signup-team-complete'> + {React.cloneElement(this.props.children, { + state: this.state, + updateParent: this.updateParent + })} + </div> + </div> + </div> + </div> + ); + } +} + +SignupTeamComplete.defaultProps = { +}; +SignupTeamComplete.propTypes = { + location: React.PropTypes.object, + children: React.PropTypes.node +}; diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx index f07b50756..280e53ce4 100644 --- a/web/react/components/team_signup_display_name_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx @@ -1,8 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; +import * as utils from '../../../utils/utils.jsx'; +import * as client from '../../../utils/client.jsx'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; @@ -133,4 +133,4 @@ TeamSignupDisplayNamePage.propTypes = { updateParent: React.PropTypes.func }; -export default injectIntl(TeamSignupDisplayNamePage);
\ No newline at end of file +export default injectIntl(TeamSignupDisplayNamePage); diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx index 790ec2e5d..c87d6ec07 100644 --- a/web/react/components/team_signup_email_item.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; +import * as Utils from '../../../utils/utils.jsx'; import {intlShape, injectIntl, defineMessages} from 'mm-intl'; diff --git a/web/react/components/signup_team_complete/components/team_signup_finished.jsx b/web/react/components/signup_team_complete/components/team_signup_finished.jsx new file mode 100644 index 000000000..fc5f756e7 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_finished.jsx @@ -0,0 +1,15 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class FinishedPage extends React.Component { + render() { + return ( + <FormattedMessage + id='signup_team_complete.completed' + defaultMessage="You've already completed the signup process for this invitation or this invitation has expired." + /> + ); + } +} diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx index 06c04854f..490a11040 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx @@ -1,12 +1,13 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from '../utils/client.jsx'; -import BrowserStore from '../stores/browser_store.jsx'; -import UserStore from '../stores/user_store.jsx'; -import Constants from '../utils/constants.jsx'; +import * as Client from '../../../utils/client.jsx'; +import BrowserStore from '../../../stores/browser_store.jsx'; +import UserStore from '../../../stores/user_store.jsx'; +import Constants from '../../../utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; const holders = defineMessages({ passwordError: { @@ -66,11 +67,11 @@ class TeamSignupPasswordPage extends React.Component { props.state.wizard = 'finished'; props.updateParent(props.state, true); - window.location.href = '/' + teamSignup.team.name + '/channels/town-square'; + browserHistory.push('/' + teamSignup.team.name + '/channels/town-square'); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { - window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); + browserHistory.push('/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name)); } else { this.setState({serverError: err.message}); $('#finish-button').button('reset'); @@ -211,4 +212,4 @@ TeamSignupPasswordPage.propTypes = { updateParent: React.PropTypes.func }; -export default injectIntl(TeamSignupPasswordPage);
\ No newline at end of file +export default injectIntl(TeamSignupPasswordPage); diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx index 55cfe5114..5e987ef2c 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import EmailItem from './team_signup_email_item.jsx'; -import * as Client from '../utils/client.jsx'; +import * as Client from '../../../utils/client.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx index 2f6c3df49..ec50e2d25 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx @@ -1,9 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; -import * as Client from '../utils/client.jsx'; -import Constants from '../utils/constants.jsx'; +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import Constants from '../../../utils/constants.jsx'; import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; @@ -202,4 +202,4 @@ TeamSignupUrlPage.propTypes = { updateParent: React.PropTypes.func }; -export default injectIntl(TeamSignupUrlPage);
\ No newline at end of file +export default injectIntl(TeamSignupUrlPage); diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx index 0fa9cb103..e56aa4cd7 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx @@ -1,9 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; -import * as Client from '../utils/client.jsx'; -import Constants from '../utils/constants.jsx'; +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import Constants from '../../../utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -161,4 +161,4 @@ TeamSignupUsernamePage.propTypes = { updateParent: React.PropTypes.func }; -export default injectIntl(TeamSignupUsernamePage);
\ No newline at end of file +export default injectIntl(TeamSignupUsernamePage); diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx index 9939c3ffd..97782e54a 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx @@ -1,12 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; -import * as Client from '../utils/client.jsx'; -import BrowserStore from '../stores/browser_store.jsx'; +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import BrowserStore from '../../../stores/browser_store.jsx'; import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; + const holders = defineMessages({ storageError: { id: 'team_signup_welcome.storageError', @@ -73,7 +75,7 @@ class TeamSignupWelcomePage extends React.Component { } else { this.props.state.wizard = 'finished'; this.props.updateParent(this.props.state); - window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(email); + browserHistory.push('/signup_team_confirm/?email=' + encodeURIComponent(email)); } }.bind(this), function error(err) { @@ -229,4 +231,4 @@ TeamSignupWelcomePage.propTypes = { state: React.PropTypes.object }; -export default injectIntl(TeamSignupWelcomePage);
\ No newline at end of file +export default injectIntl(TeamSignupWelcomePage); diff --git a/web/react/components/signup_team_confirm.jsx b/web/react/components/signup_team_confirm.jsx index 290d8e503..1afbb3d30 100644 --- a/web/react/components/signup_team_confirm.jsx +++ b/web/react/components/signup_team_confirm.jsx @@ -6,30 +6,41 @@ import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; export default class SignupTeamConfirm extends React.Component { render() { return ( - <div className='signup-team__container'> - <h3> - <FormattedMessage - id='signup_team_confirm.title' - defaultMessage='Sign up Complete' - /> - </h3> - <p> - <FormattedHTMLMessage - id='signup_team_confirm.checkEmail' - defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team' - values={{ - email: this.props.email - }} - /> - </p> + <div> + <div className='signup-header'> + <a href='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + <div className='col-sm-12'> + <div classNameName='signup-team__container'> + <h3> + <FormattedMessage + id='signup_team_confirm.title' + defaultMessage='Sign up Complete' + /> + </h3> + <p> + <FormattedHTMLMessage + id='signup_team_confirm.checkEmail' + defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team' + values={{ + email: this.props.location.query.email + }} + /> + </p> + </div> + </div> </div> ); } } SignupTeamConfirm.defaultProps = { - email: '' }; SignupTeamConfirm.propTypes = { - email: React.PropTypes.string + location: React.PropTypes.object }; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index dbec3d02d..d2128a50f 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -2,83 +2,130 @@ // See License.txt for license information. import * as Utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; +import * as Client from '../utils/client.jsx'; import UserStore from '../stores/user_store.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import Constants from '../utils/constants.jsx'; +import LoadingScreen from '../components/loading_screen.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -const holders = defineMessages({ - required: { - id: 'signup_user_completed.required', - defaultMessage: 'This field is required' - }, - validEmail: { - id: 'signup_user_completed.validEmail', - defaultMessage: 'Please enter a valid email address' - }, - reserved: { - id: 'signup_user_completed.reserved', - defaultMessage: 'This username is reserved, please choose a new one.' - }, - usernameLength: { - id: 'signup_user_completed.usernameLength', - defaultMessage: 'Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.' - }, - passwordLength: { - id: 'signup_user_completed.passwordLength', - defaultMessage: 'Please enter at least {min} characters' - } -}); +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; class SignupUserComplete extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); + this.inviteInfoRecieved = this.inviteInfoRecieved.bind(this); + + this.state = { + data: '', + hash: '', + usedBefore: false, + email: '', + teamDisplayName: '', + teamName: '', + teamId: '' + }; + } + componentWillMount() { + let data = this.props.location.query.d; + let hash = this.props.location.query.h; + const inviteId = this.props.location.query.id; + let usedBefore = false; + let email = ''; + let teamDisplayName = ''; + let teamName = ''; + let teamId = ''; + + // If we have a hash in the url then we are attempting to access a private team + if (hash) { + const parsedData = JSON.parse(data); + usedBefore = BrowserStore.getGlobalItem(hash); + email = parsedData.email; + teamDisplayName = parsedData.display_name; + teamName = parsedData.name; + teamId = parsedData.id; + } else { + Client.getInviteInfo(this.inviteInfoRecieved, null, inviteId); + data = ''; + hash = ''; + } - var initialState = BrowserStore.getGlobalItem(this.props.hash); - - if (!initialState) { - initialState = {}; - initialState.wizard = 'welcome'; - initialState.user = {}; - initialState.user.team_id = this.props.teamId; - initialState.user.email = this.props.email; - initialState.original_email = this.props.email; + this.setState({ + data, + hash, + usedBefore, + email, + teamDisplayName, + teamName, + teamId + }); + } + inviteInfoRecieved(data) { + if (!data) { + return; } - this.state = initialState; + this.setState({ + teamDisplayName: data.display_name, + teamName: data.name, + teamId: data.id + }); } handleSubmit(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; const providedEmail = ReactDOM.findDOMNode(this.refs.email).value.trim(); if (!providedEmail) { - this.setState({nameError: '', emailError: formatMessage(holders.required), passwordError: ''}); + this.setState({ + nameError: '', + emailError: (<FormattedMessage id='signup_user_completed.required'/>), + passwordError: '', + serverError: '' + }); return; } if (!Utils.isEmail(providedEmail)) { - this.setState({nameError: '', emailError: formatMessage(holders.validEmail), passwordError: ''}); + this.setState({ + nameError: '', + emailError: (<FormattedMessage id='signup_user_completed.validEmail'/>), + passwordError: '', + serverError: '' + }); return; } const providedUsername = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase(); if (!providedUsername) { - this.setState({nameError: formatMessage(holders.required), emailError: '', passwordError: '', serverError: ''}); + this.setState({ + nameError: (<FormattedMessage id='signup_user_completed.required'/>), + emailError: '', + passwordError: '', + serverError: '' + }); return; } const usernameError = Utils.isValidUsername(providedUsername); if (usernameError === 'Cannot use a reserved word as a username.') { - this.setState({nameError: formatMessage(holders.reserved), emailError: '', passwordError: '', serverError: ''}); + this.setState({ + nameError: (<FormattedMessage id='signup_user_completed.reserved'/>), + emailError: '', + passwordError: '', + serverError: '' + }); return; } else if (usernameError) { this.setState({ - nameError: formatMessage(holders.usernameLength, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH}), + nameError: ( + <FormattedMessage + id='signup_user_completed.usernameLength' + min={Constants.MIN_USERNAME_LENGTH} + max={Constants.MAX_USERNAME_LENGTH} + /> + ), emailError: '', passwordError: '', serverError: '' @@ -88,41 +135,50 @@ class SignupUserComplete extends React.Component { const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) { - this.setState({nameError: '', emailError: '', passwordError: formatMessage(holders.passwordLength, {min: Constants.MIN_PASSWORD_LENGTH}), serverError: ''}); + this.setState({ + nameError: '', + emailError: '', + passwordError: ( + <FormattedMessage + id='signup_user_completed.passwordLength' + min={Constants.MIN_PASSWORD_LENGTH} + /> + ), + serverError: '' + }); return; } - const user = { - team_id: this.props.teamId, - email: providedEmail, - username: providedUsername, - password: providedPassword, - allow_marketing: true - }; - this.setState({ - user, nameError: '', emailError: '', passwordError: '', serverError: '' }); - client.createUser(user, this.props.data, this.props.hash, + const user = { + team_id: this.state.teamId, + email: providedEmail, + username: providedUsername, + password: providedPassword, + allow_marketing: true + }; + + Client.createUser(user, this.state.data, this.state.hash, () => { - client.track('signup', 'signup_user_02_complete'); + Client.track('signup', 'signup_user_02_complete'); - client.loginByEmail(this.props.teamName, user.email, user.password, + Client.loginByEmail(this.state.teamName, user.email, user.password, () => { UserStore.setLastEmail(user.email); - if (this.props.hash > 0) { - BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); + if (this.state.hash > 0) { + BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); } - window.location.href = '/' + this.props.teamName + '/channels/town-square'; + browserHistory.push('/' + this.state.teamName + '/channels/town-square'); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { - window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName); + browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName)); } else { this.setState({serverError: err.message}); } @@ -135,9 +191,10 @@ class SignupUserComplete extends React.Component { ); } render() { - client.track('signup', 'signup_user_01_welcome'); + Client.track('signup', 'signup_user_01_welcome'); - if (this.state.wizard === 'finished') { + // If we have been used then just display a message + if (this.state.usedBefore) { return ( <div> <FormattedMessage @@ -148,6 +205,12 @@ class SignupUserComplete extends React.Component { ); } + // If we haven't got a team id yet we are waiting for + // the client so just show the standard loading screen + if (this.state.teamId === '') { + return (<LoadingScreen/>); + } + // set up error labels var emailError = null; var emailHelpText = ( @@ -160,7 +223,7 @@ class SignupUserComplete extends React.Component { ); var emailDivStyle = 'form-group'; if (this.state.emailError) { - emailError = <label className='control-label'>{this.state.emailError}</label>; + emailError = (<label className='control-label'>{this.state.emailError}</label>); emailHelpText = ''; emailDivStyle += ' has-error'; } @@ -203,13 +266,13 @@ 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) { + if (this.state.email) { yourEmailIs = ( <FormattedHTMLMessage id='signup_user_completed.emailIs' defaultMessage="Your email address is <strong>{email}</strong>. You'll use this address to sign in to {siteName}." values={{ - email: this.state.user.email, + email: this.state.email, siteName: global.window.mm_config.SiteName }} /> @@ -217,7 +280,7 @@ class SignupUserComplete extends React.Component { } var emailContainerStyle = 'margin--extra'; - if (this.state.original_email) { + if (this.state.email) { emailContainerStyle = 'hidden'; } @@ -234,7 +297,7 @@ class SignupUserComplete extends React.Component { type='email' ref='email' className='form-control' - defaultValue={this.state.user.email} + defaultValue={this.state.email} placeholder='' maxLength='128' autoFocus={true} @@ -249,20 +312,20 @@ class SignupUserComplete extends React.Component { var signupMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { signupMessage.push( - <a - className='btn btn-custom-login gitlab' - key='gitlab' - href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search} - > - <span className='icon'/> - <span> - <FormattedMessage - id='signup_user_completed.gitlab' - defaultMessage='with GitLab' - /> - </span> - </a> - ); + <a + className='btn btn-custom-login gitlab' + key='gitlab' + href={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='signup_user_completed.gitlab' + defaultMessage='with GitLab' + /> + </span> + </a> + ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { @@ -270,7 +333,7 @@ class SignupUserComplete extends React.Component { <a className='btn btn-custom-login google' key='google' - href={'/' + this.props.teamName + '/signup/google' + window.location.search} + href={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} > <span className='icon'/> <span> @@ -318,16 +381,16 @@ class SignupUserComplete extends React.Component { /> </strong></h5> <div className={passwordDivStyle}> - <input - type='password' - ref='password' - className='form-control' - placeholder='' - maxLength='128' - spellCheck='false' - /> - {passwordError} - </div> + <input + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + spellCheck='false' + /> + {passwordError} + </div> </div> </div> <p className='margin--extra'> @@ -373,58 +436,56 @@ class SignupUserComplete extends React.Component { return ( <div> - <form> - <img - className='signup-team-logo' - src='/static/images/logo.png' - /> - <h5 className='margin--less'> - <FormattedMessage - id='signup_user_completed.welcome' - defaultMessage='Welcome to:' - /> - </h5> - <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> - <h2 className='signup-team__subdomain'> - <FormattedMessage - id='signup_user_completed.onSite' - defaultMessage='on {siteName}' - values={{ - siteName: global.window.mm_config.SiteName - }} - /> - </h2> - <h4 className='color--light'> - <FormattedMessage - id='signup_user_completed.lets' - defaultMessage="Let's create your account" - /> - </h4> - {signupMessage} - {emailSignup} - {serverError} - </form> + <div className='signup-header'> + <a href='/'> + <span classNameNameName='fa fa-chevron-left'/> + <FormattedMessage id='web.header.back'/> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container padding--less'> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h5 className='margin--less'> + <FormattedMessage + id='signup_user_completed.welcome' + defaultMessage='Welcome to:' + /> + </h5> + <h2 className='signup-team__name'>{this.state.teamName}</h2> + <h2 className='signup-team__subdomain'> + <FormattedMessage + id='signup_user_completed.onSite' + defaultMessage='on {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </h2> + <h4 className='color--light'> + <FormattedMessage + id='signup_user_completed.lets' + defaultMessage="Let's create your account" + /> + </h4> + {signupMessage} + {emailSignup} + {serverError} + </form> + </div> + </div> </div> ); } } SignupUserComplete.defaultProps = { - teamName: '', - hash: '', - teamId: '', - email: '', - data: null, - teamDisplayName: '' }; SignupUserComplete.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - hash: React.PropTypes.string, - teamId: React.PropTypes.string, - email: React.PropTypes.string, - data: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + location: React.PropTypes.object }; -export default injectIntl(SignupUserComplete);
\ No newline at end of file +export default SignupUserComplete; diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx index 064b75ac5..c5bd13c26 100644 --- a/web/react/components/suggestion/at_mention_provider.jsx +++ b/web/react/components/suggestion/at_mention_provider.jsx @@ -40,7 +40,7 @@ class AtMentionSuggestion extends React.Component { icon = ( <img className='mention-img' - src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at} /> ); } diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx index ea9f835eb..12b098cbd 100644 --- a/web/react/components/suggestion/suggestion_box.jsx +++ b/web/react/components/suggestion/suggestion_box.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import Constants from '../../utils/constants.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import SuggestionStore from '../../stores/suggestion_store.jsx'; import * as Utils from '../../utils/utils.jsx'; @@ -48,7 +48,7 @@ export default class SuggestionBox extends React.Component { if (!(container.is(e.target) || container.has(e.target).length > 0)) { // we can't just use blur for this because it fires and hides the children before // their click handlers can be called - EventHelpers.emitClearSuggestions(this.suggestionId); + GlobalActions.emitClearSuggestions(this.suggestionId); } } @@ -57,7 +57,7 @@ export default class SuggestionBox extends React.Component { const caret = Utils.getCaretPosition(textbox); const pretext = textbox.value.substring(0, caret); - EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext); + GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext); if (this.props.onUserInput) { this.props.onUserInput(textbox.value); @@ -89,13 +89,13 @@ export default class SuggestionBox extends React.Component { handleKeyDown(e) { if (SuggestionStore.hasSuggestions(this.suggestionId)) { if (e.which === KeyCodes.UP) { - EventHelpers.emitSelectPreviousSuggestion(this.suggestionId); + GlobalActions.emitSelectPreviousSuggestion(this.suggestionId); e.preventDefault(); } else if (e.which === KeyCodes.DOWN) { - EventHelpers.emitSelectNextSuggestion(this.suggestionId); + GlobalActions.emitSelectNextSuggestion(this.suggestionId); e.preventDefault(); } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) { - EventHelpers.emitCompleteWordSuggestion(this.suggestionId); + GlobalActions.emitCompleteWordSuggestion(this.suggestionId); e.preventDefault(); } else if (this.props.onKeyDown) { this.props.onKeyDown(e); diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx index e3ccd0f08..ccebeb990 100644 --- a/web/react/components/suggestion/suggestion_list.jsx +++ b/web/react/components/suggestion/suggestion_list.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import SuggestionStore from '../../stores/suggestion_store.jsx'; export default class SuggestionList extends React.Component { @@ -36,7 +36,7 @@ export default class SuggestionList extends React.Component { } handleItemClick(term, e) { - EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term); + GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term); e.preventDefault(); } diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx index 9bdb16438..786e8f947 100644 --- a/web/react/components/team_members_modal.jsx +++ b/web/react/components/team_members_modal.jsx @@ -10,8 +10,36 @@ import {FormattedMessage} from 'mm-intl'; const Modal = ReactBootstrap.Modal; export default class TeamMembersModal extends React.Component { + constructor(props) { + super(props); + + this.teamChanged = this.teamChanged.bind(this); + + this.state = { + team: TeamStore.getCurrent() + }; + } + componentDidMount() { + if (this.props.show) { + this.onShow(); + } + + TeamStore.addChangeListener(this.teamChanged); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.teamChanged); + } + + teamChanged() { + this.setState({team: TeamStore.getCurrent()}); + } + render() { - const team = TeamStore.getCurrent(); + let teamDisplayName = ''; + if (this.state.team) { + teamDisplayName = this.state.team.display_name; + } let maxHeight = 1000; if (Utils.windowHeight() <= 1200) { @@ -29,7 +57,7 @@ export default class TeamMembersModal extends React.Component { id='team_member_modal.members' defaultMessage='{team} Members' values={{ - team: team.display_name + team: teamDisplayName }} /> </Modal.Header> diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index e3207d573..0eb9d1211 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -28,6 +28,9 @@ export default class TeamSettings extends React.Component { } } render() { + if (!this.state.team) { + return null; + } var result; switch (this.props.activeTab) { case 'general': diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index 7dd645b25..a81b22d90 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -5,6 +5,7 @@ import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; const holders = defineMessages({ emailError: { @@ -47,9 +48,9 @@ class EmailSignUpPage extends React.Component { Client.signupTeam(team.email, (data) => { if (data.follow_link) { - window.location.href = data.follow_link; + browserHistory.push(data.follow_link); } else { - window.location.href = `/signup_team_confirm/?email=${encodeURIComponent(team.email)}`; + browserHistory.push(`/signup_team_confirm/?email=${encodeURIComponent(team.email)}`); } }, (err) => { @@ -117,4 +118,4 @@ EmailSignUpPage.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(EmailSignUpPage);
\ No newline at end of file +export default injectIntl(EmailSignUpPage); diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx index d8442e770..1ca40687f 100644 --- a/web/react/components/user_list_row.jsx +++ b/web/react/components/user_list_row.jsx @@ -32,7 +32,7 @@ export default function UserListRow({user, actions}) { > <img className='profile-img' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} /> <div className='user-list-item__details' diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 31b2b9907..e7a286b77 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -26,22 +26,27 @@ export default class UserProfile extends React.Component { } } render() { - var name = Utils.displayUsername(this.props.user.id); - if (this.props.overwriteName) { - name = this.props.overwriteName; - } else if (!name) { - name = '...'; + let name = '...'; + let email = ''; + let profileImg = ''; + if (this.props.user) { + name = Utils.displayUsername(this.props.user.id); + email = this.props.user.email; + profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at; } - if (this.props.disablePopover) { - return <div>{name}</div>; + if (this.props.overwriteName) { + name = this.props.overwriteName; } - var profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at + '&' + Utils.getSessionIndex(); if (this.props.overwriteImage) { profileImg = this.props.overwriteImage; } + if (this.props.disablePopover) { + return <div>{name}</div>; + } + var dataContent = []; dataContent.push( <img @@ -69,14 +74,14 @@ export default class UserProfile extends React.Component { dataContent.push( <div data-toggle='tooltip' - title={this.props.user.email} + title={email} key='user-popover-email' > <a - href={'mailto:' + this.props.user.email} + href={'mailto:' + email} className='text-nowrap text-lowercase user-popover__email' > - {this.props.user.email} + {email} </a> </div> ); @@ -114,7 +119,7 @@ UserProfile.defaultProps = { disablePopover: false }; UserProfile.propTypes = { - user: React.PropTypes.object.isRequired, + user: React.PropTypes.object, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx index 2d1c74717..6b00a65c7 100644 --- a/web/react/components/user_settings/manage_languages.jsx +++ b/web/react/components/user_settings/manage_languages.jsx @@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx'; import * as Client from '../../utils/client.jsx'; import * as Utils from '../../utils/utils.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component { submitUser(user) { Client.updateUser(user, () => { - window.location.reload(true); + GlobalActions.newLocalizationSelected(user.locale); }, (err) => { let serverError; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 0acfd4a16..1dd564c8d 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,7 +3,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -28,7 +28,7 @@ class DeveloperTab extends React.Component { } register() { this.props.closeModal(); - EventHelpers.showRegisterAppModal(); + GlobalActions.showRegisterAppModal(); } render() { var appSection; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b0b1c414e..235892819 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; const holders = defineMessages({ usernameReserved: { @@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component { <SettingPicture title={formatMessage(holders.profilePicture)} submit={this.submitPicture} - src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} server_error={serverError} client_error={clientError} updateSection={(e) => { @@ -729,7 +729,14 @@ class UserSettingsGeneralTab extends React.Component { let minMessage = formatMessage(holders.uploadImage); if (user.last_picture_update) { minMessage = formatMessage(holders.imageUpdated, { - date: new Date(user.last_picture_update).toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + date: ( + <FormattedDate + value={new Date(user.last_picture_update)} + day='2-digit' + month='short' + year='numeric' + /> + ) }); } pictureSection = ( @@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsGeneralTab);
\ No newline at end of file +export default injectIntl(UserSettingsGeneralTab); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index fa3415988..0c4a3d526 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component { this.updateTab = this.updateTab.bind(this); this.updateSection = this.updateSection.bind(this); + this.onUserChanged = this.onUserChanged.bind(this); this.state = { active_tab: 'general', active_section: '', showConfirmModal: false, - enforceFocus: true + enforceFocus: true, + currentUser: UserStore.getCurrentUser() }; this.requireConfirm = false; } + onUserChanged() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + componentDidMount() { if (this.props.show) { this.handleShow(); } + UserStore.addChangeListener(this.onUserChanged); } componentDidUpdate(prevProps) { if (this.props.show && !prevProps.show) { this.handleShow(); } + UserStore.removeChangeListener(this.onUserChanged); } handleShow() { @@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component { render() { const {formatMessage} = this.props.intl; - var currentUser = UserStore.getCurrentUser(); - var isAdmin = Utils.isAdmin(currentUser.roles); + if (this.state.currentUser == null) { + return (<div/>); + } + var isAdmin = Utils.isAdmin(this.state.currentUser.roles); var tabs = []; tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index cba7ffdea..0b6b6c398 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; const holders = defineMessages({ currentPasswordError: { @@ -218,11 +218,24 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - const locale = global.window.mm_locale; const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { - date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) + date: ( + <FormattedDate + value={d} + day='2-digit' + month='short' + year='numeric' + /> + ), + time: ( + <FormattedTime + value={d} + hour12={hours12} + hour='2-digit' + minute='2-digit' + /> + ) }); updateSectionStatus = function updateSection() { @@ -251,7 +264,7 @@ class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email)} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service} > <FormattedMessage id='user.settings.security.switchEmail' @@ -269,7 +282,7 @@ class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GITLAB_SERVICE} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GITLAB_SERVICE} > <FormattedMessage id='user.settings.security.switchGitlab' @@ -287,7 +300,7 @@ class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GOOGLE_SERVICE} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GOOGLE_SERVICE} > <FormattedMessage id='user.settings.security.switchGoogle' @@ -456,4 +469,4 @@ SecurityTab.propTypes = { setEnforceFocus: React.PropTypes.func.isRequired }; -export default injectIntl(SecurityTab);
\ No newline at end of file +export default injectIntl(SecurityTab); diff --git a/web/react/package.json b/web/react/package.json index 07ffa0cdf..509c9967b 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -11,6 +11,8 @@ "marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca", "mm-intl": "mattermost/mm-intl#805442fd474fa40cd586ddeda404dbbe8e60626d", "object-assign": "4.0.1", + "react": "0.14.3", + "react-router": "2.0.0", "twemoji": "1.4.1" }, "devDependencies": { diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx deleted file mode 100644 index 989936d9e..000000000 --- a/web/react/pages/admin_console.jsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ErrorBar from '../components/error_bar.jsx'; -import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; -import AdminController from '../components/admin_console/admin_controller.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <div> - <ErrorBar/> - <AdminController - tab={this.props.map.ActiveTab} - teamId={this.props.map.TeamId} - /> - <SelectTeamModal/> - </div> - </IntlProvider> - ); - } -} - -global.window.setup_admin_console_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('admin_controller') - ); -}; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx deleted file mode 100644 index bc78c049c..000000000 --- a/web/react/pages/channel.jsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelView from '../components/channel_view.jsx'; -import ChannelLoader from '../components/channel_loader.jsx'; -import ErrorBar from '../components/error_bar.jsx'; -import * as Client from '../utils/client.jsx'; - -import GetPostLinkModal from '../components/get_post_link_modal.jsx'; -import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; -import EditPostModal from '../components/edit_post_modal.jsx'; -import DeletePostModal from '../components/delete_post_modal.jsx'; -import MoreChannelsModal from '../components/more_channels.jsx'; -import TeamSettingsModal from '../components/team_settings_modal.jsx'; -import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; -import RegisterAppModal from '../components/register_app_modal.jsx'; -import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; -import InviteMemberModal from '../components/invite_member_modal.jsx'; - -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <div className='channel-view'> - <ChannelLoader/> - <ErrorBar/> - <ChannelView/> - <GetPostLinkModal/> - <GetTeamInviteLinkModal/> - <InviteMemberModal/> - <ImportThemeModal/> - <TeamSettingsModal/> - <MoreChannelsModal/> - <EditPostModal/> - <DeletePostModal/> - <RemovedFromChannelModal/> - <RegisterAppModal/> - </div> - </IntlProvider> - ); - } -} - -global.window.setup_channel_page = function setup(props, team, channel) { - if (props.PostId === '') { - EventHelpers.emitChannelClickEvent(channel); - } else { - EventHelpers.emitPostFocusEvent(props.PostId); - } - - ReactDOM.render( - <Root map={props}/>, - document.getElementById('channel_view') - ); -}; diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx deleted file mode 100644 index abbf72ea3..000000000 --- a/web/react/pages/claim_account.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ClaimAccount from '../components/claim/claim_account.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <ClaimAccount - email={this.props.map.Email} - currentType={this.props.map.CurrentType} - newType={this.props.map.NewType} - teamName={this.props.map.TeamName} - teamDisplayName={this.props.map.TeamDisplayName} - /> - </IntlProvider> - ); - } -} - -global.window.setup_claim_account_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('claim') - ); -};
\ No newline at end of file diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx deleted file mode 100644 index 2e47e3e6a..000000000 --- a/web/react/pages/docs.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Docs from '../components/docs.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <Docs site={this.props.map.Site}/> - </IntlProvider> - ); - } -} - -global.window.mm_user = global.window.mm_user || {}; - -global.window.setup_documentation_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('docs') - ); -}; diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx deleted file mode 100644 index 93394fcde..000000000 --- a/web/react/pages/find_team.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import FindTeam from '../components/find_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <FindTeam/> - </IntlProvider> - ); - } -} - -global.window.setup_find_team_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('find-team') - ); -};
\ No newline at end of file diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx deleted file mode 100644 index ff81c4994..000000000 --- a/web/react/pages/home.jsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import TeamStore from '../stores/team_store.jsx'; -import Constants from '../utils/constants.jsx'; - -function setupHomePage() { - var last = null; - if (last == null || last.length === 0) { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; - } else { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; - } -} - -global.window.setup_home_page = setupHomePage; diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx deleted file mode 100644 index ec9080945..000000000 --- a/web/react/pages/login.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../utils/client.jsx'; -import Login from '../components/login.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <Login - teamDisplayName={this.props.map.TeamDisplayName} - teamName={this.props.map.TeamName} - inviteId={this.props.map.InviteId} - /> - </IntlProvider> - ); - } -} - -global.window.setup_login_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('login') - ); -};
\ No newline at end of file diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx deleted file mode 100644 index 7caff5034..000000000 --- a/web/react/pages/password_reset.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordReset from '../components/password_reset.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <PasswordReset - isReset={this.props.map.IsReset} - teamDisplayName={this.props.map.TeamDisplayName} - teamName={this.props.map.TeamName} - hash={this.props.map.Hash} - data={this.props.map.Data} - /> - </IntlProvider> - ); - } -} - -global.window.setup_password_reset_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('reset') - ); -}; diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx new file mode 100644 index 000000000..d0b06e32e --- /dev/null +++ b/web/react/pages/root.jsx @@ -0,0 +1,290 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import Root from '../components/root.jsx'; +import Login from '../components/login.jsx'; +import LoggedIn from '../components/logged_in.jsx'; +import NotLoggedIn from '../components/not_logged_in.jsx'; +import NeedsTeam from '../components/needs_team.jsx'; +import PasswordResetSendLink from '../components/password_reset_send_link.jsx'; +import PasswordResetForm from '../components/password_reset_form.jsx'; +import ChannelView from '../components/channel_view.jsx'; +import Sidebar from '../components/sidebar.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import ErrorStore from '../stores/error_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import SignupTeam from '../components/signup_team.jsx'; +import * as Client from '../utils/client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; +import SignupUserComplete from '../components/signup_user_complete.jsx'; +import ShouldVerifyEmail from '../components/should_verify_email.jsx'; +import DoVerifyEmail from '../components/do_verify_email.jsx'; +import AdminConsole from '../components/admin_console/admin_controller.jsx'; +import ClaimAccount from '../components/claim/claim_account.jsx'; + +import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx'; +import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx'; +import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx'; +import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx'; +import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx'; +import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx'; +import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx'; +import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx'; + +// This is for anything that needs to be done for ALL react components. +// This runs before we start to render anything. +function preRenderSetup(callwhendone) { + const d1 = Client.getClientConfig( + (data, textStatus, xhr) => { + if (!data) { + return; + } + + global.window.mm_config = data; + + var serverVersion = xhr.getResponseHeader('X-Version-ID'); + + if (serverVersion !== BrowserStore.getLastServerVersion()) { + if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') { + BrowserStore.setLastServerVersion(serverVersion); + } else { + BrowserStore.setLastServerVersion(serverVersion); + window.location.reload(true); + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console + } + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientConfig'); + } + ); + + const d2 = Client.getClientLicenceConfig( + (data) => { + if (!data) { + return; + } + + global.window.mm_license = data; + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientLicenceConfig'); + } + ); + + // Set these here so they don't fail in client.jsx track + global.window.analytics = {}; + global.window.analytics.page = () => { + // Do Nothing + }; + global.window.analytics.track = () => { + // Do Nothing + }; + + $.when(d1, d2).done(callwhendone); +} + +function preLoggedIn(nextState, replace, callback) { + const d1 = Client.getAllPreferences( + (data) => { + if (!data) { + return; + } + + PreferenceStore.setPreferences(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'getAllPreferences'); + } + ); + + const d2 = AsyncClient.getChannels(); + + $.when(d1, d2).done(() => callback()); +} + +function onChannelChange(nextState) { + const channelName = nextState.params.channel; + + // Make sure we have all the channels + AsyncClient.getChannels(true); + + // Get our channel's ID + const channel = ChannelStore.getByName(channelName); + + // User clicked channel + GlobalActions.emitChannelClickEvent(channel); +} + +function onRootEnter(nextState, replace, callback) { + if (nextState.location.pathname === '/') { + Client.getMeLoggedIn((data) => { + if (!data || data.logged_in === 'false') { + replace({pathname: '/signup_team'}); + callback(); + } else { + replace({pathname: '/' + data.team_name + '/channels/town-square'}); + callback(); + } + }); + return; + } + + callback(); +} + +function onPermalinkEnter(nextState) { + const postId = nextState.params.postid; + + GlobalActions.emitPostFocusEvent(postId); +} + +function onLoggedOut(nextState) { + const teamName = nextState.params.team; + Client.logout( + () => { + browserHistory.push('/' + teamName + '/login'); + BrowserStore.signalLogout(); + BrowserStore.clear(); + ErrorStore.clearLastError(); + }, + () => { + browserHistory.push('/' + teamName + '/login'); + } + ); +} + +function renderRootComponent() { + ReactDOM.render(( + <Router + history={browserHistory} + > + <Route + path='/' + component={Root} + onEnter={onRootEnter} + > + <Route + component={LoggedIn} + onEnter={preLoggedIn} + > + <Route + path=':team/channels/:channel' + onEnter={onChannelChange} + components={{ + sidebar: Sidebar, + center: ChannelView + }} + /> + <Route + path=':team/pl/:postid' + onEnter={onPermalinkEnter} + components={{ + sidebar: Sidebar, + center: ChannelView + }} + /> + <Route + path=':team/logout' + onEnter={onLoggedOut} + components={{ + sidebar: null, + center: null + }} + /> + <Route + path='admin_console' + components={{ + sidebar: null, + center: AdminConsole + }} + /> + </Route> + <Route component={NotLoggedIn}> + <Route + path='signup_team' + component={SignupTeam} + /> + <Route + path='signup_team_complete' + component={SignupTeamComplete} + > + <IndexRoute component={FinishedPage}/> + <Route + path='welcome' + component={WelcomePage} + /> + <Route + path='team_display_name' + component={TeamDisplayNamePage} + /> + <Route + path='team_url' + component={TeamURLPage} + /> + <Route + path='invites' + component={SendInivtesPage} + /> + <Route + path='username' + component={UsernamePage} + /> + <Route + path='password' + component={PasswordPage} + /> + </Route> + <Route + path='signup_user_complete' + component={SignupUserComplete} + /> + <Route + path='signup_team_confirm' + component={SignupTeamConfirm} + /> + <Route + path='should_verify_email' + component={ShouldVerifyEmail} + /> + <Route + path='do_verify_email' + component={DoVerifyEmail} + /> + <Route + path=':team' + component={NeedsTeam} + > + <IndexRedirect to='login'/> + <Route + path='login' + component={Login} + /> + <Route + path='claim' + component={ClaimAccount} + /> + <Route + path='reset_password' + component={PasswordResetSendLink} + /> + <Route + path='reset_password_complete' + component={PasswordResetForm} + /> + </Route> + </Route> + </Route> + </Router> + ), + document.getElementById('root')); +} + +global.window.setup_root = () => { + // Do the pre-render setup and call renderRootComponent when done + preRenderSetup(renderRootComponent); +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx deleted file mode 100644 index f276c3ff7..000000000 --- a/web/react/pages/signup_team.jsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeam from '../components/signup_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired, - teams: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <SignupTeam teams={this.props.teams}/> - </IntlProvider> - ); - } -} - -global.window.setup_signup_team_page = function setup(props) { - var teams = []; - - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') { - teams.push({name: prop, display_name: props[prop]}); - } - } - } - - ReactDOM.render( - <Root - map={props} - teams={teams} - />, - document.getElementById('signup-team') - ); -};
\ No newline at end of file diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx deleted file mode 100644 index 8c237f698..000000000 --- a/web/react/pages/signup_team_complete.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamComplete from '../components/signup_team_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <SignupTeamComplete - email={this.props.map.Email} - hash={this.props.map.Hash} - data={this.props.map.Data} - /> - </IntlProvider> - ); - } -} - -global.window.setup_signup_team_complete_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('signup-team-complete') - ); -};
\ No newline at end of file diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx deleted file mode 100644 index 13c8f3fd0..000000000 --- a/web/react/pages/signup_team_confirm.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <SignupTeamConfirm - email={this.props.map.Email} - /> - </IntlProvider> - ); - } -} - -global.window.setup_signup_team_confirm_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('signup-team-confirm') - ); -};
\ No newline at end of file diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx deleted file mode 100644 index a14f2140b..000000000 --- a/web/react/pages/signup_user_complete.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupUserComplete from '../components/signup_user_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <SignupUserComplete - teamId={this.props.map.TeamId} - teamName={this.props.map.TeamName} - teamDisplayName={this.props.map.TeamDisplayName} - email={this.props.map.Email} - hash={this.props.map.Hash} - data={this.props.map.Data} - /> - </IntlProvider> - ); - } -} - -global.window.setup_signup_user_complete_page = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('signup-user-complete') - ); -};
\ No newline at end of file diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx deleted file mode 100644 index 6b336daa1..000000000 --- a/web/react/pages/verify.jsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import EmailVerify from '../components/email_verify.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return <div></div>; - } - - return ( - <IntlProvider - locale={this.props.map.Locale} - messages={this.state.translations} - > - <EmailVerify - isVerified={this.props.map.IsVerified} - teamURL={this.props.map.TeamURL} - userEmail={this.props.map.UserEmail} - resendSuccess={this.props.map.ResendSuccess} - /> - </IntlProvider> - ); - } -} - -global.window.setupVerifyPage = function setup(props) { - ReactDOM.render( - <Root map={props}/>, - document.getElementById('verify') - ); -}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 5c911e94b..9f7f6e7ff 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter { } getSelectedTeams() { - return BrowserStore.getItem('seleted_teams'); + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; } saveSelectedTeams(teams) { @@ -156,7 +160,3 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { }); export default AdminStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AdminStore = AdminStore; -} diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx index 0ad989206..ec827f6d7 100644 --- a/web/react/stores/analytics_store.jsx +++ b/web/react/stores/analytics_store.jsx @@ -83,7 +83,3 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { }); export default AnalyticsStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AnalyticsStore = AnalyticsStore; -} diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 3417faaaf..3b35916b3 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -4,8 +4,8 @@ import {generateId} from '../utils/utils.jsx'; function getPrefix() { - if (global.window.mm_user) { - return global.window.mm_user.id + '_'; + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; } return 'unknown_'; @@ -31,7 +31,9 @@ class BrowserStoreClass { this.isSignallingLogout = this.isSignallingLogout.bind(this); this.signalLogin = this.signalLogin.bind(this); this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + checkVersion() { var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index eac24b071..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -350,7 +350,3 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { }); export default ChannelStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ChannelStore = ChannelStore; -} diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx index c1fd0ef74..6d7e0f354 100644 --- a/web/react/stores/file_store.jsx +++ b/web/react/stores/file_store.jsx @@ -57,9 +57,4 @@ class FileStore extends EventEmitter { } } -const instance = new FileStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.FileStore = instance; -} +export default new FileStore(); diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx new file mode 100644 index 000000000..0e3a63724 --- /dev/null +++ b/web/react/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index 1819fffc0..5ea38030b 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -45,7 +45,3 @@ class ModalStoreClass extends EventEmitter { const ModalStore = new ModalStoreClass(); export default ModalStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ModalStore = ModalStore; -} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 5cc3f300d..a6dfcd46f 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -608,7 +608,3 @@ function isPostListNull(pl) { return false; } - -if (window.mm_config.EnableDeveloper === 'true') { - window.PostStore = PostStore; -} diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 96071665c..549f355ef 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -135,7 +135,3 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { }); export default SearchStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SearchStore = SearchStore; -} diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..ad24a04cd 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,7 +10,7 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; @@ -42,10 +42,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,7 +50,7 @@ class SocketStoreClass extends EventEmitter { protocol = 'wss://'; } - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex(); + var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console @@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg, translations) { // Store post const post = JSON.parse(msg.props.post); - EventHelpers.emitPostRecievedEvent(post); + GlobalActions.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { @@ -291,7 +287,7 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - EventHelpers.emitPostDeletedEvent(post); + GlobalActions.emitPostDeletedEvent(post); } function handleNewUserEvent() { @@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) { function handlePreferenceChangedEvent(msg) { const preference = JSON.parse(msg.props.preference); - EventHelpers.emitPreferenceChangedEvent(preference); + GlobalActions.emitPreferenceChangedEvent(preference); } var SocketStore = new SocketStoreClass(); diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index 487bae843..efd2b76ed 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -258,9 +258,4 @@ class SuggestionStore extends EventEmitter { } } -const instance = new SuggestionStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SuggestionStore = instance; -} +export default new SuggestionStore(); diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 493d6bc4d..354a07b72 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; const CHANGE_EVENT = 'change'; @@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter { this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; } emitChange() { @@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return BrowserStore.getItem('user_teams', {}); + return this.teams; } getCurrentId() { - var team = global.window.mm_team; + var team = this.get(this.currentTeamId); if (team) { return team.id; @@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter { } getCurrent() { - if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { - this.saveTeam(global.window.mm_team); + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; } - return global.window.mm_team; + return null; } getCurrentTeamUrl() { @@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - var teams = this.getAll(); - teams[team.id] = team; - BrowserStore.setItem('user_teams', teams); + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; } } @@ -116,17 +127,16 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_TEAM: - TeamStore.saveTeam(action.team); + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - default: } }); export default TeamStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.TeamStore = TeamStore; -} diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 9fcd2440e..c1e5c75dc 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; -const CHANGE_EVENT_TEAMS = 'change_teams'; const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); this.profileCache = null; + this.currentUserId = ''; } emitChange(userId) { @@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_AUDITS, callback); } - emitTeamsChange() { - this.emit(CHANGE_EVENT_TEAMS); - } - - addTeamsChangeListener(callback) { - this.on(CHANGE_EVENT_TEAMS, callback); - } - - removeTeamsChangeListener(callback) { - this.removeListener(CHANGE_EVENT_TEAMS, callback); - } - emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } @@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter { } getCurrentUser() { - if (this.getProfiles()[global.window.mm_user.id] == null) { - this.saveProfile(global.window.mm_user); - } - - return global.window.mm_user; + return this.getProfiles()[this.currentUserId]; } setCurrentUser(user) { - var oldUser = global.window.mm_user; - - if (oldUser.id === user.id) { - global.window.mm_user = user; - this.saveProfile(user); - } else { - throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); - } + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; } getCurrentId() { - var user = global.window.mm_user; + var user = this.getCurrentUser(); if (user) { return user.id; @@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter { saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (currentId in profiles) { - delete profiles[currentId]; + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; } - this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } @@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('audits', {loading: true}); } - setTeams(teams) { - BrowserStore.setItem('teams', teams); - } - - getTeams() { - return BrowserStore.getItem('teams', []); - } - getCurrentMentionKeys() { return this.getMentionKeys(this.getCurrentId()); } @@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.setAudits(action.audits); UserStore.emitAuditsChange(); break; - case ActionTypes.RECEIVED_TEAMS: - UserStore.setTeams(action.teams); - UserStore.emitTeamsChange(); - break; case ActionTypes.RECEIVED_STATUSES: UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); @@ -325,7 +303,3 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { }); export {UserStore as default}; - -if (window.mm_config.EnableDeveloper === 'true') { - window.UserStore = UserStore; -} diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 7d5e1bd0f..b9770a6e9 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as client from './client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -44,15 +45,19 @@ function isCallInProgress(callName) { export function getChannels(checkVersion) { if (isCallInProgress('getChannels')) { - return; + return null; } callTracker.getChannels = utils.getTimestamp(); - client.getChannels( + return client.getChannels( (data, textStatus, xhr) => { callTracker.getChannels = 0; + if (xhr.status === 304 || !data) { + return; + } + if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); @@ -67,10 +72,6 @@ export function getChannels(checkVersion) { } } - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNELS, channels: data.channels, @@ -392,36 +393,6 @@ export function getAllTeams() { ); } -export function findTeams(email) { - if (isCallInProgress('findTeams_' + email)) { - return; - } - - var user = UserStore.getCurrentUser(); - if (user) { - callTracker['findTeams_' + email] = utils.getTimestamp(); - client.findTeams( - user.email, - function findTeamsSuccess(data, textStatus, xhr) { - callTracker['findTeams_' + email] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAMS, - teams: data - }); - }, - function findTeamsFailure(err) { - callTracker['findTeams_' + email] = 0; - dispatchError(err, 'findTeams'); - } - ); - } -} - export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; @@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) { export function getMe() { if (isCallInProgress('getMe')) { - return; + return null; } callTracker.getMe = utils.getTimestamp(); - client.getMe( + return client.getMe( (data, textStatus, xhr) => { callTracker.getMe = 0; @@ -661,6 +632,8 @@ export function getMe() { type: ActionTypes.RECEIVED_ME, me: data }); + + GlobalActions.newLocalizationSelected(data.locale); }, (err) => { callTracker.getMe = 0; @@ -706,11 +679,11 @@ export function getStatuses() { export function getMyTeam() { if (isCallInProgress('getMyTeam')) { - return; + return null; } callTracker.getMyTeam = utils.getTimestamp(); - client.getMyTeam( + return client.getMyTeam( function getMyTeamSuccess(data, textStatus, xhr) { callTracker.getMyTeam = 0; @@ -719,7 +692,7 @@ export function getMyTeam() { } AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM, + type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index ed94f94b8..94f3f0ce0 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl'; @@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) { <div className='post-profile-img__container channel-intro-img'> <img className='post-profile-img' - src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()} + src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at} height='50' width='50' /> @@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) { } export function createDefaultIntroMessage(channel) { - const team = TeamStore.getCurrent(); - let inviteModalLink; - if (team.type === Constants.INVITE_TEAM) { - inviteModalLink = ( - <a - className='intro-links' - href='#' - onClick={EventHelpers.showInviteMemberModal} - > - <i className='fa fa-user-plus'></i> - <FormattedMessage - id='intro_messages.inviteOthers' - defaultMessage='Invite others to this team' - /> - </a> - ); - } else { - inviteModalLink = ( - <a - className='intro-links' - href='#' - onClick={EventHelpers.showGetTeamInviteLinkModal} - > - <i className='fa fa-user-plus'></i> - <FormattedMessage - id='intro_messages.inviteOthers' - defaultMessage='Invite others to this team' - /> - </a> - ); - } + const inviteModalLink = ( + <a + className='intro-links' + href='#' + onClick={GlobalActions.showGetTeamInviteLinkModal} + > + <i className='fa fa-user-plus'></i> + <FormattedMessage + id='intro_messages.inviteOthers' + defaultMessage='Invite others to this team' + /> + </a> + ); return ( <div className='channel-intro'> diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76d42137a..e00f28a14 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1,8 +1,8 @@ // See License.txt for license information. import BrowserStore from '../stores/browser_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import ErrorStore from '../stores/error_store.jsx'; + +import {browserHistory} from 'react-router'; let translations = { connectionError: 'There appears to be a problem with your internet connection.', @@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) { if (xhr.status === 401) { if (window.location.href.indexOf('/channels') === 0) { - window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } else { - var teamURL = window.location.href.split('/channels')[0]; - window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + var teamURL = window.location.pathname.split('/channels')[0]; + browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } } @@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) { track('api', 'api_users_switch_to_email'); } -export function logout() { +export function logout(success, error) { track('api', 'api_users_logout'); - var currentTeamUrl = TeamStore.getCurrentTeamUrl(); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - window.location.href = currentTeamUrl + '/logout'; + $.ajax({ + url: '/api/v1/users/logout', + type: 'POST', + success, + error: function onError(xhr, status, err) { + var e = handleError('logout', xhr, status, err); + error(e); + } + }); } export function loginByEmail(name, email, password, success, error) { @@ -437,7 +441,7 @@ export function getServerAudits(success, error) { } export function getConfig(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/admin/config', dataType: 'json', contentType: 'application/json', @@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) { } else { url += teamId + '/' + name; } - $.ajax({ url, dataType: 'json', @@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) { }); } +export function getClientConfig(success, error) { + return $.ajax({ + url: '/api/v1/admin/client_props', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientConfig', xhr, status, err); + error(e); + } + }); +} + +export function getTeamAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', @@ -529,6 +560,21 @@ export function getAllTeams(success, error) { }); } +export function getMeLoggedIn(success, error) { + return $.ajax({ + cache: false, + url: '/api/v1/users/me_logged_in', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getMeLoggedIn', xhr, status, err); + error(e); + } + }); +} + export function getMe(success, error) { var currentUser = null; $.ajax({ @@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) { }); } -export function findTeamsSendEmail(email, success, error) { - $.ajax({ - url: '/api/v1/teams/email_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamsSendEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_email_teams'); -} - -export function findTeams(email, success, error) { - $.ajax({ - url: '/api/v1/teams/find_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeams', xhr, status, err); - error(e); - } - }); -} - export function createChannel(channel, success, error) { $.ajax({ url: '/api/v1/channels/create', @@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) { } export function getChannels(success, error) { - $.ajax({ + return $.ajax({ cache: false, url: '/api/v1/channels/', dataType: 'json', @@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { url += '/' + memberLimit; } - $.ajax({ + return $.ajax({ url, dataType: 'json', contentType: 'application/json', @@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) } export function getPosts(channelId, since, success, error, complete) { - $.ajax({ + return $.ajax({ url: '/api/v1/channels/' + channelId + '/posts/' + since, dataType: 'json', type: 'GET', @@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) { } export function getMyTeam(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/teams/me', dataType: 'json', type: 'GET', @@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) { } export function getAllPreferences(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/preferences/', dataType: 'json', type: 'GET', @@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) { track('api', 'api_license_upload'); } + +export function getClientLicenceConfig(success, error) { + return $.ajax({ + url: '/api/v1/license/client_config', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientLicenceConfig', xhr, status, err); + error(e); + } + }); +} + +export function getInviteInfo(success, error, id) { + $.ajax({ + url: '/api/v1/teams/get_invite_info', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({invite_id: id}), + success, + error: function onError(xhr, status, err) { + var e = handleError('getInviteInfo', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function verifyEmail(success, error, uid, hid) { + $.ajax({ + url: '/api/v1/users/verify_email', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({uid, hid}), + success, + error: function onError(xhr, status, err) { + var e = handleError('verifyEmail', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function resendVerification(success, error, teamName, email) { + $.ajax({ + url: '/api/v1/users/resend_verification', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({team_name: teamName, email}), + success, + error: function onError(xhr, status, err) { + var e = handleError('resendVerification', xhr, status, err); + if (error) { + error(e); + } + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e..2cff4dbed 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -42,13 +42,15 @@ export default { RECEIVED_MSG: null, - RECEIVED_TEAM: null, + RECEIVED_MY_TEAM: null, RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_LOCALE: null, + SHOW_SEARCH: null, TOGGLE_IMPORT_THEME_MODAL: null, @@ -143,6 +145,7 @@ export default { EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', + SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 6942a8e08..88777164b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; @@ -941,7 +942,7 @@ export function updateAddressBar(channelName) { } export function switchChannel(channel) { - EventHelpers.emitChannelClickEvent(channel); + GlobalActions.emitChannelClickEvent(channel); updateAddressBar(channel.name); @@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) { // Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. export function getFileUrl(filename, isDownload) { - const downloadParam = isDownload ? '&download=1' : ''; - return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam; + const downloadParam = isDownload ? '?download=1' : ''; + return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam; } // Gets the name of a file (including extension) from a given url or file path. @@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) { return ''; } -export function getSessionIndex() { - if (global.window.mm_session_token_index >= 0) { - return 'session_token_index=' + global.window.mm_session_token_index; - } - - return ''; -} - // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 @@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) { export function getRootId(post) { return post.root_id === '' ? post.id : post.root_id; } + +export function localizeMessage(id, defaultMessage) { + const translations = LocalizationStore.getTranslations(); + if (translations) { + const value = translations[id]; + if (value) { + return value; + } + } + + if (defaultMessage) { + return defaultMessage; + } + + return id; +} diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5e7f04724..44681291c 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -24,12 +24,6 @@ padding: 1em 1em 0; display: none; } - > div { - height: 100%; - position: absolute; - padding-bottom: 70px; - width: 100%; - } .badge { background-color: $primary-color; position: absolute; diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md deleted file mode 100644 index 2063ad41c..000000000 --- a/web/static/help/Messaging_en.md +++ /dev/null @@ -1,47 +0,0 @@ -# Messaging - -### Writing Messages - -You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost. - -Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message. - -### Formatting Messages - -Mattermost messages are formatted using a standard called "markdown". Here are examples: - -| Text Entered | How it appears | -|:---------------|:---------------| -|`**bold**`| **bold** | -| `_italic_`|_italic_| -|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)| -|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/). - - -### Mentioning Teammates - -You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention. - -For example, you might write: - -``` -@alice how did your interview go with the new candidate? -``` - -Which sends a special mention notification to **alice** to check your message. - -To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned. - -You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences** - -### Messages Dropdown Menu - -To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message: - -- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread. -- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives. -- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message. -- **Edit:** Lets you edit your own message. diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md deleted file mode 100644 index d3947f36a..000000000 --- a/web/static/help/Messaging_es.md +++ /dev/null @@ -1,37 +0,0 @@ -# Mensajes - -## Escribiendo Mensajes - -Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost. - -Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje. - -## Darle formato a los Mensajes - -Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquà algunos ejemplos: - -| Texto escrito | Como aparece | -|:--------------|:-------------| -|`**negrita**`| **negrita** | -| `_italica_`|_italica_| -|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)| -|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Revisa la lista completa de Emojis [aquÃ](http://www.emoji-cheat-sheet.com/). - -## Mencionando a compañeros - -Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención. - -Por ejemplo, podrÃas escribir: - -``` -@alicia como te fue con la entrevista del nuevo candidato? -``` - -Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje. - -Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos. - -Puedes configurar como te gustarÃa ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación** diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index d2e340641..2a536925c 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -661,8 +661,10 @@ "email_signup.find": "Find my teams", "email_verify.almost": "{siteName}: You are almost done", "email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.", + "email_verify.verifyFailed": "Failed to verify your email.", "email_verify.resend": "Resend Email", "email_verify.sent": " Verification email sent.", + "email_verify.failed": " Failed to send verification email.", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -758,6 +760,7 @@ "login.or": "or", "login.signTo": "Sign in to:", "login.verified": " Email Verified", + "login.session_expired": " Your session has expired. Please login again.", "login_email.badTeam": "Bad team name", "login_email.email": "Email", "login_email.emailReq": "An email is required", @@ -822,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click <a href={url}>here</a> to log in.", - "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.", + "password_form.enter": "Enter a new password for your {siteName} account.", "password_form.error": "Please enter at least {chars} characters.", "password_form.pwd": "Password", "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", + "password_send.description": "To reset your password, enter the email address you used to sign up.", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", - "password_send.link": "<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>", + "password_send.link": "<p>A password reset link has been sent to <b>{email}</b></p>", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "â–² collapse text", @@ -1303,5 +1306,11 @@ "view_image.loading": "Loading ", "view_image_popover.download": "Download", "view_image_popover.file": "File {count} of {total}", - "view_image_popover.publicLink": "Get Public Link" + "view_image_popover.publicLink": "Get Public Link", + "web.footer.about": "About", + "web.footer.help": "Help", + "web.footer.privacy": "Privacy", + "web.footer.terms": "Terms", + "web.header.back": "Back", + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" } diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index c6b16a293..f42dc879a 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -822,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha <a href={url}>aquÃ</a> para iniciar sesión.", - "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.", + "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.", "password_form.error": "Por favor ingresa al menos {chars} caracteres.", "password_form.pwd": "Contraseña", "password_form.title": "Restablecer Contraseña", "password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.", + "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.", "password_send.email": "Correo electrónico", "password_send.error": "Por favor ingresa una dirección correo electrónico válida.", - "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>", + "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b></p>", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "â–² colapsar texto", @@ -1303,5 +1303,11 @@ "view_image.loading": "Cargando ", "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", - "view_image_popover.publicLink": "Obtener Enlace Público" + "view_image_popover.publicLink": "Obtener Enlace Público", + "web.footer.about": "Acerca", + "web.footer.help": "Ayuda", + "web.footer.privacy": "Privacidad", + "web.footer.terms": "Términos", + "web.header.back": "Atrás", + "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" } diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index b9b8f4c07..d276e339a 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -820,16 +820,15 @@ "navbar_dropdown.teamSettings": "Configurações da Equipe", "password_form.change": "Alterar minha senha", "password_form.click": "Clique <a href={url}>aqui</a> para logar.", - "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.", + "password_form.enter": "Entre uma nova senha para sua conta {siteName}.", "password_form.error": "Por favor, insira pelo menos {chars} caracteres.", "password_form.pwd": "Senha", "password_form.title": "Resetar Senha", "password_form.update": "Sua senha foi atualizada com sucesso.", "password_send.checkInbox": "Por favor verifique sua caixa de entrada.", - "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.", + "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.", "password_send.email": "E-mail", "password_send.error": "Por favor entre um endereço de e-mail válido.", - "password_send.link": "<p>Um link para resetar a sua senha na equipe <b>{teamDisplayName}</b> em {hostname} foi enviado para <b>{email}</b>.</p>", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "â–² recolher texto", diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493e..000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body> -<script src="/static/js/Chart.min.js"></script> - -<div id='admin_controller'></div> - -<script> - window.setup_admin_console_page({{ .Props }}); - - $(document).ready(function(){ - $('[data-toggle="tooltip"]').tooltip(); - $('[data-toggle="popover"]').popover(); - }); -</script> -</body> -</html> -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a022..000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body> - <div id="channel_view" class='channel-view'></div> -<script> - window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}); - $('body').tooltip( {selector: '[data-toggle=tooltip]'} ); - var modals = $('.modal-body').not('.edit-modal-body'); - if($(window).height() > 1200){ - modals.css('max-height', 1000); - } else { - modals.css('max-height', $(window).height() - 200); - } - modals.perfectScrollbar(); -</script> -</body> -</html> -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1b..000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container"> - <img class="signup-team-logo" src="/static/images/logo.png" /> - <div id="claim"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - <div> - </div> - <script> - window.setup_claim_account_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/docs.html b/web/templates/docs.html deleted file mode 100644 index dc18e5cb6..000000000 --- a/web/templates/docs.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "docs"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> -<div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="docs__page" id="docs"></div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> -</div> -<script> - window.setup_documentation_page({{ .Props }}); -</script> -</body> -</html> -{{end}} diff --git a/web/templates/find_team.html b/web/templates/find_team.html deleted file mode 100644 index b7e1d7eca..000000000 --- a/web/templates/find_team.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "find_team"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container"> - <img class="signup-team-logo" src="/static/images/logo.png" /> - <div id="find-team"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setup_find_team_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/footer.html b/web/templates/footer.html deleted file mode 100644 index 5b11328fb..000000000 --- a/web/templates/footer.html +++ /dev/null @@ -1,39 +0,0 @@ -{{define "footer"}} -<div class="footer-pane col-xs-12"> - <div class="col-xs-12"> - <span class="pull-right footer-site-name">{{ .ClientCfg.SiteName }}</span> - </div> - <div class="col-xs-12"> - <span class="pull-right footer-link copyright">© 2015 Mattermost, Inc.</span> - <a id="help_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterHelp }}</a> - <a id="terms_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterTerms }}</a> - <a id="privacy_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterPrivacy }}</a> - <a id="about_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterAbout }}</a> - </div> -</div> -<script> - if (window.mm_config.HelpLink) { - document.getElementById("help_link").setAttribute("href", window.mm_config.HelpLink); - } else { - $("#help_link").remove(); - } - - if (window.mm_config.TermsOfServiceLink) { - document.getElementById("terms_link").setAttribute("href", window.mm_config.TermsOfServiceLink); - } else { - $("#terms_link").remove(); - } - - if (window.mm_config.PrivacyPolicyLink) { - document.getElementById("privacy_link").setAttribute("href", window.mm_config.PrivacyPolicyLink); - } else { - $("#privacy_link").remove(); - } - - if (window.mm_config.AboutLink) { - document.getElementById("about_link").setAttribute("href", window.mm_config.AboutLink); - } else { - $("#about_link").remove(); - } -</script> -{{end}} diff --git a/web/templates/head.html b/web/templates/head.html deleted file mode 100644 index 61b1aa12b..000000000 --- a/web/templates/head.html +++ /dev/null @@ -1,191 +0,0 @@ -{{define "head"}} -<head> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <meta name="robots" content="noindex, nofollow"> - <meta name="referrer" content="no-referrer"> - - <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="{{ .Props.Title }}"> - <meta name="application-name" content="{{ .Props.Title }}"> - <meta name="format-detection" content="telephone=no"> - <!-- iOS add to homescreen --> - - <!-- Android add to homescreen --> - <link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png"> - <link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png"> - <link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png"> - <link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png"> - <link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png"> - <link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png"> - <link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png"> - <link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png"> - <link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png"> - <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png"> - <link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png"> - <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png"> - <link rel="manifest" href="/static/config/manifest.json"> - <!-- Android add to homescreen --> - - <!-- CSS Should always go first --> - <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> - <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css"> - <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css"> - <link rel="stylesheet" href="/static/css/styles.css"> - <link rel="stylesheet" href="/static/css/google-fonts.css"> - <link rel="stylesheet" href="/static/css/katex.min.css"> - <link rel="stylesheet" class="code_theme" href=""> - - <script src="/static/js/intl-1.0.0/Intl.js"></script> - <script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script> - <script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script> - <script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script> - - <script src="/static/js/react-0.14.3.js"></script> - <script src="/static/js/react-dom-0.14.3.js"></script> - <script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script> - <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script> - <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script> - <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script> - <script src="/static/js/jquery-2.1.4.js"></script> - <script src="/static/js/bootstrap-3.3.5.js"></script> - <script src="/static/js/bootstrap-colorpicker.min.js"></script> - <script src="/static/js/react-bootstrap-0.28.1.js"></script> - <script src="/static/js/velocity.min.js"></script> - <script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script> - <script src="/static/js/jquery-dragster/jquery.dragster.js"></script> - <script src="/static/js/babel-polyfill-6.1.18.min.js"></script> - <script src="/static/js/katex.min.js"></script> - - <style id="antiClickjack">body{display:none !important;}</style> - - <script> - if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) { - Object.keys(ReactIntlLocaleData).forEach(function(lang) { - ReactIntl.addLocaleData(ReactIntlLocaleData[lang]); - }); - } - - window.mm_config = {{ .ClientCfg }}; - window.mm_license = {{ .ClientLicense }}; - window.mm_team = {{ .Team }}; - window.mm_user = {{ .User }}; - window.mm_channel = {{ .Channel }}; - window.mm_locale = {{ .Locale }}; - window.mm_preferences = {{ .Preferences }}; - - $(function() { - if (window.mm_preferences != null) { - PreferenceStore.setPreferences(window.mm_preferences); - PreferenceStore.emitChange(); - } - }); - - if ({{.SessionTokenIndex}} >= 0) { - window.mm_session_token_index = {{.SessionTokenIndex}}; - $.ajaxSetup({ - headers: { - 'X-MM-TokenIndex': mm_session_token_index, - 'Accept-Language': mm_locale - } - }); - } else { - $.ajaxSetup({ - headers: { - 'Accept-Language': mm_locale - } - }); - } - - $(function () { - $(window).bind('storage', function (e) { - // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out - if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { - // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { - return; - } - - console.log('detected logout from a different tab'); - window.location.href = '/' + window.mm_team.name; - } - - if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { - // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { - return; - } - - console.log('detected login from a different tab'); - location.reload(); - } - }); - }); - - $(window).on('beforeunload', function(){ - if (window.SocketStore) { - SocketStore.close(); - } - }); - </script> - - <script> - window.onerror = function(msg, url, line, column, stack) { - var l = {}; - l.level = 'ERROR'; - l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; - - $.ajax({ - url: '/api/v1/admin/log_client', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(l) - }); - - if (window.mm_config.EnableDeveloper === 'true') { - window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'}); - window.ErrorStore.emitChange(); - } - } - </script> - - <script src="/static/js/libs.min.js"></script> - <script src="/static/js/bundle.js"></script> - - <script type="text/javascript"> - if (self === top) { - var blocker = document.getElementById("antiClickjack"); - blocker.parentNode.removeChild(blocker); - } - </script> - - <script type="text/javascript"> - if (window.mm_config.SegmentDeveloperKey != null && window.mm_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.mm_config.SegmentDeveloperKey); - if (window.mm_user) { - analytics.identify(window.mm_user.id, { - name: window.mm_user.nickname, - email: window.mm_user.email, - createdAt: window.mm_user.create_at, - username: window.mm_user.username, - team_id: window.mm_user.team_id, - id: window.mm_user.id - }); - } - analytics.page(); - }}(); - } else { - analytics = {}; - analytics.page = function(){}; - analytics.track = function(){}; - } - </script> -</head> -{{end}} diff --git a/web/templates/home.html b/web/templates/home.html deleted file mode 100644 index 08876d41d..000000000 --- a/web/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "home"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body> - <div class="container-fluid"> - <div class="sidebar--right" id="sidebar-right"></div> - <div class="sidebar--left" id="sidebar-left"></div> - <div class="inner__wrap"> - <div class="row header"> - <div id="navbar"></div> - </div> - <div class="row main"> - <div class="hidden-xs" id="sidebar"></div> - <div class="app__content"></div> - </div> - </div> - </div> - <script> - window.setup_home_page(); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index 88540a906..000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "login"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div id="login"></div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setup_login_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html deleted file mode 100644 index e68f8b693..000000000 --- a/web/templates/password_reset.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "password_reset"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container"> - <img class="signup-team-logo" src="/static/images/logo.png" /> - <div id="reset"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setup_password_reset_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html deleted file mode 100644 index afba58066..000000000 --- a/web/templates/signup_team.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="col-sm-12"> - <div class="signup-team__container"> - <img class="signup-team-logo" src="/static/images/logo.png" /> - <h1>{{ .ClientCfg.SiteName }}</h1> - <h4 class="color--light">{{.Props.Info}}</h4> - <div id="signup-team"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> -window.setup_signup_team_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html deleted file mode 100644 index 3873d8978..000000000 --- a/web/templates/signup_team_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team_complete"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container"> - <div id="signup-team-complete"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> -window.setup_signup_team_complete_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html deleted file mode 100644 index 31f1ba95b..000000000 --- a/web/templates/signup_team_confirm.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_team_confirm"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div id="signup-team-confirm"></div> - </div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> -window.setup_signup_team_confirm_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html deleted file mode 100644 index 937a89dd2..000000000 --- a/web/templates/signup_user_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_user_complete"}} -<!DOCTYPE html> -<html> -{{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container padding--less"> - <div id="signup-user-complete"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setup_signup_user_complete_page({{ .Props }}); - </script> -</body> -</html> -{{end}} diff --git a/web/templates/verify.html b/web/templates/verify.html deleted file mode 100644 index 2e5496d7a..000000000 --- a/web/templates/verify.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "verify"}} -<!DOCTYPE html> -<html> - {{template "head" . }} - <body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a> - </div> - <div class="col-sm-12"> - <div class="signup-team__container"> - <img class="signup-team-logo" src="/static/images/logo.png" /> - <div id="verify"></div> - </div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setupVerifyPage({{ .Props }}); - </script> - </body> -</html> -{{end}} diff --git a/web/web.go b/web/web.go index 09450b976..2a44ece00 100644 --- a/web/web.go +++ b/web/web.go @@ -4,67 +4,16 @@ package web import ( - "fmt" + "net/http" + "strings" + l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" - "gopkg.in/fsnotify.v1" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" ) -var Templates *template.Template - -type HtmlTemplatePage api.Page - -func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage { - - if len(title) > 0 { - title = utils.Cfg.TeamSettings.SiteName + " - " + title - } - - props := make(map[string]string) - props["Title"] = title - return &HtmlTemplatePage{ - TemplateName: templateName, - Props: props, - ClientCfg: utils.ClientCfg, - ClientLicense: utils.ClientLicense, - Locale: locale, - } -} - -func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { - if me.Team != nil { - me.Team.Sanitize() - } - - if me.User != nil { - me.User.Sanitize(map[string]bool{}) - me.Locale = me.User.Locale - } - - me.Props["Locale"] = me.Locale - me.SessionTokenIndex = c.SessionTokenIndex - - me.ClientCfg["HeaderBack"] = c.T("web.header.back") - me.ClientCfg["FooterHelp"] = c.T("web.footer.help") - me.ClientCfg["FooterTerms"] = c.T("web.footer.terms") - me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy") - me.ClientCfg["FooterAbout"] = c.T("web.footer.about") - - if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { - c.SetUnknownError(me.TemplateName, err.Error()) - } -} - func InitWeb() { l4g.Debug(utils.T("web.init.debug")) @@ -74,81 +23,7 @@ func InitWeb() { l4g.Debug("Using static directory at %v", staticDir) 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") - mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") - mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") - mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") - - mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET") - - mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") - - mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET") - - // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team specific should go below this line - // ---------------------------------------------------------------------------------------------- - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") - mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - - watchAndParseTemplates() -} - -func watchAndParseTemplates() { - - templatesDir := utils.FindDir("web/templates") - l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir) - var err error - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(utils.T("web.create_dir.error"), err) - } - - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - l4g.Info(utils.T("web.reparse_templates.info"), event.Name) - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - } - case err := <-watcher.Errors: - l4g.Error(utils.T("web.dir_fail.error"), err) - } - } - }() - - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(utils.T("web.watcher_fail.error"), err) - } + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" @@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if len(c.Session.UserId) == 0 { - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Props["Info"] = c.T("web.root.singup_info") - - if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - for _, team := range teams { - page.Props[team.Name] = team.DisplayName - } - - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { - http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) - return - } - } - - page.Render(c, w) - } else { - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale) - page.Team = team - page.User = user - page.Render(c, w) - } -} - -func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !CheckBrowserCompatability(c, r) { - return - } - - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Render(c, w) -} - -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - params := mux.Vars(r) - teamName := params["team"] - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - // We still might be able to switch to this team because we've logged in before - _, session := api.FindMultiSessionForTeamId(r, team.Id) - if session != nil { - w.Header().Set(model.HEADER_TOKEN, session.Token) - lastViewChannelName := "town-square" - if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil { - if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil { - lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name - } - } - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect) - return - } - - page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale) - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - if team.AllowOpenInvite { - page.Props["InviteId"] = team.InviteId - } - - page.Render(c, w) -} - -func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale) - page.Props["Email"] = email - page.Render(c, w) -} - -func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - data := r.FormValue("d") - hash := r.FormValue("h") - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "") - return - } - - page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - - id := r.FormValue("id") - data := r.FormValue("d") - hash := r.FormValue("h") - var props map[string]string - - if len(id) > 0 { - props = make(map[string]string) - - if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { - c.Err = result.Err - return - } else { - team := result.Data.(*model.Team) - if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { - c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id) - return - } - - props["email"] = "" - props["display_name"] = team.DisplayName - props["name"] = team.Name - props["id"] = team.Id - data = model.MapToJson(props) - hash = "" - } - } else { - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "") - return - } - - props = model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "") - return - } - } - - page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["TeamDisplayName"] = props["display_name"] - page.Props["TeamName"] = props["name"] - page.Props["TeamId"] = props["id"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { - api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) -} - -func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - teamName := params["team"] - postId := params["postid"] - - if len(postId) != 26 { - c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId) - return - } - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var post *model.Post - if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil { - c.Err = result.Err - return - } else { - postlist := result.Data.(*model.PostList) - post = postlist.Posts[postlist.Order[0]] - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - if result.Data.(int64) == 0 { - if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, post.Id) -} - -func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - name := params["channelname"] - teamName := params["team"] - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - channelId := result.Data.(string) - if len(channelId) == 0 { - if channel = autoJoinChannelName(c, w, r, name); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, "") -} - -func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel { - if strings.Index(channelName, "__") > 0 { - // It's a direct message channel that doesn't exist yet so let's create it - ids := strings.Split(channelName, "__") - otherUserId := "" - if ids[0] == c.Session.UserId { - otherUserId = ids[1] - } else { - otherUserId = ids[0] - } - - if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { - api.Handle404(w, r) - return nil - } else { - return sc - } - } else { - // We will attempt to auto-join open channels - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName)) - } - - return nil -} - -func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel { - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId)) -} - -func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel { - if cr := <-channel; cr.Err != nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } else { - channel := cr.Data.(*model.Channel) - if channel.Type == model.CHANNEL_OPEN { - api.JoinChannel(c, channel.Id, "") - if c.Err != nil { - return nil - } - } else { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } - return channel - } -} - -func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team { - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - // We are logged into a different team. Lets see if we have another - // session in the cookie that will give us access. - if c.Session.TeamId != team.Id { - index, session := api.FindMultiSessionForTeamId(r, team.Id) - if session == nil { - // redirect to login - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) - } else { - c.Session = *session - c.SessionTokenIndex = index - } - } - - return team -} - -func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId) - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - c.RemoveSessionCookie(w, r) - l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId) - return - } else { - user = ur.Data.(*model.User) - } - - var preferences model.Preferences - if result := <-prefChan; result.Err != nil { - l4g.Error("Error in getting preferences for id=%v", c.Session.UserId) - } else { - preferences = result.Data.(model.Preferences) - } - - page := NewHtmlTemplatePage("channel", "", c.Locale) - page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["ChannelName"] = channel.Name - page.Props["ChannelId"] = channel.Id - page.Props["PostId"] = postid - page.Team = team - page.User = user - page.Channel = channel - page.Preferences = &preferences - page.Render(c, w) -} - -func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { - resend := r.URL.Query().Get("resend") - resendSuccess := r.URL.Query().Get("resend_success") - name := r.URL.Query().Get("teamname") - email := r.URL.Query().Get("email") - hashedId := r.URL.Query().Get("hid") - userId := r.URL.Query().Get("uid") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if resend == "true" { - if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user := result.Data.(*model.User) - - if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } else { - api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } - - newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) - http.Redirect(w, r, newAddress, http.StatusFound) - return - } - } - - if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) { - if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) - return - } - } - - page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale) - page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) - page.Props["UserEmail"] = email - page.Props["ResendSuccess"] = resendSuccess - page.Render(c, w) -} - -func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { - page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale) - page.Render(c, w) -} - -func docs(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - doc := params["doc"] - - var user *model.User - if len(c.Session.UserId) != 0 { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - if userChan := <-userChan; userChan.Err == nil { - user = userChan.Data.(*model.User) - } - } - - page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale) - page.Props["Site"] = doc - page.User = user - page.Render(c, w) -} - -func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { - isResetLink := true - hash := r.URL.Query().Get("h") - data := r.URL.Query().Get("d") - params := mux.Vars(r) - teamName := params["team"] - - if len(hash) == 0 || len(data) == 0 { - isResetLink = false - } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "") - return - } - } - - teamDisplayName := "Developer/Beta" - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - - if team != nil { - teamDisplayName = team.DisplayName - } - - page := NewHtmlTemplatePage("password_reset", "", c.Locale) - page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = teamDisplayName - page.Props["TeamName"] = teamName - page.Props["Hash"] = hash - page.Props["Data"] = data - page.Props["TeamName"] = teamName - page.Props["IsReset"] = strconv.FormatBool(isResetLink) - page.Render(c, w) -} - -func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - - if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - hash := r.URL.Query().Get("h") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if api.IsVerifyHashRequired(nil, team, hash) { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") - return - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") - return - } - - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return - } - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) - - if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { - c.Err = err - return - } else { - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - api.CreateOAuthUser(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_LOGIN: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - } - } -} - -func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - loginHint := r.URL.Query().Get("login_hint") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !c.HasSystemAdminPermissions("adminConsole") { - return - } - - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - params := mux.Vars(r) - activeTab := params["tab"] - teamId := params["team"] - - page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale) - page.User = user - page.Team = team - page.Props["ActiveTab"] = activeTab - page.Props["TeamId"] = teamId - page.Render(c, w) -} - -func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") - 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.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = result.Err - return - } else { - app = result.Data.(*model.OAuthApp) - } - - var team *model.Team - if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) + page := utils.NewHTMLTemplate("root", c.Locale) + page.Props["Title"] = c.T("web.root.home_title") + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) } - - page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale) - 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.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") - 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.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - 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.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := api.Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") - 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.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } else { - app = result.Data.(*model.OAuthApp) - } - - if !model.ComparePassword(app.ClientSecret, secret) { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } - - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } - - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - 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(utils.T("web.get_access_token.revoking.error") + err.Message) - } - - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") - return - } - - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - 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.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - 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.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - - 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())) -} - -func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - params := mux.Vars(r) - id := params["id"] - - hchan := api.Srv.Store.Webhook().GetIncoming(id) - - r.ParseForm() - - var parsedRequest *model.IncomingWebhookRequest - contentType := r.Header.Get("Content-Type") - if strings.Split(contentType, "; ")[0] == "application/json" { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) - } else { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) - } - - if parsedRequest == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") - return - } - - text := parsedRequest.Text - if len(text) == 0 && parsedRequest.Attachments == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "") - return - } - - channelName := parsedRequest.ChannelName - webhookType := parsedRequest.Type - - //attachments is in here for slack compatibility - if parsedRequest.Attachments != nil { - if len(parsedRequest.Props) == 0 { - parsedRequest.Props = make(model.StringInterface) - } - parsedRequest.Props["attachments"] = parsedRequest.Attachments - webhookType = model.POST_SLACK_ATTACHMENT - } - - var hook *model.IncomingWebhook - if result := <-hchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message) - return - } else { - hook = result.Data.(*model.IncomingWebhook) - } - - var channel *model.Channel - var cchan store.StoreChannel - - if len(channelName) != 0 { - if channelName[0] == '@' { - if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) - return - } else { - channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) - } - } else if channelName[0] == '#' { - channelName = channelName[1:] - } - - cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) - } else { - cchan = api.Srv.Store.Channel().Get(hook.ChannelId) - } - - overrideUsername := parsedRequest.Username - overrideIconUrl := parsedRequest.IconURL - - if result := <-cchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) - return - } else { - channel = result.Data.(*model.Channel) - } - - pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) - - // create a mock session - c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} - - if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") - return - } - - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { - c.Err = err - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("ok")) -} - -func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - - params := mux.Vars(r) - teamName := params["team"] - email := r.URL.Query().Get("email") - newType := r.URL.Query().Get("new_type") - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - authType := "" - if len(email) != 0 { - if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { - l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - user := uResult.Data.(*model.User) - authType = user.AuthService - - // if user is not logged in to their SSO account, ask them to log in - if len(authType) != 0 && user.Id != c.Session.UserId { - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL - stateProps["email"] = email - - if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } - } - } - } - - page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale) - page.Props["Email"] = email - page.Props["CurrentType"] = authType - page.Props["NewType"] = newType - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - page.Render(c, w) } |