diff --git a/CHANGELOG.md b/CHANGELOG.md index 8606fc72c0..08b5ddbf07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,130 @@ # Mattermost Changelog +## Release v2.1.0 + +Expected release date: 2016-03-16 + +### Highlights + +- New Android application now available. +- New desktop applications for Windows, Mac and Linux now in beta. +- Brazilian Portuguese translation added. + +### New Features + +Android Application + +- New [Mattermost Android App](https://github.com/mattermost/android) supporting push notifications available for devices running Android 4.4.2+. Requires Mattermost server 2.1 and higher. See [list of tested devices](https://github.com/mattermost/android/blob/master/DEVICES.md). + +Desktop Application + +- New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release. + +Languages + +- Added Portuguese language translation (Beta) available from **Account Settings** > **Display**. + +### Improvements + +System Console + +- Removed unused “Disable File Storage” option from the System Console as it is no longer relevant. +- Added a warning message if a system admin demotes themselves. +- System Console statistics now use a client store instead of fetching data and storing it in state. + +Messaging + +- Custom slash commands now support temporary messages that appear only to the user that issued the command. +- Username autocomplete list no longer suggests inactive users. + +Mobile + +- Significant responsiveness and speed improvements using [fastclick](https://github.com/ftlabs/fastclick). +- Team name and username are now shown in the LHS header. +- Added a button to go back to the team URL page from the login page. + +Files and Images + +- Increased the maximum size of image uploads to 24 megapixels. + +User Interface + +- Custom theme color selectors are now organized into categories. +- Add Members and Manage Members dialogs can now be filtered using a search bar. +- Deactivated members no longer appear in the channel members list. +- Keyboard focus is set to the text input box in the RHS if a user clicks the reply icon. +- Permalinks are now displayed in a Copy Permalink dialog instead of a popover. +- Permalink option is now available from the [...] menu on messages and comments in the RHS. +- Reply icon now only appears on-hover for messages that don’t have replies. +- Scroll bar now appears in the center channel. + +#### Bug Fixes + +- System console user management tab now shows username and email on different lines. +- Yellow text box error no longer appears when the system is connected. +- Wildcard search on MySQL databases is now fixed. +- Usernames in the center channel no longer appear as “...” on login. +- Deleted messages now delete in the RHS and center channel without requiring a page refresh. +- Contact us email address in the footer of notification emails now uses the SupportEmail config setting instead of FeedbackEmail. +- Email addresses are now required to have at least one letter before and after the @ sign. +- Firefox desktop notifications are now fixed for some users experiencing missed notifications. +- “User is typing” message containing long usernames no longer causes text wrapping. +- Usernames appearing as “...” in the RHS when performing a search is fixed. +- Links that end in image extensions but do not actually link to raw images no longer generate a blank image preview. +- Channel handle field in the Rename Channel dialog is now visible on themes with dark backgrounds. +- Autolinked images no longer persist after the post containing the link is deleted. +- Code theme selector on IE11 now only shows one dropdown arrow and clicking directly on the arrow opens the dropdown. +- Save/Cancel buttons for language selection in Account Settings are now formatted the same as other settings. +- Inconsistent field spacing in the Channel Info dialog is fixed. +- Recent mentions icon no longer jumps to the left of the search bar when the RHS is opened. +- Custom slash command hints now show up in the autocomplete list. +- GIF links inside code blocks no longer auto-post the GIFs. +- Changing usernames no longer adds the old username to “words that trigger mentions”. +- Notification email footer is now translated based on the sender’s language setting. +- Slash command `/me` now posts as the user instead of a webhook message. +- Logout slash command now forces logout. +- Public links to file attachments on deleted posts no longer work. +- Error message is now shown in IE11 when uploading more than 5 files or a file over 50 MB. + + +### Compatibility +Changes from v2.0 to v2.1: + +**Android** +Mattermost Android Application is for use with Mattermost platform v2.1 and higher. + +#### Known Issues + +- File name tooltip stays open after clicking to download. +- Unable to paste images into the text box on Firefox, Safari, and IE11. +- Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh. +- First load of an empty channel does not display the introduction message. +- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks. +- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator. +- Hashtags containing a dash incorrectly highlight in the search results. +- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected. +- Incorrect formatting when a new line is added directly after a list. +- Timestamps are displayed in 12-hour format when set to 24-hour format. +- Syntax highlighting code block is missing the label for Latex documents. +- Posts from webhooks do not fire notifications to the user who created the webhook. +- Theme color vector is not updated after making custom changes to a default theme. +- Search term highlighting doesn't update on IE11 when search terms change but return the same posts. +- Team creation via SSO fails when email domain is restricted. + +#### Contributors + +Many thanks to all our external contributors. In no particular order: + +- [rodrigocorsi2](https://github.com/rodrigocorsi2) +- [enahum](https://github.com/enahum) +- [khoa-le](https://github.com/khoa-le) +- [alanmoo](https://github.com/alanmoo) +- [daizenberg](https://github.com/daizenberg) +- [GuillaumeAmat](https://github.com/GuillaumeAmat) +- [kernicPanel](https://github.com/kernicPanel) +- [timlyo](https://github.com/timlyo) +- [ttyniwa](https://github.com/ttyniwa) + ## Release v2.0.0 Expected Release date: 2016-02-16 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/file.go b/api/file.go index 0011afd5b8..9150e4bfed 100644 --- a/api/file.go +++ b/api/file.go @@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError { return nil } +func moveFile(oldPath, newPath string) *model.AppError { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + fileData := make(chan []byte) + getFileAndForget(oldPath, fileData) + fileBytes := <-fileData + + if fileBytes == nil { + return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "") + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey + + s := s3.New(auth, awsRegion()) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) + + if err := bucket.Del(oldPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) + } + + if err := writeFile(fileBytes, newPath); err != nil { + return err + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) + } + } else { + return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "") + } + + return nil +} + func writeFileLocally(f []byte, path string) *model.AppError { if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil { return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error()) 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 e6560a8e88..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 @@ -1094,6 +1095,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { message.Add("post", post.ToJson()) PublishAndForget(message) + DeletePostFilesAndForget(c.Session.TeamId, post) result := make(map[string]string) result["id"] = postId @@ -1101,6 +1103,23 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func DeletePostFilesAndForget(teamId string, post *model.Post) { + go func() { + if len(post.Filenames) == 0 { + return + } + + prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/" + for _, filename := range post.Filenames { + splitUrl := strings.Split(filename, "/") + oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1] + newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1] + moveFile(oldPath, newPath) + } + + }() +} + func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) { getPostsBeforeOrAfter(c, w, r, true) } 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 @@ - -
- -
-
|
-
+
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 = (
+
+
+
+ + {this.state.serverError} +
+
-
-
-
A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.
' - }, - 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: ( +
+
+
+
+
+
+
This compiled release of Mattermost platform is provided under a commercial license from Mattermost, Inc. based on your subscription level and is subject to the Terms of Service.
Your subscription details are as follows:
Name: {name}This compiled release of Mattermost platform is offered under an MIT license.
See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.
", @@ -658,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": "Your email has been verified! Click here to log in.
", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -755,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", @@ -819,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click here 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": "A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.
", + "password_send.link": "A password reset link has been sent to {email}
", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "▲ collapse text", @@ -1300,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 b144708722..f42dc879a6 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -1,11 +1,12 @@ { "about.close": "Cerrar", "about.date": "Fecha de compilación:", - "about.enterpriseEdition": "Edición Enterprise", + "about.enterpriseEditione1": "Edición Enterprise E1", "about.hash": "Hash de compilación:", "about.licensed": "Licenciado por:", "about.number": "Número de compilación:", - "about.teamEdtion": "Edición Team", + "about.teamEditiont0": "Edición Team T0", + "about.teamEditiont1": "Edición Team T1", "about.title": "Acerca de Mattermost", "about.version": "Versión:", "access_history.title": "Historial de Acceso", @@ -152,14 +153,14 @@ "admin.ldap.bannerDesc": "Si el atributo de un usuario cambia en el servidor LDAP será actualizado la próxima vez que el usuario ingrese sus credenciales para iniciar sesión en Mattermost. Esto incluye si un usuario se inactiva o se remueve en el servidor LDAP. Sincronización con servidores LDAP está planificado para futuras versiones.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "El DN Base es el Nombre Distinguido de la ubicación donde Mattermost debe comenzar a buscar a los usuarios en el árbol del LDAP.", - "admin.ldap.baseEx": "Ex \"dc=midominio,dc=com\"", + "admin.ldap.baseEx": "Ej \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "DN Base:", "admin.ldap.bindPwdDesc": "Contraseña del usuario asignado en \"Usuario de Enlace\".", "admin.ldap.bindPwdTitle": "Contraseña de Enlace:", "admin.ldap.bindUserDesc": "El usuario que realizará las busquedas LDAP. Normalmente este debería ser una cuenta específicamente creada para ser utilizada por Mattermost. Debería contat con acceso limitado para leer la porción del árbol LDAP especificada en el campo DN Base.", "admin.ldap.bindUserTitle": "Usuario de Enlace:", "admin.ldap.emailAttrDesc": "El atributo en el servidor LDAP que será utilizado para poblar la dirección de correo electrónico de los usuarios en Mattermost.", - "admin.ldap.emailAttrEx": "Ej \"mail\"", + "admin.ldap.emailAttrEx": "Ej \"mail\" o \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de Correo Electrónico:", "admin.ldap.enableDesc": "Cuando es verdadero, Mattermost permite realizar inicio de sesión utilizando LDAP", "admin.ldap.enableTitle": "Habilitar inicio de sesión con LDAP:", @@ -191,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ej \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo Usuario:", "admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, deshabilita todas las características de la Edición Enterprise de este servidor. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.", + "admin.license.chooseFile": "Escoger Archivo", "admin.license.edition": "Edición: ", "admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.", "admin.license.enterpriseType": "Esta versión compilada de la plataforma de Mattermost es provista bajo una licencia comercial de Mattermost, Inc. en función en su nivel de subscripción y bajo los Términos del Servicio.
Los detalles de tu subscripción son los siguientes:
Nombre: {name}Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.
Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.
", @@ -819,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha aquí 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": "Se ha enviado un enlace para restablecer la contraseña a {email} para tu equipo {teamDisplayName} en {hostname}.
", + "password_send.link": "Se ha enviado un enlace para restablecer la contraseña a {email}
", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "▲ colapsar texto", @@ -1300,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 f4b998ded9..f6210a6ae4 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -1,11 +1,12 @@ { "about.close": "Fechar", "about.date": "Data De Criação:", - "about.enterpriseEdition": "Enterprise Edition", + "about.enterpriseEditione1": "Enterprise Edition E1", "about.hash": "Hash de Compilação:", "about.licensed": "Licenciado pela:", "about.number": "O Número De Compilação:", - "about.teamEdtion": "Team Edition", + "about.teamEditiont0": "Team Edition T0", + "about.teamEditiont1": "Team Edition T1", "about.title": "Sobre o Mattermost", "about.version": "Versão:", "access_history.title": "Histórico de Acesso", @@ -26,7 +27,7 @@ "admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.", "admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ", "admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.", - "admin.email.allowSignupTitle": "Permitir Login com E-Mail: ", + "admin.email.allowSignupTitle": "Permitir Inscrição com E-Mail: ", "admin.email.allowUsernameSignInDescription": "Quando verdadeiro, Mattermost permite os usuários fazer login usando seu nome de usuário e senha. Esta configuração é normalmente utilizado apenas quando a verificação de e-mail está desativada.", "admin.email.allowUsernameSignInTitle": "Permitir Login Com Usuário: ", "admin.email.connectionSecurityNone": "Nenhum", @@ -43,7 +44,7 @@ "admin.email.false": "falso", "admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.", "admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"", - "admin.email.inviteSaltTitle": "Convidar Salt:", + "admin.email.inviteSaltTitle": "Salt Convite:", "admin.email.notificationDisplayDescription": "Mostra o nome da conta de e-mail usada quando a notificação de e-mail é enviado do Mattermost.", "admin.email.notificationDisplayExample": "Ex: \"Mattermost Notificação\", \"Sistema\", \"Não-Responda\"", "admin.email.notificationDisplayTitle": "Notificação Nome de Exibição:", @@ -152,14 +153,14 @@ "admin.ldap.bannerDesc": "Se um atributo de usuário de mudar no servidor LDAP, ele será atualizado na próxima vez que o usuário inserir suas credenciais para iniciar sessão no Mattermost. Isso inclui se um usuário estiver inativo ou removido de um servidor LDAP. Sincronização com servidores LDAP está prevista para um lançamento futuro.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "Base DN é o nome distinto do local onde Mattermost deve começar sua busca para os usuários na árvore LDAP.", - "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"", + "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "BaseDN:", "admin.ldap.bindPwdDesc": "Senha do usuário fornecido em \"Bind Username\".", "admin.ldap.bindPwdTitle": "Vincular Senha:", "admin.ldap.bindUserDesc": "O nome de usuário usado para realizar a pesquisa LDAP. Isso deve ser tipicamente uma conta criada especificamente para uso do Mattermost. Deve ter acesso limitado a ler a parte da árvore LDAP especificado no campo BaseDN.", "admin.ldap.bindUserTitle": "Bind Username:", "admin.ldap.emailAttrDesc": "O atributo no servidor LDAP que será usado para preencher os endereços de e-mail de usuários no Mattermost.", - "admin.ldap.emailAttrEx": "Ex \"mail\"", + "admin.ldap.emailAttrEx": "Ex \"mail\" ou \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de E-mail:", "admin.ldap.enableDesc": "Quando verdadeiro, Mattermost permite login utilizando LDAP", "admin.ldap.enableTitle": "Ativar Login With LDAP:", @@ -191,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo do Usuário:", "admin.licence.keyMigration": "Se você estiver migrando seu servidor você pode precisar remover sua chave da licença deste servidor a pedido para instala-la em um novo servidor. Para iniciar, desativar todos os recursos Enterprise Edition deste servidor. Isto irá habilitar para remover a chave da licença e fazer downgrade deste servidor Enterprise Edition para Team Edition.", + "admin.license.chooseFile": "Escolha um Arquivo", "admin.license.edition": "Edição: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Desenvolvido para escala empresarial de comunicação.", "admin.license.enterpriseType": "Esta versão compilada da plataforma Mattermost é fornecida sob a licença comercial para Mattermost, Inc. com base em seu nível de subscrição e está sujeito a Termos de Serviço.
Os detalhes de sua assinatura, são como segue:
Nome: {name}Esta versão compilada da plataforma Mattermost é oferecido sob uma licença MIT.
Ver MIT-COMPILED-LICENSE.txt no raiz do diretório de instalação para obter detalhes. Ver NOTICES.txt para obter informações sobre o software open source usados neste sistema.
", @@ -205,7 +208,7 @@ "admin.license.uploadDesc": "Enviar uma chave da licença para Mattermost Enterprise Edition para fazer upgrade deste servidor. Visite-nos online para saber mais sobre os beneficios da Enterprise Edition ou para comprar uma chave.", "admin.license.uploading": "Enviando Licença...", "admin.log.consoleDescription": "Normalmente definido como falso em produção. Os desenvolvedores podem definir este campo como verdadeiro para mensagens de log de saída no console baseado na opção de nível de console. Se verdadeiro, o servidor escreve mensagens para o fluxo de saída padrão (stdout).", - "admin.log.consoleTitle": "Log Do Console: ", + "admin.log.consoleTitle": "Log Para o Console: ", "admin.log.false": "falso", "admin.log.fileDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, arquivos de log são gravados no arquivo de log especificado no campo de localização abaixo.", "admin.log.fileLevelDescription": "Esta configuração determina o nível de detalhe que são gravados no log de eventos no console. ERROR: Saídas somente mensagens de erro. INFO: Saídas de mensagens de erro e informações em torno de inicialização. DEBUG: Impressões de alto detalhe para desenvolvedores que trabalham na depuração de problemas.", @@ -277,6 +280,9 @@ "admin.service.attemptTitle": "Máxima Tentativas de Login:", "admin.service.cmdsDesc": "Quando verdadeiro, comandos slash criados por usuários serão permitidos.", "admin.service.cmdsTitle": "Ativar Comandos Slash: ", + "admin.service.corsDescription": "Ativar requisição de origem HTTP Cross dos domínios especificados (separados por espaço). Usar \"*\" se você quiser permitir CORS de qualquer domínio ou deixe em branco para desativar.", + "admin.service.corsEx": "http://example.com https://example.com", + "admin.service.corsTitle": "Permitir Requisição Cross-origin de:", "admin.service.developerDesc": "(Opção dos desenvolvedores) Quando verdadeira, a informação extra em torno dos erros será exibida na UI.", "admin.service.developerTitle": "Ativar o Modo Desenvolvedor: ", "admin.service.false": "falso", @@ -558,6 +564,7 @@ "channel_loader.wrote": " escreveu: ", "channel_members_modal.addNew": " Adicionar Novos Membros", "channel_members_modal.close": "Fechar", + "channel_members_modal.remove": "Remover", "channel_memebers_modal.members": " Membros", "channel_modal.cancel": "Cancelar", "channel_modal.channel": "Canal", @@ -589,6 +596,7 @@ "choose_auth_page.find": "Encontrar minhas equipes", "choose_auth_page.gitlabCreate": "Criar uma equipe com uma conta GitLab", "choose_auth_page.googleCreate": "Criar nova equipe com a Conta do Google Apps", + "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP", "choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email específicado", "claim.email_to_sso.enterPwd": "Entre a senha para o sua conta {team} {site}", @@ -665,8 +673,8 @@ "file_upload.filesAbove": "Arquivos acima {max}MB não podem ser enviados: {filenames}", "file_upload.limited": "Limite máximo de uploads de {count} arquivos. Por favor use um post adicional para mais arquivos.", "file_upload.pasted": "Imagem Colada em ", - "filtered_user_list.count": "{count, number} {count, plural, one {Membro} other {Membros}}", - "filtered_user_list.countTotal": "{count, number} {count, plural, one {Membro} other {Membros}} de {total} Total", + "filtered_user_list.count": "{count, number} {count, plural, one {membro} other {membros}}", + "filtered_user_list.countTotal": "{count, number} {count, plural, one {membro} other {membros}} de {total} Total", "filtered_user_list.search": "Procurar membros", "find_team.email": "E-mail", "find_team.findDescription": "Foi enviado um e-mail com links para todas as equipes do qual você é membro.", @@ -690,7 +698,7 @@ "general_tab.regenerate": "Re-Gerar", "general_tab.required": "Este campo é obrigatório", "general_tab.teamName": "Nome da Equipe", - "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela inicial e no topo na lateral esquerda.", + "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela de login e no topo na lateral esquerda.", "general_tab.title": "Definições Gerais", "general_tab.yes": "Sim", "get_link.clipboard": " Link copiado para a área de transferência.", @@ -698,6 +706,7 @@ "get_link.copy": "Copiar Link", "get_post_link_modal.help": "O link abaixo permite que usuários autorizados possam ver seus posts.", "get_post_link_modal.title": "Copiar Permalink", + "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele não muda pode ser compartilhado com várias pessoas ao menos que seja re-gerado em Configurações de Equipe pelo Administrador de Equipe.", "get_team_invite_link_modal.helpDisabled": "Criação de usuários está desabilitada para sua equipe. Por favor peça ao administrador de equipe por detalhes.", "get_team_invite_link_modal.title": "Link para Convite de Equipe", "intro_messages.DM": "Este é o início do seu histórico de mensagens diretas com {teammate}.Um link para resetar a sua senha na equipe {teamDisplayName} em {hostname} foi enviado para {email}.
", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "▲ recolher texto", @@ -936,7 +950,7 @@ "signup_user_completed.chooseUser": "Escolha o seu nome de usuário", "signup_user_completed.create": "Criar Conta", "signup_user_completed.emailHelp": "Email valido necessário para inscrição", - "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para entrar em {siteName}.", + "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para logar no {siteName}.", "signup_user_completed.expired": "Você já concluiu o processo de inscrição para este convite ou este convite expirou.", "signup_user_completed.gitlab": "com GitLab", "signup_user_completed.google": "com Google", @@ -960,6 +974,7 @@ "sso_signup.team_error": "Por favor entre o nome da equipe", "suggestion.mention.all": "Notificar todo mundo na equipe", "suggestion.mention.channel": "Notifica todos no canal", + "suggestion.search.private": "Grupos Privados", "suggestion.search.public": "Canais Públicos", "team_export_tab.download": "download", "team_export_tab.export": "Exportar", @@ -999,7 +1014,7 @@ "team_signup_display_name.required": "Este campo é obrigatório", "team_signup_display_name.teamName": "Nome Da Equipe", "team_signup_email.address": "Endereço de E-mail", - "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado no login", + "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado na inscrição", "team_signup_email.validEmail": "Por favor entre um endereço de e-mail válido", "team_signup_password.agreement": "Ao prosseguir para criar sua conta e usar {siteName}, você concorda com nosso Termo de Serviço e Politica de Privacidade. Se você não concorda, você não pode usar {siteName}.", "team_signup_password.back": "Voltar para o passo anterior", @@ -1052,9 +1067,9 @@ "textbox.help": "Ajuda", "textbox.inlinecode": "`código`", "textbox.italic": "_itálico_", - "textbox.preformatted": "```pre-formatado```", + "textbox.preformatted": "```pré-formatado```", "textbox.preview": "Pré-visualização", - "textbox.quote": ">citado", + "textbox.quote": ">citar", "textbox.strike": "tachado", "tutorial_intro.allSet": "Está tudo pronto", "tutorial_intro.end": "Clique em “Próximo” para entrar Town Square. Este é o primeiro canal que sua equipe de trabalho vê quando eles se inscrevem. Use para postar atualizações que todos precisam saber.", @@ -1272,7 +1287,7 @@ "user.settings.security.logoutActiveSessions": "Ver e fazer Logout das Sessões Ativas", "user.settings.security.method": "Método de Login", "user.settings.security.newPassword": "Nova Senha", - "user.settings.security.oneSignin": "Você pode ter somente um método de inscrição por vez. Trocando o método de inscrição será enviado um email de notificação se você alterar com sucesso.", + "user.settings.security.oneSignin": "Você pode ter somente um método de login por vez. Trocando o método de login será enviado um email de notificação se você alterar com sucesso.", "user.settings.security.password": "Senha", "user.settings.security.passwordLengthError": "Novas senhas precisam ter pelo menos {chars} characters", "user.settings.security.passwordMatchError": "As novas senhas que você inseriu não correspondem", diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized-dark.png similarity index 100% rename from web/static/images/themes/code_themes/solarized_dark.png rename to web/static/images/themes/code_themes/solarized-dark.png diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized-light.png similarity index 100% rename from web/static/images/themes/code_themes/solarized_light.png rename to web/static/images/themes/code_themes/solarized-light.png diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493ee..0000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} - - -{{template "head" . }} - - - - - - - - -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a0227..0000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} - - -{{template "head" . }} - - - - - -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1be..0000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} - - -{{template "head" . }} - -
-
-
-
-
-
-
-
-
-