diff --git a/Makefile b/Makefile
index a7c277e4c0..57a28bf3ae 100644
--- a/Makefile
+++ b/Makefile
@@ -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 4fecd3dd4a..20f77e558d 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 edcdcbfeff..eed035dafb 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 23e7946c8e..542b45e266 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 0000000000..b34aeb7a63
--- /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 1ae3dbf78e..9b7f3699d5 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 cd78b16f0a..d0ec5826ab 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 2f680dc768..2559825226 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 c942e2e1f6..bbbc8385df 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -108,37 +108,10 @@ func TestCreateTeam(t *testing.T) {
}
}
-func TestFindTeamByEmail(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.FindTeams(user.Email); 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 _, 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 := &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"}
@@ -147,8 +120,22 @@ func TestGetAllTeams(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
- if _, err := Client.GetAllTeams(); err == nil {
- t.Fatal("you shouldn't have permissions")
+ 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].Email != "" {
+ t.Fatal("Non admin users shoudn't get full listings")
+ }
}
c := &Context{}
@@ -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 afabc21915..0000000000
--- 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 4fc4f48467..0000000000
--- 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 2f588aead5..0000000000
--- a/api/templates/error.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- Back - Error
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
deleted file mode 100644
index 1324091aa0..0000000000
--- a/api/templates/find_teams_body.html
+++ /dev/null
@@ -1,52 +0,0 @@
-{{define "find_teams_body"}}
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
- {{.Props.Title}}
- {{ if .Extra }}
- {{.Props.Found}}
- {{range $index, $element := .Extra}}
- {{ $index }}
- {{ end }}
- {{ else }}
- {{.Props.NotFound}}
- {{ end }}
-
- |
-
-
- {{template "email_info" . }}
-
-
- |
-
-
- {{template "email_footer" . }}
-
-
- |
-
-
- |
-
-
-
-{{end}}
-
-
-
diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html
deleted file mode 100644
index ebc339562a..0000000000
--- 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 60daaa432a..0000000000
--- 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 b7e6220d82..0841c38aa5 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 1a1cf9634a..27f00829fd 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 bc33fc019e..d16de7dbbf 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1623,6 +1623,10 @@
"id": "api.user.upload_profile_user.upload_profile.app_error",
"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"
@@ -3399,22 +3403,6 @@
"id": "web.find_team.title",
"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"
@@ -3547,10 +3535,6 @@
"id": "web.root.home_title",
"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 4c0c1fd039..93ffb23418 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -3399,22 +3399,6 @@
"id": "web.find_team.title",
"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"
@@ -3547,10 +3531,6 @@
"id": "web.root.home_title",
"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 560e47b76c..3adcb980d7 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 5d9424d641..bf0d9531e0 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 9e9eaa25f3..bed7bbd8da 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
similarity index 100%
rename from web/templates/authorize.html
rename to templates/authorize.html
diff --git a/api/templates/email_change_body.html b/templates/email_change_body.html
similarity index 100%
rename from api/templates/email_change_body.html
rename to templates/email_change_body.html
diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html
new file mode 100644
index 0000000000..540bc6eabb
--- /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
similarity index 100%
rename from api/templates/email_change_verify_body.html
rename to 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 0000000000..04da7593c4
--- /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
similarity index 100%
rename from api/templates/email_footer.html
rename to templates/email_footer.html
diff --git a/api/templates/email_info.html b/templates/email_info.html
similarity index 100%
rename from api/templates/email_info.html
rename to templates/email_info.html
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000000..b86039ca38
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,24 @@
+{{define "error"}}
+
+
+{{template "head" . }}
+
+
+
+
+
+{{end}}
diff --git a/templates/head.html b/templates/head.html
new file mode 100644
index 0000000000..a7eacc85ff
--- /dev/null
+++ b/templates/head.html
@@ -0,0 +1,92 @@
+{{define "head"}}
+
+
+
+
+
+ {{ .Props.Title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/api/templates/invite_body.html b/templates/invite_body.html
similarity index 100%
rename from api/templates/invite_body.html
rename to templates/invite_body.html
diff --git a/api/templates/invite_subject.html b/templates/invite_subject.html
similarity index 100%
rename from api/templates/invite_subject.html
rename to templates/invite_subject.html
diff --git a/api/templates/password_change_body.html b/templates/password_change_body.html
similarity index 100%
rename from api/templates/password_change_body.html
rename to templates/password_change_body.html
diff --git a/api/templates/password_change_subject.html b/templates/password_change_subject.html
similarity index 100%
rename from api/templates/password_change_subject.html
rename to templates/password_change_subject.html
diff --git a/api/templates/post_body.html b/templates/post_body.html
similarity index 100%
rename from api/templates/post_body.html
rename to templates/post_body.html
diff --git a/templates/post_subject.html b/templates/post_subject.html
new file mode 100644
index 0000000000..9789d4142c
--- /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
similarity index 100%
rename from api/templates/reset_body.html
rename to templates/reset_body.html
diff --git a/api/templates/reset_subject.html b/templates/reset_subject.html
similarity index 100%
rename from api/templates/reset_subject.html
rename to templates/reset_subject.html
diff --git a/templates/root.html b/templates/root.html
new file mode 100644
index 0000000000..560c7a4b08
--- /dev/null
+++ b/templates/root.html
@@ -0,0 +1,12 @@
+{{define "root"}}
+
+
+{{template "head" . }}
+
+
+
+
+
+{{end}}
diff --git a/api/templates/signin_change_body.html b/templates/signin_change_body.html
similarity index 100%
rename from api/templates/signin_change_body.html
rename to templates/signin_change_body.html
diff --git a/api/templates/signin_change_subject.html b/templates/signin_change_subject.html
similarity index 100%
rename from api/templates/signin_change_subject.html
rename to templates/signin_change_subject.html
diff --git a/api/templates/signup_team_body.html b/templates/signup_team_body.html
similarity index 100%
rename from api/templates/signup_team_body.html
rename to templates/signup_team_body.html
diff --git a/api/templates/signup_team_subject.html b/templates/signup_team_subject.html
similarity index 100%
rename from api/templates/signup_team_subject.html
rename to templates/signup_team_subject.html
diff --git a/api/templates/verify_body.html b/templates/verify_body.html
similarity index 100%
rename from api/templates/verify_body.html
rename to templates/verify_body.html
diff --git a/api/templates/verify_subject.html b/templates/verify_subject.html
similarity index 100%
rename from api/templates/verify_subject.html
rename to templates/verify_subject.html
diff --git a/api/templates/welcome_body.html b/templates/welcome_body.html
similarity index 100%
rename from api/templates/welcome_body.html
rename to templates/welcome_body.html
diff --git a/api/templates/welcome_subject.html b/templates/welcome_subject.html
similarity index 100%
rename from api/templates/welcome_subject.html
rename to templates/welcome_subject.html
diff --git a/utils/html.go b/utils/html.go
new file mode 100644
index 0000000000..4203160d5d
--- /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 5c975aec2d..b1f15ad928 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
similarity index 89%
rename from web/react/dispatcher/event_helpers.jsx
rename to web/react/action_creators/global_actions.jsx
index 367347d4ba..4375d6c878 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 95b4caa127..db366f8ed1 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: (
+
+ ),
+ time: (
+
+ )
}}
/>
@@ -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: (
+
+ ),
+ time: (
+
+ )
}}
/>
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 32ed70a995..4c4f21f08b 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 (
-
+
);
}
-}
\ 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 4af350bcde..7d6cfb5c38 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 {
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
index 47eee6d3fa..917093840b 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 = (
+
+
+ {' - '}
+
+
+ );
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 2422588cfe..2ea840c1ec 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()))
};
}
+ validState() {
+ return this.state.user && this.state.channel && this.state.profiles;
+ }
+ onStoresChange() {
+ this.setState(this.getStateFromStores());
+ }
componentDidMount() {
- PreferenceStore.addChangeListener(this.onPreferenceChange);
- ChannelStore.addChangeListener(this.onChannelChange);
- UserStore.addChangeListener(this.onUserChange);
+ PreferenceStore.addChangeListener(this.onStoresChange);
+ ChannelStore.addChangeListener(this.onStoresChange);
+ UserStore.addChangeListener(this.onStoresChange);
}
componentWillUnmount() {
- PreferenceStore.removeChangeListener(this.onPreferenceChange);
- ChannelStore.removeChangeListener(this.onChannelChange);
- UserStore.removeChangeListener(this.onUserChange);
- }
- onPreferenceChange() {
- const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
- this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
- }
- onChannelChange() {
- this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
- }
- onUserChange() {
- this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
+ 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 51be13dcf4..882c575f07 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 {
);
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 {
|