Merge branch 'master' into PLT-2115
Этот коммит содержится в:
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,5 +1,130 @@
|
|||||||
# Mattermost Changelog
|
# 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
|
## Release v2.0.0
|
||||||
|
|
||||||
Expected Release date: 2016-02-16
|
Expected Release date: 2016-02-16
|
||||||
|
|||||||
25
Makefile
25
Makefile
@@ -127,10 +127,9 @@ package:
|
|||||||
cp -RL web/static/help $(DIST_PATH)/web/static
|
cp -RL web/static/help $(DIST_PATH)/web/static
|
||||||
cp -RL web/static/images $(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/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
|
mkdir -p $(DIST_PATH)/api
|
||||||
cp -RL api/templates $(DIST_PATH)/api
|
|
||||||
cp -RL i18n $(DIST_PATH)
|
cp -RL i18n $(DIST_PATH)
|
||||||
|
|
||||||
cp build/MIT-COMPILED-LICENSE.md $(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/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
|
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-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)/web/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)/web/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)/web/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)/web/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)/web/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)/web/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)/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)/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|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)/web/templates/head.html
|
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
|
||||||
rm $(DIST_PATH)/web/templates/*.bak
|
rm $(DIST_PATH)/templates/*.bak
|
||||||
|
|
||||||
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
|
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
|
||||||
|
|
||||||
|
|||||||
43
api/api.go
43
api/api.go
@@ -4,47 +4,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"net/http"
|
||||||
l4g "github.com/alecthomas/log4go"
|
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
_ "github.com/cloudfoundry/jibber_jabber"
|
_ "github.com/cloudfoundry/jibber_jabber"
|
||||||
_ "github.com/nicksnyder/go-i18n/i18n"
|
_ "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() {
|
func InitApi() {
|
||||||
r := Srv.Router.PathPrefix("/api/v1").Subrouter()
|
r := Srv.Router.PathPrefix("/api/v1").Subrouter()
|
||||||
InitUser(r)
|
InitUser(r)
|
||||||
@@ -60,12 +28,7 @@ func InitApi() {
|
|||||||
InitPreference(r)
|
InitPreference(r)
|
||||||
InitLicense(r)
|
InitLicense(r)
|
||||||
|
|
||||||
templatesDir := utils.FindDir("api/templates")
|
utils.InitHTML()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool {
|
func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
|||||||
137
api/context.go
137
api/context.go
@@ -5,11 +5,9 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
l4g "github.com/alecthomas/log4go"
|
||||||
@@ -31,33 +29,16 @@ var allowedMethods []string = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Session model.Session
|
Session model.Session
|
||||||
RequestId string
|
RequestId string
|
||||||
IpAddress string
|
IpAddress string
|
||||||
Path string
|
Path string
|
||||||
Err *model.AppError
|
Err *model.AppError
|
||||||
teamURLValid bool
|
teamURLValid bool
|
||||||
teamURL string
|
teamURL string
|
||||||
siteURL string
|
siteURL string
|
||||||
SessionTokenIndex int64
|
T goi18n.TranslateFunc
|
||||||
T goi18n.TranslateFunc
|
Locale string
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
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
|
// Attempt to parse the token from the cookie
|
||||||
if len(token) == 0 {
|
if len(token) == 0 {
|
||||||
tokens := GetMultiSessionCookieTokens(r)
|
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||||
if len(tokens) > 0 {
|
token = cookie.Value
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if session == nil || session.IsExpired() {
|
if session == nil || session.IsExpired() {
|
||||||
c.RemoveSessionCookie(w, r)
|
c.RemoveSessionCookie(w, r)
|
||||||
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
|
if h.requireUser || h.requireSystemAdmin {
|
||||||
c.Err.StatusCode = http.StatusUnauthorized
|
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
|
||||||
|
c.Err.StatusCode = http.StatusUnauthorized
|
||||||
|
}
|
||||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||||
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
|
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
|
||||||
c.Err.StatusCode = http.StatusUnauthorized
|
c.Err.StatusCode = http.StatusUnauthorized
|
||||||
@@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
|
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{
|
cookie := &http.Cookie{
|
||||||
Name: model.SESSION_COOKIE_TOKEN,
|
Name: model.SESSION_COOKIE_TOKEN,
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
|
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
|
||||||
props := make(map[string]string)
|
T, locale := utils.GetTranslationsAndLocale(w, r)
|
||||||
props["Message"] = err.Message
|
page := utils.NewHTMLTemplate("error", locale)
|
||||||
props["Details"] = err.DetailedError
|
page.Props["Message"] = err.Message
|
||||||
|
page.Props["Details"] = err.DetailedError
|
||||||
|
|
||||||
pathParts := strings.Split(r.URL.Path, "/")
|
pathParts := strings.Split(r.URL.Path, "/")
|
||||||
if len(pathParts) > 1 {
|
if len(pathParts) > 1 {
|
||||||
props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
|
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
|
||||||
} else {
|
} else {
|
||||||
props["SiteURL"] = GetProtocol(r) + "://" + r.Host
|
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
T, _ := utils.GetTranslationsAndLocale(w, r)
|
page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
||||||
props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
page.Props["Link"] = T("api.templates.error.link")
|
||||||
props["Link"] = T("api.templates.error.link")
|
|
||||||
|
|
||||||
w.WriteHeader(err.StatusCode)
|
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) {
|
func Handle404(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -588,29 +528,6 @@ func GetSession(token string) *model.Session {
|
|||||||
return 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) {
|
func AddSessionToCache(session *model.Session) {
|
||||||
sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
|
sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
|
||||||
}
|
}
|
||||||
|
|||||||
35
api/file.go
35
api/file.go
@@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError {
|
|||||||
return nil
|
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 {
|
func writeFileLocally(f []byte, path string) *model.AppError {
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
|
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())
|
return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error())
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) {
|
|||||||
sr := r.PathPrefix("/license").Subrouter()
|
sr := r.PathPrefix("/license").Subrouter()
|
||||||
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
|
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
|
||||||
sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).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) {
|
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"
|
rdata["status"] = "ok"
|
||||||
w.Write([]byte(model.MapToJson(rdata)))
|
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)))
|
||||||
|
}
|
||||||
|
|||||||
22
api/license_test.go
Обычный файл
22
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
343
api/oauth.go
343
api/oauth.go
@@ -5,12 +5,15 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
l4g "github.com/alecthomas/log4go"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitOAuth(r *mux.Router) {
|
func InitOAuth(r *mux.Router) {
|
||||||
@@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) {
|
|||||||
|
|
||||||
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
|
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
|
||||||
sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
|
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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
25
api/post.go
25
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
|
// copy the context and create a mock session for posting the message
|
||||||
mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
|
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 text, ok := respProps["text"]; ok {
|
||||||
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
|
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())
|
year := fmt.Sprintf("%d", tm.Year())
|
||||||
zone, _ := tm.Zone()
|
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",
|
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
|
||||||
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||||
"Month": month[:3], "Day": day, "Year": year})
|
"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["SiteURL"] = c.GetSiteURL()
|
||||||
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
|
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
|
||||||
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
|
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())
|
message.Add("post", post.ToJson())
|
||||||
|
|
||||||
PublishAndForget(message)
|
PublishAndForget(message)
|
||||||
|
DeletePostFilesAndForget(c.Session.TeamId, post)
|
||||||
|
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
result["id"] = postId
|
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) {
|
func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
getPostsBeforeOrAfter(c, w, r, true)
|
getPostsBeforeOrAfter(c, w, r, true)
|
||||||
}
|
}
|
||||||
|
|||||||
114
api/team.go
114
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_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST")
|
||||||
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
|
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
|
||||||
sr.Handle("/signup", ApiAppHandler(signupTeam)).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_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("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
|
||||||
sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
|
sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
|
||||||
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
|
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
|
// These should be moved to the global admain console
|
||||||
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
|
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
|
||||||
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
|
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
|
||||||
@@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject",
|
||||||
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
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["SiteURL"] = c.GetSiteURL()
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
|
||||||
bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button")
|
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 {
|
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", " *")
|
w.Header().Set("Access-Control-Allow-Origin", " *")
|
||||||
@@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)))
|
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) {
|
func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
if !c.HasSystemAdminPermissions("getLogs") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
|
if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
|
||||||
c.Err = result.Err
|
c.Err = result.Err
|
||||||
return
|
return
|
||||||
@@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
m := make(map[string]*model.Team)
|
m := make(map[string]*model.Team)
|
||||||
for _, v := range teams {
|
for _, v := range teams {
|
||||||
m[v.Id] = v
|
m[v.Id] = v
|
||||||
|
if !c.IsSystemAdmin() {
|
||||||
|
m[v.Id].SanitizeForNotLoggedIn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write([]byte(model.TeamMapToJson(m)))
|
w.Write([]byte(model.TeamMapToJson(m)))
|
||||||
@@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
|
|||||||
return false
|
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) {
|
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
invites := model.InvitesFromJson(r.Body)
|
invites := model.InvitesFromJson(r.Body)
|
||||||
if len(invites.Invites) == 0 {
|
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")
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.invite_subject",
|
||||||
map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
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["SiteURL"] = c.GetSiteURL()
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
|
||||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info",
|
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)))
|
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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
119
api/team_test.go
119
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) {
|
func TestGetAllTeams(t *testing.T) {
|
||||||
Setup()
|
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)
|
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 := &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")
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
if _, err := Client.GetAllTeams(); err == nil {
|
enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing
|
||||||
t.Fatal("you shouldn't have permissions")
|
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{}
|
c := &Context{}
|
||||||
@@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) {
|
|||||||
if teams[team.Id].Name != team.Name {
|
if teams[team.Id].Name != team.Name {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
if teams[team.Id].Email != team.Email {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) {
|
|||||||
Client.ClearOAuthToken()
|
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) {
|
func TestInviteMembers(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
||||||
<title><span class='fa fa-chevron-left'></span>Back - Error</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<script src="/static/js/react-with-addons-0.13.3.min.js"></script>
|
|
||||||
<script src="/static/js/jquery-1.11.1.min.js"></script>
|
|
||||||
<script src="/static/js/bootstrap-3.3.5.min.js"></script>
|
|
||||||
<script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
|
|
||||||
|
|
||||||
<link id="favicon" rel="icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
|
|
||||||
<link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
|
|
||||||
<link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
|
|
||||||
<link rel="stylesheet" href="/static/css/styles.css">
|
|
||||||
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body class="white error">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="error__container">
|
|
||||||
<div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
|
|
||||||
<h2>{{.Props.Title}}</h2>
|
|
||||||
<p>{{ .Props.Message }}</p>
|
|
||||||
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
var details = "{{ .Details }}";
|
|
||||||
if (details.length > 0) {
|
|
||||||
console.log("error details: " + details);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{{define "find_teams_body"}}
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
|
||||||
<tr>
|
|
||||||
<td style="border: 1px solid #ddd;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
|
||||||
<img src="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
|
|
||||||
<tr>
|
|
||||||
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
|
|
||||||
<h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
|
|
||||||
<p>{{ if .Extra }}
|
|
||||||
{{.Props.Found}}<br>
|
|
||||||
{{range $index, $element := .Extra}}
|
|
||||||
<a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
|
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
|
||||||
{{.Props.NotFound}}
|
|
||||||
{{ end }}
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
{{template "email_info" . }}
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
{{template "email_footer" . }}
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
|
|
||||||
146
api/user.go
146
api/user.go
@@ -53,10 +53,13 @@ func InitUser(r *mux.Router) {
|
|||||||
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
|
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
|
||||||
sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
|
sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
|
||||||
sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).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("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
||||||
|
|
||||||
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
|
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("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
|
||||||
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
|
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
|
||||||
sr.Handle("/profiles/{id:[A-Za-z0-9]+}", 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) {
|
func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
|
||||||
go func() {
|
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})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
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")
|
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
|
bodyPage.Props["TeamURL"] = teamURL
|
||||||
|
|
||||||
if !verified {
|
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
|
bodyPage.Props["VerifyUrl"] = link
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) {
|
|||||||
func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
|
func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
|
||||||
go func() {
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.verify_subject",
|
||||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
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")
|
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)
|
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)
|
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
|
||||||
|
sessionCookie := &http.Cookie{
|
||||||
multiSessionCookie := &http.Cookie{
|
|
||||||
Name: model.SESSION_COOKIE_TOKEN,
|
Name: model.SESSION_COOKIE_TOKEN,
|
||||||
Value: multiToken,
|
Value: session.Token,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
Expires: expiresAt,
|
Expires: expiresAt,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, multiSessionCookie)
|
http.SetCookie(w, sessionCookie)
|
||||||
|
|
||||||
c.Session = *session
|
c.Session = *session
|
||||||
c.LogAuditWithUserId(user.Id, "success")
|
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) {
|
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
id := params["id"]
|
id := params["id"]
|
||||||
@@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
data := model.MapToJson(newProps)
|
data := model.MapToJson(newProps)
|
||||||
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt))
|
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")
|
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["SiteURL"] = c.GetSiteURL()
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
|
||||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
|
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) {
|
func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
|
||||||
go func() {
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject",
|
||||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
|
||||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
|
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) {
|
func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
|
||||||
go func() {
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject",
|
||||||
map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
|
||||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info",
|
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)
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject",
|
||||||
map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
|
bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
|
||||||
bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
|
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
|
props["team"] = teamName
|
||||||
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
|
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)
|
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) {
|
func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
|
||||||
go func() {
|
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",
|
subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject",
|
||||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
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["SiteURL"] = siteURL
|
||||||
bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
|
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",
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) {
|
|||||||
t.Fatal("should have failed - wrong user")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Двоичные данные
docker/1.4/Dockerrun.aws.zip
Двоичные данные
docker/1.4/Dockerrun.aws.zip
Двоичный файл не отображается.
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
|
|||||||
WORKDIR /mattermost
|
WORKDIR /mattermost
|
||||||
|
|
||||||
# Copy over files
|
# Copy over files
|
||||||
ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz /
|
ADD https://github.com/mattermost/platform/releases/download/v2.0.0/mattermost.tar.gz /
|
||||||
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
|
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
|
||||||
ADD config_docker.json /
|
ADD config_docker.json /
|
||||||
ADD docker-entry.sh /
|
ADD docker-entry.sh /
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ VOLUME /var/lib/mysql
|
|||||||
WORKDIR /mattermost
|
WORKDIR /mattermost
|
||||||
|
|
||||||
# Copy over files
|
# Copy over files
|
||||||
ADD https://github.com/mattermost/platform/releases/download/v1.4.0/mattermost.tar.gz /
|
ADD https://releases.mattermost.com/2.1.0-rc1/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz /
|
||||||
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
|
RUN tar -zxvf /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz --strip-components=1 && rm /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz
|
||||||
ADD config_docker.json /
|
ADD config_docker.json /
|
||||||
ADD docker-entry.sh /
|
ADD docker-entry.sh /
|
||||||
|
|
||||||
Двоичные данные
docker/2.1/Dockerrun.aws.zip
Обычный файл
Двоичные данные
docker/2.1/Dockerrun.aws.zip
Обычный файл
Двоичный файл не отображается.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"AWSEBDockerrunVersion": "1",
|
"AWSEBDockerrunVersion": "1",
|
||||||
"Image": {
|
"Image": {
|
||||||
"Name": "mattermost/platform:1.4",
|
"Name": "mattermost/platform:2.1",
|
||||||
"Update": "true"
|
"Update": "true"
|
||||||
},
|
},
|
||||||
"Ports": [
|
"Ports": [
|
||||||
@@ -7,11 +7,14 @@
|
|||||||
"EnableOAuthServiceProvider": false,
|
"EnableOAuthServiceProvider": false,
|
||||||
"EnableIncomingWebhooks": false,
|
"EnableIncomingWebhooks": false,
|
||||||
"EnableOutgoingWebhooks": false,
|
"EnableOutgoingWebhooks": false,
|
||||||
|
"EnableCommands": false,
|
||||||
|
"EnableOnlyAdminIntegrations": true,
|
||||||
"EnablePostUsernameOverride": false,
|
"EnablePostUsernameOverride": false,
|
||||||
"EnablePostIconOverride": false,
|
"EnablePostIconOverride": false,
|
||||||
"EnableTesting": false,
|
"EnableTesting": false,
|
||||||
"EnableDeveloper": false,
|
"EnableDeveloper": false,
|
||||||
"EnableSecurityFixAlert": true,
|
"EnableSecurityFixAlert": true,
|
||||||
|
"EnableInsecureOutgoingConnections": false,
|
||||||
"SessionLengthWebInDays" : 30,
|
"SessionLengthWebInDays" : 30,
|
||||||
"SessionLengthMobileInDays" : 30,
|
"SessionLengthMobileInDays" : 30,
|
||||||
"SessionLengthSSOInDays" : 30,
|
"SessionLengthSSOInDays" : 30,
|
||||||
@@ -66,6 +69,8 @@
|
|||||||
},
|
},
|
||||||
"EmailSettings": {
|
"EmailSettings": {
|
||||||
"EnableSignUpWithEmail": true,
|
"EnableSignUpWithEmail": true,
|
||||||
|
"EnableSignInWithEmail": true,
|
||||||
|
"EnableSignInWithUsername": false,
|
||||||
"SendEmailNotifications": false,
|
"SendEmailNotifications": false,
|
||||||
"RequireEmailVerification": false,
|
"RequireEmailVerification": false,
|
||||||
"FeedbackName": "",
|
"FeedbackName": "",
|
||||||
48
i18n/en.json
48
i18n/en.json
@@ -456,8 +456,8 @@
|
|||||||
"translation": "S3 is not supported for local storage export."
|
"translation": "S3 is not supported for local storage export."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.export.write_file.app_error",
|
"id": "api.file.file_upload.exceeds",
|
||||||
"translation": "Unable to write to export file"
|
"translation": "File exceeds max image size."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.file_upload.exceeds",
|
"id": "api.file.file_upload.exceeds",
|
||||||
@@ -507,6 +507,22 @@
|
|||||||
"id": "api.file.init.debug",
|
"id": "api.file.init.debug",
|
||||||
"translation": "Initializing file api routes"
|
"translation": "Initializing file api routes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.configured.app_error",
|
||||||
|
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.delete_from_s3.app_error",
|
||||||
|
"translation": "Unable to delete file from S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.get_from_s3.app_error",
|
||||||
|
"translation": "Unable to get file from S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.rename.app_error",
|
||||||
|
"translation": "Unable to move file locally."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.open_file_write_stream.configured.app_error",
|
"id": "api.file.open_file_write_stream.configured.app_error",
|
||||||
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
||||||
@@ -1607,6 +1623,10 @@
|
|||||||
"id": "api.user.upload_profile_user.upload_profile.app_error",
|
"id": "api.user.upload_profile_user.upload_profile.app_error",
|
||||||
"translation": "Couldn't upload profile image"
|
"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",
|
"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"
|
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
|
||||||
@@ -3404,22 +3424,6 @@
|
|||||||
"id": "web.find_team.title",
|
"id": "web.find_team.title",
|
||||||
"translation": "Find Team"
|
"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",
|
"id": "web.get_access_token.bad_client_id.app_error",
|
||||||
"translation": "invalid_request: Bad client_id"
|
"translation": "invalid_request: Bad client_id"
|
||||||
@@ -3552,10 +3556,6 @@
|
|||||||
"id": "web.root.home_title",
|
"id": "web.root.home_title",
|
||||||
"translation": "Home"
|
"translation": "Home"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "web.root.singup_info",
|
|
||||||
"translation": "All team communication in one place, searchable and accessible anywhere"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "web.root.singup_title",
|
"id": "web.root.singup_title",
|
||||||
"translation": "Signup"
|
"translation": "Signup"
|
||||||
@@ -3611,5 +3611,9 @@
|
|||||||
{
|
{
|
||||||
"id": "web.watcher_fail.error",
|
"id": "web.watcher_fail.error",
|
||||||
"translation": "Failed to add directory to watcher %v"
|
"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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
40
i18n/es.json
40
i18n/es.json
@@ -456,8 +456,8 @@
|
|||||||
"translation": "S3 no está soportado para exportar al almacenamiento local."
|
"translation": "S3 no está soportado para exportar al almacenamiento local."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.export.write_file.app_error",
|
"id": "api.file.file_upload.exceeds",
|
||||||
"translation": "No se puede escribir al archivo a exportar"
|
"translation": "El archivo excede el tamaño máximo para una imagen."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.file_upload.exceeds",
|
"id": "api.file.file_upload.exceeds",
|
||||||
@@ -507,6 +507,22 @@
|
|||||||
"id": "api.file.init.debug",
|
"id": "api.file.init.debug",
|
||||||
"translation": "Inicializando rutas del API para los archivos"
|
"translation": "Inicializando rutas del API para los archivos"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.configured.app_error",
|
||||||
|
"translation": "No ha sido configurado apropiadamente el almacenamiento. Por favor configuralo para utilizar ya sea S3 o almacenamiento local."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.delete_from_s3.app_error",
|
||||||
|
"translation": "No se pudo eliminar el archivo del S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.get_from_s3.app_error",
|
||||||
|
"translation": "No se pudo obtener el archivo desde el S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.rename.app_error",
|
||||||
|
"translation": "No se pudo mover el archivo localmente."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.open_file_write_stream.configured.app_error",
|
"id": "api.file.open_file_write_stream.configured.app_error",
|
||||||
"translation": "El almacenamiento de archivos no ha sido configurado apropiadamente. Por favor configuralo ya sea para S3 o para almacenamiento en el servidor local."
|
"translation": "El almacenamiento de archivos no ha sido configurado apropiadamente. Por favor configuralo ya sea para S3 o para almacenamiento en el servidor local."
|
||||||
@@ -3383,22 +3399,6 @@
|
|||||||
"id": "web.find_team.title",
|
"id": "web.find_team.title",
|
||||||
"translation": "Encontrar Equipo"
|
"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",
|
"id": "web.get_access_token.bad_client_id.app_error",
|
||||||
"translation": "invalid_request: client_id malo"
|
"translation": "invalid_request: client_id malo"
|
||||||
@@ -3531,10 +3531,6 @@
|
|||||||
"id": "web.root.home_title",
|
"id": "web.root.home_title",
|
||||||
"translation": "Inicio"
|
"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",
|
"id": "web.root.singup_title",
|
||||||
"translation": "Registrar"
|
"translation": "Registrar"
|
||||||
|
|||||||
64
i18n/pt.json
64
i18n/pt.json
@@ -456,8 +456,8 @@
|
|||||||
"translation": "S3 não é suportado para o armazenamento local de exportação."
|
"translation": "S3 não é suportado para o armazenamento local de exportação."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.export.write_file.app_error",
|
"id": "api.file.file_upload.exceeds",
|
||||||
"translation": "Não é possível gravar para exportar arquivo"
|
"translation": "Arquivo excede o máximo de tamanho de imagem."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.file_upload.exceeds",
|
"id": "api.file.file_upload.exceeds",
|
||||||
@@ -507,6 +507,22 @@
|
|||||||
"id": "api.file.init.debug",
|
"id": "api.file.init.debug",
|
||||||
"translation": "Inicializando file api routes"
|
"translation": "Inicializando file api routes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.configured.app_error",
|
||||||
|
"translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.delete_from_s3.app_error",
|
||||||
|
"translation": "Não é possível deletar o arquivo a partir do S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.get_from_s3.app_error",
|
||||||
|
"translation": "Não é possível obter o arquivo a partir do S3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.move_file.rename.app_error",
|
||||||
|
"translation": "Não foi possível mover o arquivo localmente."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.file.open_file_write_stream.configured.app_error",
|
"id": "api.file.open_file_write_stream.configured.app_error",
|
||||||
"translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local."
|
"translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local."
|
||||||
@@ -1205,7 +1221,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.signin_change_email.body.title",
|
"id": "api.templates.signin_change_email.body.title",
|
||||||
"translation": "Você atualizou seu método de acesso"
|
"translation": "Você atualizou seu método de login"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.signup_team_body.button",
|
"id": "api.templates.signup_team_body.button",
|
||||||
@@ -1225,7 +1241,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.singin_change_email.body.info",
|
"id": "api.templates.singin_change_email.body.info",
|
||||||
"translation": "Você atualizou seu método de inscrição para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.<br>Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema."
|
"translation": "Você atualizou seu método de login para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.<br>Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.singin_change_email.subject",
|
"id": "api.templates.singin_change_email.subject",
|
||||||
@@ -1257,7 +1273,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.welcome_body.info2",
|
"id": "api.templates.welcome_body.info2",
|
||||||
"translation": "Você pode acessar sua nova equipe pelo endereço web:"
|
"translation": "Você pode fazer login sua nova equipe pelo endereço web:"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.welcome_body.info3",
|
"id": "api.templates.welcome_body.info3",
|
||||||
@@ -1333,7 +1349,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_oauth_user.already_used.app_error",
|
"id": "api.user.create_oauth_user.already_used.app_error",
|
||||||
"translation": "Está conta {{.Service}} já foi utilizada para logar na equipe {{.DisplayName}}"
|
"translation": "Está conta {{.Service}} já foi utilizada para se inscrever na equipe {{.DisplayName}}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_oauth_user.create.app_error",
|
"id": "api.user.create_oauth_user.create.app_error",
|
||||||
@@ -1357,7 +1373,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.accepted_domain.app_error",
|
"id": "api.user.create_user.accepted_domain.app_error",
|
||||||
"translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou entre com um email diferente."
|
"translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou se inscreve com um email diferente."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.joining.error",
|
"id": "api.user.create_user.joining.error",
|
||||||
@@ -1373,11 +1389,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.signup_link_expired.app_error",
|
"id": "api.user.create_user.signup_link_expired.app_error",
|
||||||
"translation": "O link de acesso expirou"
|
"translation": "O link de inscrição expirou"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.signup_link_invalid.app_error",
|
"id": "api.user.create_user.signup_link_invalid.app_error",
|
||||||
"translation": "O link de acesso não parece ser válido"
|
"translation": "O link de inscrição não parece ser válido"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.team_name.app_error",
|
"id": "api.user.create_user.team_name.app_error",
|
||||||
@@ -2407,6 +2423,10 @@
|
|||||||
"id": "store.sql.pinging.info",
|
"id": "store.sql.pinging.info",
|
||||||
"translation": "Pingando banco de dados sql %v"
|
"translation": "Pingando banco de dados sql %v"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql.read_replicas_not_licensed.critical",
|
||||||
|
"translation": "A funcionalidade de mais de uma replica está desabilitada pela licença atual. Entre em contato com o administrador do sistema sobre como atualizar sua licença de empresa."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql.remove_index.critical",
|
"id": "store.sql.remove_index.critical",
|
||||||
"translation": "Falha ao remover o índice %v"
|
"translation": "Falha ao remover o índice %v"
|
||||||
@@ -2647,6 +2667,10 @@
|
|||||||
"id": "store.sql_channel.update_member.app_error",
|
"id": "store.sql_channel.update_member.app_error",
|
||||||
"translation": "Encontramos um erro ao atualizar o membro do canal"
|
"translation": "Encontramos um erro ao atualizar o membro do canal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_command.analytics_command_count.app_error",
|
||||||
|
"translation": "Não foi possível contar os comandos"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_command.save.delete.app_error",
|
"id": "store.sql_command.save.delete.app_error",
|
||||||
"translation": "Não foi possível deletar o comando"
|
"translation": "Não foi possível deletar o comando"
|
||||||
@@ -2675,10 +2699,6 @@
|
|||||||
"id": "store.sql_command.save.update.app_error",
|
"id": "store.sql_command.save.update.app_error",
|
||||||
"translation": "Não foi possível atualizar o comando"
|
"translation": "Não foi possível atualizar o comando"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "store.sql_command.analytics_command_count.app_error",
|
|
||||||
"translation": "Não foi possível contar os comandos"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "store.sql_license.get.app_error",
|
"id": "store.sql_license.get.app_error",
|
||||||
"translation": "Encontramos um erro ao obter a licença"
|
"translation": "Encontramos um erro ao obter a licença"
|
||||||
@@ -2899,6 +2919,10 @@
|
|||||||
"id": "store.sql_preference.update.app_error",
|
"id": "store.sql_preference.update.app_error",
|
||||||
"translation": "Não foi possível atualizar a preferência"
|
"translation": "Não foi possível atualizar a preferência"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_session.analytics_session_count.app_error",
|
||||||
|
"translation": "Não foi possível contar a sessão"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_session.cleanup_expired_sessions.app_error",
|
"id": "store.sql_session.cleanup_expired_sessions.app_error",
|
||||||
"translation": "Encontramos um erro enquanto deletava a sessão expirada do usuário"
|
"translation": "Encontramos um erro enquanto deletava a sessão expirada do usuário"
|
||||||
@@ -2951,10 +2975,6 @@
|
|||||||
"id": "store.sql_session.update_roles.app_error",
|
"id": "store.sql_session.update_roles.app_error",
|
||||||
"translation": "Não foi possível atualizar as funções"
|
"translation": "Não foi possível atualizar as funções"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "store.sql_session.analytics_session_count.app_error",
|
|
||||||
"translation": "Não foi possível contar a sessão"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "store.sql_system.get.app_error",
|
"id": "store.sql_system.get.app_error",
|
||||||
"translation": "Encontramos um erro ao procurar as propriedades de sistema"
|
"translation": "Encontramos um erro ao procurar as propriedades de sistema"
|
||||||
@@ -2967,6 +2987,10 @@
|
|||||||
"id": "store.sql_system.update.app_error",
|
"id": "store.sql_system.update.app_error",
|
||||||
"translation": "Encontramos um erro ao atualizar as propriedades de sistema"
|
"translation": "Encontramos um erro ao atualizar as propriedades de sistema"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_team.analytics_team_count.app_error",
|
||||||
|
"translation": "Não foi possível contar as equipes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_team.get.find.app_error",
|
"id": "store.sql_team.get.find.app_error",
|
||||||
"translation": "Não foi possível encontrar a equipe existente"
|
"translation": "Não foi possível encontrar a equipe existente"
|
||||||
@@ -3035,10 +3059,6 @@
|
|||||||
"id": "store.sql_team.update_display_name.app_error",
|
"id": "store.sql_team.update_display_name.app_error",
|
||||||
"translation": "Não foi possível atualizar o nome da equipe"
|
"translation": "Não foi possível atualizar o nome da equipe"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "store.sql_team.analytics_team_count.app_error",
|
|
||||||
"translation": "Não foi possível contar as equipes"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.analytics_unique_user_count.app_error",
|
"id": "store.sql_user.analytics_unique_user_count.app_error",
|
||||||
"translation": "Não foi possível obter o número de usuários únicos"
|
"translation": "Não foi possível obter o número de usuários únicos"
|
||||||
@@ -3587,4 +3607,4 @@
|
|||||||
"id": "web.watcher_fail.error",
|
"id": "web.watcher_fail.error",
|
||||||
"translation": "Falha ao adicionar diretório observador %v"
|
"translation": "Falha ao adicionar diretório observador %v"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -16,19 +16,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HEADER_REQUEST_ID = "X-Request-ID"
|
HEADER_REQUEST_ID = "X-Request-ID"
|
||||||
HEADER_VERSION_ID = "X-Version-ID"
|
HEADER_VERSION_ID = "X-Version-ID"
|
||||||
HEADER_ETAG_SERVER = "ETag"
|
HEADER_ETAG_SERVER = "ETag"
|
||||||
HEADER_ETAG_CLIENT = "If-None-Match"
|
HEADER_ETAG_CLIENT = "If-None-Match"
|
||||||
HEADER_FORWARDED = "X-Forwarded-For"
|
HEADER_FORWARDED = "X-Forwarded-For"
|
||||||
HEADER_REAL_IP = "X-Real-IP"
|
HEADER_REAL_IP = "X-Real-IP"
|
||||||
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||||
HEADER_TOKEN = "token"
|
HEADER_TOKEN = "token"
|
||||||
HEADER_BEARER = "BEARER"
|
HEADER_BEARER = "BEARER"
|
||||||
HEADER_AUTH = "Authorization"
|
HEADER_AUTH = "Authorization"
|
||||||
HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex"
|
API_URL_SUFFIX = "/api/v1"
|
||||||
SESSION_TOKEN_INDEX = "session_token_index"
|
|
||||||
API_URL_SUFFIX = "/api/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Result struct {
|
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) {
|
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
|
||||||
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
|
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
|
||||||
return nil, err
|
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) {
|
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
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
@@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) {
|
|||||||
c.AuthToken = sessionToken
|
c.AuthToken = sessionToken
|
||||||
c.AuthType = HEADER_BEARER
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SESSION_COOKIE_TOKEN = "MMTOKEN"
|
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||||
SESSION_CACHE_SIZE = 10000
|
SESSION_CACHE_SIZE = 10000
|
||||||
SESSION_PROP_PLATFORM = "platform"
|
SESSION_PROP_PLATFORM = "platform"
|
||||||
SESSION_PROP_OS = "os"
|
SESSION_PROP_OS = "os"
|
||||||
|
|||||||
@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
|
|||||||
o.Email = ""
|
o.Email = ""
|
||||||
o.AllowedDomains = ""
|
o.AllowedDomains = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Team) SanitizeForNotLoggedIn() {
|
||||||
|
o.Email = ""
|
||||||
|
o.AllowedDomains = ""
|
||||||
|
o.CompanyName = ""
|
||||||
|
o.InviteId = ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
// It should be maitained in chronological order with most current
|
// It should be maitained in chronological order with most current
|
||||||
// release at the front of the list.
|
// release at the front of the list.
|
||||||
var versions = []string{
|
var versions = []string{
|
||||||
|
"2.1.0",
|
||||||
"2.0.0",
|
"2.0.0",
|
||||||
"1.4.0",
|
"1.4.0",
|
||||||
"1.3.0",
|
"1.3.0",
|
||||||
|
|||||||
1
templates/email_change_subject.html
Обычный файл
1
templates/email_change_subject.html
Обычный файл
@@ -0,0 +1 @@
|
|||||||
|
{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||||
1
templates/email_change_verify_subject.html
Обычный файл
1
templates/email_change_verify_subject.html
Обычный файл
@@ -0,0 +1 @@
|
|||||||
|
{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||||
24
templates/error.html
Обычный файл
24
templates/error.html
Обычный файл
@@ -0,0 +1,24 @@
|
|||||||
|
{{define "error"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
{{template "head" . }}
|
||||||
|
<body class="white error">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="error__container">
|
||||||
|
<div class="error__icon">
|
||||||
|
<i class="fa fa-exclamation-triangle"/>
|
||||||
|
</div>
|
||||||
|
<h2>{{.Props.Title}}</h2>
|
||||||
|
<p>{{ .Props.Message }}</p>
|
||||||
|
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
var details = {{ .Props.Details }};
|
||||||
|
if (details.length > 0) {
|
||||||
|
console.log("error details: " + details);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
92
templates/head.html
Обычный файл
92
templates/head.html
Обычный файл
@@ -0,0 +1,92 @@
|
|||||||
|
{{define "head"}}
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
|
||||||
|
<title>{{ .Props.Title }}</title>
|
||||||
|
|
||||||
|
<!-- iOS add to homescreen -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}">
|
||||||
|
<meta name="application-name" content="{{ .Props.Title }}">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<!-- iOS add to homescreen -->
|
||||||
|
|
||||||
|
<!-- Android add to homescreen -->
|
||||||
|
<link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/static/config/manifest.json">
|
||||||
|
<!-- Android add to homescreen -->
|
||||||
|
|
||||||
|
<!-- CSS Should always go first -->
|
||||||
|
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/google-fonts.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/katex.min.css">
|
||||||
|
<link rel="stylesheet" class="code_theme" href="">
|
||||||
|
|
||||||
|
<script src="/static/js/intl-1.0.0/Intl.js"></script>
|
||||||
|
<script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script>
|
||||||
|
<script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script>
|
||||||
|
<script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/js/react-0.14.3.js"></script>
|
||||||
|
<script src="/static/js/react-dom-0.14.3.js"></script>
|
||||||
|
<script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script>
|
||||||
|
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script>
|
||||||
|
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script>
|
||||||
|
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script>
|
||||||
|
<script src="/static/js/jquery-2.1.4.js"></script>
|
||||||
|
<script src="/static/js/bootstrap-3.3.5.js"></script>
|
||||||
|
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
|
||||||
|
<script src="/static/js/react-bootstrap-0.28.1.js"></script>
|
||||||
|
<script src="/static/js/velocity.min.js"></script>
|
||||||
|
<script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
|
||||||
|
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
|
||||||
|
<script src="/static/js/babel-polyfill-6.1.18.min.js"></script>
|
||||||
|
<script src="/static/js/katex.min.js"></script>
|
||||||
|
<script src="/static/js/Chart.min.js"></script>
|
||||||
|
|
||||||
|
<style id="antiClickjack">body{display:none !important;}</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) {
|
||||||
|
Object.keys(ReactIntlLocaleData).forEach(function(lang) {
|
||||||
|
ReactIntl.addLocaleData(ReactIntlLocaleData[lang]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).on('beforeunload', function(){
|
||||||
|
if (window.SocketStore) {
|
||||||
|
SocketStore.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="/static/js/libs.min.js"></script>
|
||||||
|
<script src="/static/js/bundle.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
if (self === top) {
|
||||||
|
var blocker = document.getElementById("antiClickjack");
|
||||||
|
blocker.parentNode.removeChild(blocker);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
{{end}}
|
||||||
1
templates/post_subject.html
Обычный файл
1
templates/post_subject.html
Обычный файл
@@ -0,0 +1 @@
|
|||||||
|
{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||||
12
templates/root.html
Обычный файл
12
templates/root.html
Обычный файл
@@ -0,0 +1,12 @@
|
|||||||
|
{{define "root"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
{{template "head" . }}
|
||||||
|
<body>
|
||||||
|
<div id='root'/>
|
||||||
|
<script>
|
||||||
|
window.setup_root();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
@@ -40,4 +40,5 @@ Click on the linked hashtags below, and confirm that the search results match th
|
|||||||
|
|
||||||
#### Markdown surrounding a hashtag:
|
#### Markdown surrounding a hashtag:
|
||||||
|
|
||||||
*#markdown-hashtag*
|
*#markdown* **#markdown** ~~#markdown~~
|
||||||
|
##### #markdown
|
||||||
|
|||||||
97
utils/html.go
Обычный файл
97
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
|
||||||
|
}
|
||||||
@@ -20,17 +20,16 @@ import (
|
|||||||
|
|
||||||
var IsLicensed bool = false
|
var IsLicensed bool = false
|
||||||
var License *model.License = &model.License{}
|
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"}
|
||||||
|
|
||||||
// test public key
|
|
||||||
var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
|
var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
|
||||||
0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5
|
tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
|
||||||
VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6
|
EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
|
||||||
PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN
|
HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
|
||||||
K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ
|
1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
|
||||||
NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
|
a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
|
||||||
1wIDAQAB
|
hwIDAQAB
|
||||||
-----END PUBLIC KEY-----`)
|
-----END PUBLIC KEY-----`)
|
||||||
|
|
||||||
func LoadLicense(licenseBytes []byte) {
|
func LoadLicense(licenseBytes []byte) {
|
||||||
|
|||||||
@@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) {
|
|||||||
|
|
||||||
emitPostRecievedEvent(post);
|
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();
|
||||||
|
}
|
||||||
@@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component {
|
|||||||
|
|
||||||
let title = (
|
let title = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='about.teamEdtion'
|
id='about.teamEditiont0'
|
||||||
defaultMessage='Team Edition'
|
defaultMessage='Team Edition T0'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
let licensee;
|
let licensee;
|
||||||
if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
|
if (config.BuildEnterpriseReady === 'true') {
|
||||||
title = (
|
title = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='about.enterpriseEdition'
|
id='about.teamEditiont1'
|
||||||
defaultMessage='Enterprise Edition'
|
defaultMessage='Team Edition T1'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
licensee = (
|
if (license.IsLicensed === 'true') {
|
||||||
<div className='row form-group'>
|
title = (
|
||||||
<div className='col-sm-3 info__label'>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='about.enterpriseEditione1'
|
||||||
id='about.licensed'
|
defaultMessage='Enterprise Edition E1'
|
||||||
defaultMessage='Licensed by:'
|
/>
|
||||||
/>
|
);
|
||||||
|
licensee = (
|
||||||
|
<div className='row form-group'>
|
||||||
|
<div className='col-sm-3 info__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='about.licensed'
|
||||||
|
defaultMessage='Licensed by:'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-sm-9'>{license.Company}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-sm-9'>{license.Company}</div>
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal;
|
|||||||
import LoadingScreen from './loading_screen.jsx';
|
import LoadingScreen from './loading_screen.jsx';
|
||||||
import * as Utils from '../utils/utils.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 {
|
export default class ActivityLogModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component {
|
|||||||
id='activity_log.firstTime'
|
id='activity_log.firstTime'
|
||||||
defaultMessage='First time active: {date}, {time}'
|
defaultMessage='First time active: {date}, {time}'
|
||||||
values={{
|
values={{
|
||||||
date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
|
date: (
|
||||||
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
|
<FormattedDate
|
||||||
|
value={firstAccessTime}
|
||||||
|
day='2-digit'
|
||||||
|
month='long'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
time: (
|
||||||
|
<FormattedTime
|
||||||
|
value={firstAccessTime}
|
||||||
|
hour='2-digit'
|
||||||
|
minute='2-digit'
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component {
|
|||||||
id='activity_log.lastActivity'
|
id='activity_log.lastActivity'
|
||||||
defaultMessage='Last activity: {date}, {time}'
|
defaultMessage='Last activity: {date}, {time}'
|
||||||
values={{
|
values={{
|
||||||
date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
|
date: (
|
||||||
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
|
<FormattedDate
|
||||||
|
value={lastAccessTime}
|
||||||
|
day='2-digit'
|
||||||
|
month='long'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
time: (
|
||||||
|
<FormattedTime
|
||||||
|
value={lastAccessTime}
|
||||||
|
hour='2-digit'
|
||||||
|
minute='2-digit'
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx';
|
|||||||
import TeamStore from '../../stores/team_store.jsx';
|
import TeamStore from '../../stores/team_store.jsx';
|
||||||
import * as AsyncClient from '../../utils/async_client.jsx';
|
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||||
import LoadingScreen from '../loading_screen.jsx';
|
import LoadingScreen from '../loading_screen.jsx';
|
||||||
import * as Utils from '../../utils/utils.jsx';
|
|
||||||
|
|
||||||
import EmailSettingsTab from './email_settings.jsx';
|
import EmailSettingsTab from './email_settings.jsx';
|
||||||
import LogSettingsTab from './log_settings.jsx';
|
import LogSettingsTab from './log_settings.jsx';
|
||||||
@@ -50,11 +49,6 @@ export default class AdminController extends React.Component {
|
|||||||
selected: props.tab || 'system_analytics',
|
selected: props.tab || 'system_analytics',
|
||||||
selectedTeam: props.teamId || null
|
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() {
|
componentDidMount() {
|
||||||
@@ -63,6 +57,9 @@ export default class AdminController extends React.Component {
|
|||||||
|
|
||||||
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
|
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
|
||||||
AsyncClient.getAllTeams();
|
AsyncClient.getAllTeams();
|
||||||
|
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
$('[data-toggle="popover"]').popover();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -175,7 +172,7 @@ export default class AdminController extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id='admin_controller'>
|
||||||
<div
|
<div
|
||||||
className='sidebar--menu'
|
className='sidebar--menu'
|
||||||
id='sidebar-menu'
|
id='sidebar-menu'
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import * as Utils from '../../utils/utils.jsx';
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
import * as Client from '../../utils/client.jsx';
|
|
||||||
import TeamStore from '../../stores/team_store.jsx';
|
import TeamStore from '../../stores/team_store.jsx';
|
||||||
|
|
||||||
import Constants from '../../utils/constants.jsx';
|
import Constants from '../../utils/constants.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
import {Link} from 'react-router';
|
||||||
|
|
||||||
function getStateFromStores() {
|
function getStateFromStores() {
|
||||||
return {currentTeam: TeamStore.getCurrent()};
|
return {currentTeam: TeamStore.getCurrent()};
|
||||||
}
|
}
|
||||||
@@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.blockToggle = false;
|
this.blockToggle = false;
|
||||||
|
|
||||||
this.handleLogoutClick = this.handleLogoutClick.bind(this);
|
|
||||||
|
|
||||||
this.state = getStateFromStores();
|
this.state = getStateFromStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLogoutClick(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
Client.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||||
this.blockToggle = true;
|
this.blockToggle = true;
|
||||||
@@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
|
||||||
href='#'
|
|
||||||
onClick={this.handleLogoutClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.nav.logout'
|
id='admin.nav.logout'
|
||||||
defaultMessage='Logout'
|
defaultMessage='Logout'
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className='divider'></li>
|
<li className='divider'></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import AdminSidebarHeader from './admin_sidebar_header.jsx';
|
import AdminSidebarHeader from './admin_sidebar_header.jsx';
|
||||||
import SelectTeamModal from './select_team_modal.jsx';
|
import SelectTeamModal from './select_team_modal.jsx';
|
||||||
import * as Utils from '../../utils/utils.jsx';
|
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
@@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component {
|
|||||||
handleClick(name, teamId, e) {
|
handleClick(name, teamId, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.selectTab(name, teamId);
|
this.props.selectTab(name, teamId);
|
||||||
var tokenIndex = Utils.getUrlParameter('session_token_index');
|
|
||||||
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(name, teamId) {
|
isSelected(name, teamId) {
|
||||||
@@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
teamSelectedModal(teamId) {
|
teamSelectedModal(teamId) {
|
||||||
this.props.selectedTeams[teamId] = 'true';
|
|
||||||
this.setState({showSelectModal: false});
|
this.setState({showSelectModal: false});
|
||||||
this.props.addSelectedTeam(teamId);
|
this.props.addSelectedTeam(teamId);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
|
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
|
||||||
import UserStore from '../../stores/user_store.jsx';
|
import UserStore from '../../stores/user_store.jsx';
|
||||||
import * as Utils from '../../utils/utils.jsx';
|
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component {
|
|||||||
profilePicture = (
|
profilePicture = (
|
||||||
<img
|
<img
|
||||||
className='user__picture'
|
className='user__picture'
|
||||||
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
|
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ var holders = defineMessages({
|
|||||||
},
|
},
|
||||||
baseEx: {
|
baseEx: {
|
||||||
id: 'admin.ldap.baseEx',
|
id: 'admin.ldap.baseEx',
|
||||||
defaultMessage: 'Ex "dc=mydomain,dc=com"'
|
defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
|
||||||
},
|
},
|
||||||
firstnameAttrEx: {
|
firstnameAttrEx: {
|
||||||
id: 'admin.ldap.firstnameAttrEx',
|
id: 'admin.ldap.firstnameAttrEx',
|
||||||
@@ -32,7 +32,7 @@ var holders = defineMessages({
|
|||||||
},
|
},
|
||||||
emailAttrEx: {
|
emailAttrEx: {
|
||||||
id: 'admin.ldap.emailAttrEx',
|
id: 'admin.ldap.emailAttrEx',
|
||||||
defaultMessage: 'Ex "mail"'
|
defaultMessage: 'Ex "mail" or "userPrincipalName"'
|
||||||
},
|
},
|
||||||
usernameAttrEx: {
|
usernameAttrEx: {
|
||||||
id: 'admin.ldap.usernameAttrEx',
|
id: 'admin.ldap.usernameAttrEx',
|
||||||
@@ -581,4 +581,4 @@ LdapSettings.propTypes = {
|
|||||||
config: React.PropTypes.object
|
config: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(LdapSettings);
|
export default injectIntl(LdapSettings);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class LicenseSettings extends React.Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
fileSelected: false,
|
fileSelected: false,
|
||||||
|
fileName: null,
|
||||||
serverError: null
|
serverError: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -34,7 +35,7 @@ class LicenseSettings extends React.Component {
|
|||||||
handleChange() {
|
handleChange() {
|
||||||
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
|
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
|
||||||
if (element.prop('files').length > 0) {
|
if (element.prop('files').length > 0) {
|
||||||
this.setState({fileSelected: true});
|
this.setState({fileSelected: true, fileName: element.prop('files')[0].name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +57,13 @@ class LicenseSettings extends React.Component {
|
|||||||
() => {
|
() => {
|
||||||
Utils.clearFileInput(element[0]);
|
Utils.clearFileInput(element[0]);
|
||||||
$('#upload-button').button('reset');
|
$('#upload-button').button('reset');
|
||||||
this.setState({serverError: null});
|
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
Utils.clearFileInput(element[0]);
|
Utils.clearFileInput(element[0]);
|
||||||
$('#upload-button').button('reset');
|
$('#upload-button').button('reset');
|
||||||
this.setState({serverError: error.message});
|
this.setState({fileSelected: false, fileName: null, serverError: error.message});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,12 +76,12 @@ class LicenseSettings extends React.Component {
|
|||||||
Client.removeLicenseFile(
|
Client.removeLicenseFile(
|
||||||
() => {
|
() => {
|
||||||
$('#remove-button').button('reset');
|
$('#remove-button').button('reset');
|
||||||
this.setState({serverError: null});
|
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
$('#remove-button').button('reset');
|
$('#remove-button').button('reset');
|
||||||
this.setState({serverError: error.message});
|
this.setState({fileSelected: false, fileName: null, serverError: error.message});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -172,17 +173,36 @@ class LicenseSettings extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let fileName;
|
||||||
|
if (this.state.fileName) {
|
||||||
|
fileName = this.state.fileName;
|
||||||
|
} else {
|
||||||
|
fileName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.license.noFile'
|
||||||
|
defaultMessage='No file uploaded'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
licenseKey = (
|
licenseKey = (
|
||||||
<div className='col-sm-8'>
|
<div className='col-sm-8'>
|
||||||
<input
|
<div className='file__upload'>
|
||||||
className='pull-left'
|
<button className='btn btn-default'>
|
||||||
ref='fileInput'
|
<FormattedMessage
|
||||||
type='file'
|
id='admin.license.choose'
|
||||||
accept='.mattermost-license'
|
defaultMessage='Choose File'
|
||||||
onChange={this.handleChange}
|
/>
|
||||||
/>
|
</button>
|
||||||
|
<input
|
||||||
|
ref='fileInput'
|
||||||
|
type='file'
|
||||||
|
accept='.mattermost-license'
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className={btnClass + ' pull-left'}
|
className={btnClass}
|
||||||
disabled={!this.state.fileSelected}
|
disabled={!this.state.fileSelected}
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
id='upload-button'
|
id='upload-button'
|
||||||
@@ -193,11 +213,12 @@ class LicenseSettings extends React.Component {
|
|||||||
defaultMessage='Upload'
|
defaultMessage='Upload'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<br/>
|
<div className='help-text no-margin'>
|
||||||
<br/>
|
{fileName}
|
||||||
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
{serverError}
|
{serverError}
|
||||||
<p className='help-text'>
|
<p className='help-text no-margin'>
|
||||||
<FormattedHTMLMessage
|
<FormattedHTMLMessage
|
||||||
id='admin.license.uploadDesc'
|
id='admin.license.uploadDesc'
|
||||||
defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'
|
defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ export default class UserItem extends React.Component {
|
|||||||
<td className='row member-div padding--equal'>
|
<td className='row member-div padding--equal'>
|
||||||
<img
|
<img
|
||||||
className='post-profile-img pull-left'
|
className='post-profile-img pull-left'
|
||||||
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
|
src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
|
||||||
height='36'
|
height='36'
|
||||||
width='36'
|
width='36'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx';
|
|||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
import * as Utils from '../utils/utils.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({
|
const holders = defineMessages({
|
||||||
sessionRevoked: {
|
sessionRevoked: {
|
||||||
@@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(audit.create_at);
|
const date = new Date(audit.create_at);
|
||||||
let auditInfo = {};
|
const 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'});
|
auditInfo.timestamp = (
|
||||||
|
<div>
|
||||||
|
<FormattedDate
|
||||||
|
value={date}
|
||||||
|
day='2-digit'
|
||||||
|
month='short'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
{' - '}
|
||||||
|
<FormattedTime
|
||||||
|
value={date}
|
||||||
|
hour='2-digit'
|
||||||
|
minute='2-digit'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
auditInfo.userId = audit.user_id;
|
auditInfo.userId = audit.user_id;
|
||||||
auditInfo.desc = auditDesc;
|
auditInfo.desc = auditDesc;
|
||||||
auditInfo.ip = audit.ip_address;
|
auditInfo.ip = audit.ip_address;
|
||||||
|
|||||||
@@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
this.onChannelChange = this.onChannelChange.bind(this);
|
this.validState = this.validState.bind(this);
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
this.onStoresChange = this.onStoresChange.bind(this);
|
||||||
|
|
||||||
|
this.state = this.getStateFromStores();
|
||||||
|
}
|
||||||
|
getStateFromStores() {
|
||||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||||
this.state = {
|
return {
|
||||||
showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS,
|
showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
|
||||||
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
|
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
|
||||||
user: UserStore.getCurrentUser(),
|
user: UserStore.getCurrentUser(),
|
||||||
|
channel: ChannelStore.getCurrent(),
|
||||||
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
|
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
validState() {
|
||||||
|
return this.state.user && this.state.channel && this.state.profiles;
|
||||||
|
}
|
||||||
|
onStoresChange() {
|
||||||
|
this.setState(this.getStateFromStores());
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
PreferenceStore.addChangeListener(this.onStoresChange);
|
||||||
ChannelStore.addChangeListener(this.onChannelChange);
|
ChannelStore.addChangeListener(this.onStoresChange);
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
UserStore.addChangeListener(this.onStoresChange);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
PreferenceStore.removeChangeListener(this.onStoresChange);
|
||||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
ChannelStore.removeChangeListener(this.onStoresChange);
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
UserStore.removeChangeListener(this.onStoresChange);
|
||||||
}
|
|
||||||
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()))});
|
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const channel = ChannelStore.getCurrent();
|
if (!this.validState()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const channel = this.state.channel;
|
||||||
var handleClick = null;
|
var handleClick = null;
|
||||||
let postsContainer;
|
let postsContainer;
|
||||||
let createPost;
|
let createPost;
|
||||||
|
|||||||
@@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component {
|
|||||||
memberChannel: ChannelStore.getCurrentMember(),
|
memberChannel: ChannelStore.getCurrentMember(),
|
||||||
users: extraInfo.members,
|
users: extraInfo.members,
|
||||||
userCount: extraInfo.member_count,
|
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() {
|
componentDidMount() {
|
||||||
ChannelStore.addChangeListener(this.onListenerChange);
|
ChannelStore.addChangeListener(this.onListenerChange);
|
||||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||||
SearchStore.addSearchChangeListener(this.onListenerChange);
|
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||||
PreferenceStore.addChangeListener(this.onListenerChange);
|
PreferenceStore.addChangeListener(this.onListenerChange);
|
||||||
|
UserStore.addChangeListener(this.onListenerChange);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||||
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||||
PreferenceStore.removeChangeListener(this.onListenerChange);
|
PreferenceStore.removeChangeListener(this.onListenerChange);
|
||||||
|
UserStore.removeChangeListener(this.onListenerChange);
|
||||||
}
|
}
|
||||||
onListenerChange() {
|
onListenerChange() {
|
||||||
const newState = this.getStateFromStores();
|
const newState = this.getStateFromStores();
|
||||||
@@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
searchMentions(e) {
|
searchMentions(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const user = this.props.user;
|
const user = this.state.currentUser;
|
||||||
|
|
||||||
let terms = '';
|
let terms = '';
|
||||||
if (user.notify_props && user.notify_props.mention_keys) {
|
if (user.notify_props && user.notify_props.mention_keys) {
|
||||||
@@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
if (this.state.channel === null) {
|
if (!this.validState()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component {
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
let channelTitle = channel.display_name;
|
let channelTitle = channel.display_name;
|
||||||
const currentId = this.props.user.id;
|
const currentId = this.state.currentUser.id;
|
||||||
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles);
|
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
|
||||||
const isDirect = (this.state.channel.type === 'D');
|
const isDirect = (this.state.channel.type === 'D');
|
||||||
|
|
||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
@@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
<ToggleModalButton
|
<ToggleModalButton
|
||||||
role='menuitem'
|
role='menuitem'
|
||||||
dialogType={ChannelInviteModal}
|
dialogType={ChannelInviteModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='chanel_header.addMembers'
|
id='chanel_header.addMembers'
|
||||||
@@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component {
|
|||||||
<ToggleModalButton
|
<ToggleModalButton
|
||||||
role='menuitem'
|
role='menuitem'
|
||||||
dialogType={ChannelNotificationsModal}
|
dialogType={ChannelNotificationsModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{
|
||||||
|
channel,
|
||||||
|
channelMember: this.state.memberChannel,
|
||||||
|
currentUser: this.state.currentUser
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_header.notificationPreferences'
|
id='channel_header.notificationPreferences'
|
||||||
@@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelHeader.propTypes = {
|
ChannelHeader.propTypes = {
|
||||||
user: React.PropTypes.object.isRequired
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import FilteredUserList from './filtered_user_list.jsx';
|
import FilteredUserList from './filtered_user_list.jsx';
|
||||||
import LoadingScreen from './loading_screen.jsx';
|
import LoadingScreen from './loading_screen.jsx';
|
||||||
|
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
@@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl';
|
|||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class ChannelInviteModal extends React.Component {
|
export default class ChannelInviteModal extends React.Component {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this.onListenerChange = this.onListenerChange.bind(this);
|
this.onListenerChange = this.onListenerChange.bind(this);
|
||||||
this.handleInvite = this.handleInvite.bind(this);
|
this.handleInvite = this.handleInvite.bind(this);
|
||||||
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
this.createInviteButton = this.createInviteButton.bind(this);
|
this.createInviteButton = this.createInviteButton.bind(this);
|
||||||
|
|
||||||
// the state gets populated when the modal is shown
|
this.state = this.getStateFromStores();
|
||||||
this.state = {
|
|
||||||
loading: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
if (!this.props.show && !nextProps.show) {
|
if (!this.props.show && !nextProps.show) {
|
||||||
@@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentUser = UserStore.getCurrentUser();
|
||||||
|
if (!currentUser) {
|
||||||
|
return {
|
||||||
|
loading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMember = ChannelStore.getCurrentMember();
|
||||||
|
if (!currentMember) {
|
||||||
|
return {
|
||||||
|
loading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const memberIds = extraInfo.members.map((user) => user.id);
|
const memberIds = extraInfo.members.map((user) => user.id);
|
||||||
|
|
||||||
var nonmembers = [];
|
var nonmembers = [];
|
||||||
@@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
nonmembers,
|
nonmembers,
|
||||||
loading: false
|
loading: false,
|
||||||
|
currentUser,
|
||||||
|
currentMember
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
UserStore.removeChangeListener(this.onListenerChange);
|
UserStore.removeChangeListener(this.onListenerChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||||
|
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||||
|
UserStore.removeChangeListener(this.onListenerChange);
|
||||||
|
}
|
||||||
onListenerChange() {
|
onListenerChange() {
|
||||||
var newState = this.getStateFromStores();
|
var newState = this.getStateFromStores();
|
||||||
if (!Utils.areObjectsEqual(this.state, newState)) {
|
if (!Utils.areObjectsEqual(this.state, newState)) {
|
||||||
@@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
if (Utils.windowHeight() <= 1200) {
|
if (Utils.windowHeight() <= 1200) {
|
||||||
maxHeight = Utils.windowHeight() - 300;
|
maxHeight = Utils.windowHeight() - 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<FilteredUserList
|
<FilteredUserList
|
||||||
style={{maxHeight}}
|
style={{maxHeight}}
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
/* This is a special React control with the sole purpose of making all the AsyncClient calls
|
|
||||||
to the server on page load. This is to prevent other React controls from spamming
|
|
||||||
AsyncClient with requests. */
|
|
||||||
|
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
|
||||||
import * as Client from '../utils/client.jsx';
|
|
||||||
import SocketStore from '../stores/socket_store.jsx';
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
|
||||||
import PostStore from '../stores/post_store.jsx';
|
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import PreferenceStore from '../stores/preference_store.jsx';
|
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
|
||||||
import Constants from '../utils/constants.jsx';
|
|
||||||
|
|
||||||
import {intlShape, injectIntl, defineMessages} from 'mm-intl';
|
|
||||||
|
|
||||||
const holders = defineMessages({
|
|
||||||
socketError: {
|
|
||||||
id: 'channel_loader.socketError',
|
|
||||||
defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'
|
|
||||||
},
|
|
||||||
someone: {
|
|
||||||
id: 'channel_loader.someone',
|
|
||||||
defaultMessage: 'Someone'
|
|
||||||
},
|
|
||||||
posted: {
|
|
||||||
id: 'channel_loader.posted',
|
|
||||||
defaultMessage: 'Posted'
|
|
||||||
},
|
|
||||||
uploadedImage: {
|
|
||||||
id: 'channel_loader.uploadedImage',
|
|
||||||
defaultMessage: ' uploaded an image'
|
|
||||||
},
|
|
||||||
uploadedFile: {
|
|
||||||
id: 'channel_loader.uploadedFile',
|
|
||||||
defaultMessage: ' uploaded a file'
|
|
||||||
},
|
|
||||||
something: {
|
|
||||||
id: 'channel_loader.something',
|
|
||||||
defaultMessage: ' did something new'
|
|
||||||
},
|
|
||||||
wrote: {
|
|
||||||
id: 'channel_loader.wrote',
|
|
||||||
defaultMessage: ' wrote: '
|
|
||||||
},
|
|
||||||
connectionError: {
|
|
||||||
id: 'channel_loader.connection_error',
|
|
||||||
defaultMessage: 'There appears to be a problem with your internet connection.'
|
|
||||||
},
|
|
||||||
unknownError: {
|
|
||||||
id: 'channel_loader.unknown_error',
|
|
||||||
defaultMessage: 'We received an unexpected status code from the server.'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class ChannelLoader extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.intervalId = null;
|
|
||||||
|
|
||||||
this.onSocketChange = this.onSocketChange.bind(this);
|
|
||||||
|
|
||||||
const {formatMessage} = this.props.intl;
|
|
||||||
SocketStore.setTranslations({
|
|
||||||
socketError: formatMessage(holders.socketError),
|
|
||||||
someone: formatMessage(holders.someone),
|
|
||||||
posted: formatMessage(holders.posted),
|
|
||||||
uploadedImage: formatMessage(holders.uploadedImage),
|
|
||||||
uploadedFile: formatMessage(holders.uploadedFile),
|
|
||||||
something: formatMessage(holders.something),
|
|
||||||
wrote: formatMessage(holders.wrote)
|
|
||||||
});
|
|
||||||
|
|
||||||
Client.setTranslations({
|
|
||||||
connectionError: formatMessage(holders.connectionError),
|
|
||||||
unknownError: formatMessage(holders.unknownError)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
/* Initial aysnc loads */
|
|
||||||
AsyncClient.getPosts(ChannelStore.getCurrentId());
|
|
||||||
AsyncClient.getChannels();
|
|
||||||
AsyncClient.getChannelExtraInfo();
|
|
||||||
AsyncClient.findTeams();
|
|
||||||
AsyncClient.getMyTeam();
|
|
||||||
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
|
|
||||||
|
|
||||||
/* Perform pending post clean-up */
|
|
||||||
PostStore.clearPendingPosts();
|
|
||||||
|
|
||||||
/* Set up interval functions */
|
|
||||||
this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000);
|
|
||||||
|
|
||||||
/* Device tracking setup */
|
|
||||||
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
|
|
||||||
if (iOS) {
|
|
||||||
$('body').addClass('ios');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set up tracking for whether the window is active */
|
|
||||||
window.isActive = true;
|
|
||||||
|
|
||||||
$(window).on('focus', function windowFocus() {
|
|
||||||
AsyncClient.updateLastViewedAt();
|
|
||||||
ChannelStore.resetCounts(ChannelStore.getCurrentId());
|
|
||||||
ChannelStore.emitChange();
|
|
||||||
window.isActive = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(window).on('blur', function windowBlur() {
|
|
||||||
window.isActive = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Start global change listeners setup */
|
|
||||||
SocketStore.addChangeListener(this.onSocketChange);
|
|
||||||
|
|
||||||
/* Update CSS classes to match user theme */
|
|
||||||
var user = UserStore.getCurrentUser();
|
|
||||||
|
|
||||||
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
|
|
||||||
Utils.applyTheme(user.theme_props);
|
|
||||||
} else {
|
|
||||||
Utils.applyTheme(Constants.THEMES.default);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
|
|
||||||
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
|
|
||||||
Utils.applyFont(selectedFont);
|
|
||||||
|
|
||||||
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
|
|
||||||
if (ev.type === 'mouseenter') {
|
|
||||||
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
|
|
||||||
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
|
|
||||||
} else {
|
|
||||||
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
|
|
||||||
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
|
|
||||||
if (ev.type === 'mouseenter') {
|
|
||||||
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
|
|
||||||
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
|
|
||||||
} else {
|
|
||||||
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
|
|
||||||
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
|
|
||||||
if (ev.type === 'mouseenter') {
|
|
||||||
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
|
|
||||||
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
|
|
||||||
} else {
|
|
||||||
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
|
|
||||||
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Prevent backspace from navigating back a page */
|
|
||||||
$(window).on('keydown.preventBackspace', (e) => {
|
|
||||||
if (e.which === 8 && !$(e.target).is('input, textarea')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearInterval(this.intervalId);
|
|
||||||
|
|
||||||
$(window).off('focus');
|
|
||||||
$(window).off('blur');
|
|
||||||
|
|
||||||
SocketStore.removeChangeListener(this.onSocketChange);
|
|
||||||
|
|
||||||
$('body').off('click.userpopover');
|
|
||||||
$('body').off('mouseenter mouseleave', '.post');
|
|
||||||
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
|
|
||||||
|
|
||||||
$('.modal').off('show.bs.modal');
|
|
||||||
|
|
||||||
$(window).off('keydown.preventBackspace');
|
|
||||||
}
|
|
||||||
onSocketChange(msg) {
|
|
||||||
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
|
|
||||||
UserStore.setStatus(msg.user_id, 'online');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return <div/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelLoader.propTypes = {
|
|
||||||
intl: intlShape.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ChannelLoader);
|
|
||||||
@@ -6,7 +6,6 @@ import SettingItemMin from './setting_item_min.jsx';
|
|||||||
import SettingItemMax from './setting_item_max.jsx';
|
import SettingItemMax from './setting_item_max.jsx';
|
||||||
|
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
@@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onListenerChange = this.onListenerChange.bind(this);
|
|
||||||
this.updateSection = this.updateSection.bind(this);
|
this.updateSection = this.updateSection.bind(this);
|
||||||
|
|
||||||
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
|
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
|
||||||
@@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
|
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
|
||||||
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
|
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
|
||||||
|
|
||||||
const member = ChannelStore.getMember(props.channel.id);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
notifyLevel: member.notify_props.desktop,
|
activeSection: '',
|
||||||
markUnreadLevel: member.notify_props.mark_unread,
|
notifyLevel: '',
|
||||||
channelId: ChannelStore.getCurrentId(),
|
unreadLevel: ''
|
||||||
activeSection: ''
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (!this.props.show && nextProps.show) {
|
|
||||||
this.onListenerChange();
|
|
||||||
ChannelStore.addChangeListener(this.onListenerChange);
|
|
||||||
} else {
|
|
||||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onListenerChange() {
|
|
||||||
const curChannelId = ChannelStore.getCurrentId();
|
|
||||||
|
|
||||||
if (!curChannelId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newState = {channelId: curChannelId};
|
|
||||||
const member = ChannelStore.getMember(curChannelId);
|
|
||||||
|
|
||||||
if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
|
|
||||||
newState.notifyLevel = member.notify_props.desktop;
|
|
||||||
newState.markUnreadLevel = member.notify_props.mark_unread;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
updateSection(section) {
|
updateSection(section) {
|
||||||
this.setState({activeSection: section});
|
this.setState({activeSection: section});
|
||||||
}
|
}
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (!this.props.show && nextProps.show) {
|
||||||
|
this.setState({
|
||||||
|
notifyLevel: nextProps.channelMember.notify_props.desktop,
|
||||||
|
unreadLevel: nextProps.channelMember.notify_props.mark_unread
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
handleSubmitNotifyLevel() {
|
handleSubmitNotifyLevel() {
|
||||||
var channelId = this.state.channelId;
|
var channelId = this.props.channel.id;
|
||||||
var notifyLevel = this.state.notifyLevel;
|
var notifyLevel = this.state.notifyLevel;
|
||||||
|
|
||||||
if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
|
if (this.props.channelMember.notify_props.desktop === notifyLevel) {
|
||||||
this.updateSection('');
|
this.updateSection('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = {};
|
var data = {};
|
||||||
data.channel_id = channelId;
|
data.channel_id = channelId;
|
||||||
data.user_id = UserStore.getCurrentId();
|
data.user_id = this.props.currentUser.id;
|
||||||
data.desktop = notifyLevel;
|
data.desktop = notifyLevel;
|
||||||
|
|
||||||
|
//TODO: This should be moved to event_helpers
|
||||||
Client.updateNotifyProps(data,
|
Client.updateNotifyProps(data,
|
||||||
() => {
|
() => {
|
||||||
|
// YUCK
|
||||||
var member = ChannelStore.getMember(channelId);
|
var member = ChannelStore.getMember(channelId);
|
||||||
member.notify_props.desktop = notifyLevel;
|
member.notify_props.desktop = notifyLevel;
|
||||||
ChannelStore.setChannelMember(member);
|
ChannelStore.setChannelMember(member);
|
||||||
@@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
this.setState({notifyLevel});
|
this.setState({notifyLevel});
|
||||||
}
|
}
|
||||||
createNotifyLevelSection(serverError) {
|
createNotifyLevelSection(serverError) {
|
||||||
var handleUpdateSection;
|
// Get glabal user setting for notifications
|
||||||
|
const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
|
||||||
const user = UserStore.getCurrentUser();
|
|
||||||
const globalNotifyLevel = user.notify_props.desktop;
|
|
||||||
|
|
||||||
let globalNotifyLevelName;
|
let globalNotifyLevelName;
|
||||||
if (globalNotifyLevel === 'all') {
|
if (globalNotifyLevel === 'all') {
|
||||||
globalNotifyLevelName = (
|
globalNotifyLevelName = (
|
||||||
@@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const notificationLevel = this.state.notifyLevel;
|
||||||
|
|
||||||
if (this.state.activeSection === 'desktop') {
|
if (this.state.activeSection === 'desktop') {
|
||||||
var notifyActive = [false, false, false, false];
|
const notifyActive = [false, false, false, false];
|
||||||
if (this.state.notifyLevel === 'default') {
|
if (notificationLevel === 'default') {
|
||||||
notifyActive[0] = true;
|
notifyActive[0] = true;
|
||||||
} else if (this.state.notifyLevel === 'all') {
|
} else if (notificationLevel === 'all') {
|
||||||
notifyActive[1] = true;
|
notifyActive[1] = true;
|
||||||
} else if (this.state.notifyLevel === 'mention') {
|
} else if (notificationLevel === 'mention') {
|
||||||
notifyActive[2] = true;
|
notifyActive[2] = true;
|
||||||
} else {
|
} else {
|
||||||
notifyActive[3] = true;
|
notifyActive[3] = true;
|
||||||
@@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
handleUpdateSection = function updateSection(e) {
|
const handleUpdateSection = function updateSection(e) {
|
||||||
this.updateSection('');
|
this.updateSection('');
|
||||||
this.onListenerChange();
|
this.onListenerChange();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var describe;
|
var describe;
|
||||||
if (this.state.notifyLevel === 'default') {
|
if (notificationLevel === 'default') {
|
||||||
describe = (
|
describe = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_notifications.globalDefault'
|
id='channel_notifications.globalDefault'
|
||||||
@@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.state.notifyLevel === 'mention') {
|
} else if (notificationLevel === 'mention') {
|
||||||
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
|
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
|
||||||
} else if (this.state.notifyLevel === 'all') {
|
} else if (notificationLevel === 'all') {
|
||||||
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
|
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
|
||||||
} else {
|
} else {
|
||||||
describe = (<FormattedMessage id='channel_notifications.never'/>);
|
describe = (<FormattedMessage id='channel_notifications.never'/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateSection = function updateSection(e) {
|
|
||||||
this.updateSection('desktop');
|
|
||||||
e.preventDefault();
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingItemMin
|
<SettingItemMin
|
||||||
title={sendDesktop}
|
title={sendDesktop}
|
||||||
describe={describe}
|
describe={describe}
|
||||||
updateSection={handleUpdateSection}
|
updateSection={() => {
|
||||||
|
this.updateSection('desktop');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmitMarkUnreadLevel() {
|
handleSubmitMarkUnreadLevel() {
|
||||||
const channelId = this.state.channelId;
|
const channelId = this.props.channel.id;
|
||||||
const markUnreadLevel = this.state.markUnreadLevel;
|
const markUnreadLevel = this.state.unreadLevel;
|
||||||
|
|
||||||
if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
|
if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
|
||||||
this.updateSection('');
|
this.updateSection('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
user_id: UserStore.getCurrentId(),
|
user_id: this.props.currentUser.id,
|
||||||
mark_unread: markUnreadLevel
|
mark_unread: markUnreadLevel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//TODO: This should be fixed, moved to event_helpers
|
||||||
Client.updateNotifyProps(data,
|
Client.updateNotifyProps(data,
|
||||||
() => {
|
() => {
|
||||||
|
// Yuck...
|
||||||
var member = ChannelStore.getMember(channelId);
|
var member = ChannelStore.getMember(channelId);
|
||||||
member.notify_props.mark_unread = markUnreadLevel;
|
member.notify_props.mark_unread = markUnreadLevel;
|
||||||
ChannelStore.setChannelMember(member);
|
ChannelStore.setChannelMember(member);
|
||||||
@@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateMarkUnreadLevel(markUnreadLevel) {
|
handleUpdateMarkUnreadLevel(unreadLevel) {
|
||||||
this.setState({markUnreadLevel});
|
this.setState({unreadLevel});
|
||||||
}
|
}
|
||||||
|
|
||||||
createMarkUnreadLevelSection(serverError) {
|
createMarkUnreadLevelSection(serverError) {
|
||||||
@@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
checked={this.state.markUnreadLevel === 'all'}
|
checked={this.state.unreadLevel === 'all'}
|
||||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
||||||
/>
|
/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
checked={this.state.markUnreadLevel === 'mention'}
|
checked={this.state.unreadLevel === 'mention'}
|
||||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
||||||
/>
|
/>
|
||||||
<FormattedMessage id='channel_notifications.onlyMentions'/>
|
<FormattedMessage id='channel_notifications.onlyMentions'/>
|
||||||
@@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
let describe;
|
let describe;
|
||||||
|
|
||||||
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
|
if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
|
||||||
describe = (
|
describe = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_notifications.allUnread'
|
id='channel_notifications.allUnread'
|
||||||
@@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
ChannelNotificationsModal.propTypes = {
|
ChannelNotificationsModal.propTypes = {
|
||||||
show: React.PropTypes.bool.isRequired,
|
show: React.PropTypes.bool.isRequired,
|
||||||
onHide: React.PropTypes.func.isRequired,
|
onHide: React.PropTypes.func.isRequired,
|
||||||
channel: React.PropTypes.object.isRequired
|
channel: React.PropTypes.object.isRequired,
|
||||||
|
channelMember: React.PropTypes.object.isRequired,
|
||||||
|
currentUser: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,34 +2,11 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import CenterPanel from '../components/center_panel.jsx';
|
import CenterPanel from '../components/center_panel.jsx';
|
||||||
import Sidebar from '../components/sidebar.jsx';
|
|
||||||
import SidebarRight from '../components/sidebar_right.jsx';
|
|
||||||
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
|
|
||||||
|
|
||||||
export default class ChannelView extends React.Component {
|
export default class ChannelView extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className='container-fluid'>
|
<CenterPanel/>
|
||||||
<div
|
|
||||||
className='sidebar--right'
|
|
||||||
id='sidebar-right'
|
|
||||||
>
|
|
||||||
<SidebarRight/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='sidebar--menu'
|
|
||||||
id='sidebar-menu'
|
|
||||||
>
|
|
||||||
<SidebarRightMenu/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='sidebar--left'
|
|
||||||
id='sidebar-left'
|
|
||||||
>
|
|
||||||
<Sidebar/>
|
|
||||||
</div>
|
|
||||||
<CenterPanel/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,4 +14,5 @@ ChannelView.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ChannelView.propTypes = {
|
ChannelView.propTypes = {
|
||||||
|
params: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import EmailToSSO from './email_to_sso.jsx';
|
import EmailToSSO from './email_to_sso.jsx';
|
||||||
import SSOToEmail from './sso_to_email.jsx';
|
import SSOToEmail from './sso_to_email.jsx';
|
||||||
|
import TeamStore from '../../stores/team_store.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
@@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.onTeamChange = this.onTeamChange.bind(this);
|
||||||
|
this.updateStateFromStores = this.updateStateFromStores.bind(this);
|
||||||
|
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({
|
||||||
|
email: this.props.location.query.email,
|
||||||
|
newType: this.props.location.query.new_type,
|
||||||
|
oldType: this.props.location.query.old_type,
|
||||||
|
teamName: this.props.params.team,
|
||||||
|
teamDisplayName: ''
|
||||||
|
});
|
||||||
|
this.updateStateFromStores();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
TeamStore.addChangeListener(this.onTeamChange);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
TeamStore.removeChangeListener(this.onTeamChange);
|
||||||
|
}
|
||||||
|
updateStateFromStores() {
|
||||||
|
const team = TeamStore.getByName(this.state.teamName);
|
||||||
|
let displayName = '';
|
||||||
|
if (team) {
|
||||||
|
displayName = team.displayName;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
teamDisplayName: displayName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onTeamChange() {
|
||||||
|
this.updateStateFromStores();
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
|
if (this.state.teamDisplayName === '') {
|
||||||
|
return (<div/>);
|
||||||
|
}
|
||||||
let content;
|
let content;
|
||||||
if (this.props.email === '') {
|
if (this.state.email === '') {
|
||||||
content = (
|
content = (
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else if (this.props.currentType === '' && this.props.newType !== '') {
|
} else if (this.state.oldType === '' && this.state.newType !== '') {
|
||||||
content = (
|
content = (
|
||||||
<EmailToSSO
|
<EmailToSSO
|
||||||
email={this.props.email}
|
email={this.state.email}
|
||||||
type={this.props.newType}
|
type={this.state.newType}
|
||||||
teamName={this.props.teamName}
|
teamName={this.state.teamName}
|
||||||
teamDisplayName={this.props.teamDisplayName}
|
teamDisplayName={this.state.teamDisplayName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<SSOToEmail
|
<SSOToEmail
|
||||||
email={this.props.email}
|
email={this.state.email}
|
||||||
currentType={this.props.currentType}
|
currentType={this.state.oldType}
|
||||||
teamName={this.props.teamName}
|
teamName={this.state.teamName}
|
||||||
teamDisplayName={this.props.teamDisplayName}
|
teamDisplayName={this.state.teamDisplayName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='signup-header'>
|
||||||
|
<a href='/'>
|
||||||
|
<span className='fa fa-chevron-left'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='web.header.back'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className='col-sm-12'>
|
||||||
|
<div className='signup-team__container'>
|
||||||
|
<img
|
||||||
|
className='signup-team-logo'
|
||||||
|
src='/static/images/logo.png'
|
||||||
|
/>
|
||||||
|
<div id='claim'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClaimAccount.defaultProps = {
|
ClaimAccount.defaultProps = {
|
||||||
};
|
};
|
||||||
ClaimAccount.propTypes = {
|
ClaimAccount.propTypes = {
|
||||||
currentType: React.PropTypes.string.isRequired,
|
params: React.PropTypes.object.isRequired,
|
||||||
newType: React.PropTypes.string.isRequired,
|
location: React.PropTypes.object.isRequired
|
||||||
email: React.PropTypes.string.isRequired,
|
|
||||||
teamName: React.PropTypes.string.isRequired,
|
|
||||||
teamDisplayName: React.PropTypes.string.isRequired
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ SSOToEmail.propTypes = {
|
|||||||
currentType: React.PropTypes.string.isRequired,
|
currentType: React.PropTypes.string.isRequired,
|
||||||
email: React.PropTypes.string.isRequired,
|
email: React.PropTypes.string.isRequired,
|
||||||
teamName: React.PropTypes.string.isRequired,
|
teamName: React.PropTypes.string.isRequired,
|
||||||
teamDisplayName: React.PropTypes.string.isRequired
|
teamDisplayName: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SSOToEmail);
|
export default injectIntl(SSOToEmail);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
|
|||||||
import TutorialTip from './tutorial/tutorial_tip.jsx';
|
import TutorialTip from './tutorial/tutorial_tip.jsx';
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
@@ -165,7 +165,7 @@ class CreatePost extends React.Component {
|
|||||||
|
|
||||||
const channel = ChannelStore.get(this.state.channelId);
|
const channel = ChannelStore.get(this.state.channelId);
|
||||||
|
|
||||||
EventHelpers.emitUserPostedEvent(post);
|
GlobalActions.emitUserPostedEvent(post);
|
||||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||||
|
|
||||||
Client.createPost(post, channel,
|
Client.createPost(post, channel,
|
||||||
@@ -177,7 +177,7 @@ class CreatePost extends React.Component {
|
|||||||
member.last_viewed_at = Date.now();
|
member.last_viewed_at = Date.now();
|
||||||
ChannelStore.setChannelMember(member);
|
ChannelStore.setChannelMember(member);
|
||||||
|
|
||||||
EventHelpers.emitPostRecievedEvent(data);
|
GlobalActions.emitPostRecievedEvent(data);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
|
|||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
import {browserHistory} from 'react-router';
|
||||||
|
|
||||||
export default class DeleteChannelModal extends React.Component {
|
export default class DeleteChannelModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
|
||||||
Client.deleteChannel(
|
Client.deleteChannel(
|
||||||
this.props.channel.id,
|
this.props.channel.id,
|
||||||
() => {
|
() => {
|
||||||
AsyncClient.getChannels(true);
|
AsyncClient.getChannels(true);
|
||||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
|
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
AsyncClient.dispatchError(err, 'handleDelete');
|
AsyncClient.dispatchError(err, 'handleDelete');
|
||||||
|
|||||||
82
web/react/components/do_verify_email.jsx
Обычный файл
82
web/react/components/do_verify_email.jsx
Обычный файл
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
import * as Client from '../utils/client.jsx';
|
||||||
|
import LoadingScreen from './loading_screen.jsx';
|
||||||
|
|
||||||
|
import {browserHistory} from 'react-router';
|
||||||
|
|
||||||
|
export default class DoVerifyEmail extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
verifyStatus: 'pending',
|
||||||
|
serverError: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
componentWillMount() {
|
||||||
|
const uid = this.props.location.query.uid;
|
||||||
|
const hid = this.props.location.query.hid;
|
||||||
|
const teamName = this.props.location.query.teamname;
|
||||||
|
const email = this.props.location.query.email;
|
||||||
|
|
||||||
|
Client.verifyEmail(
|
||||||
|
() => {
|
||||||
|
browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({verifyStatus: 'failure', serverError: err.message});
|
||||||
|
},
|
||||||
|
uid,
|
||||||
|
hid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.verifyStatus !== 'failure') {
|
||||||
|
return (<LoadingScreen/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='signup-header'>
|
||||||
|
<a href='/'>
|
||||||
|
<span className='fa fa-chevron-left'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='web.header.back'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className='col-sm-12'>
|
||||||
|
<div className='signup-team__container'>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage
|
||||||
|
id='email_verify.almost'
|
||||||
|
defaultMessage='{siteName}: You are almost done'
|
||||||
|
values={{
|
||||||
|
siteName: global.window.mm_config.SiteName
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage id='email_verify.verifyFailed'/>
|
||||||
|
</p>
|
||||||
|
<p className='alert alert-danger'>
|
||||||
|
<i className='fa fa-times'/>
|
||||||
|
{this.state.serverError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DoVerifyEmail.defaultProps = {
|
||||||
|
};
|
||||||
|
DoVerifyEmail.propTypes = {
|
||||||
|
location: React.PropTypes.object.isRequired
|
||||||
|
};
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import * as TextFormatting from '../utils/text_formatting.jsx';
|
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
|
|
||||||
export default class Docs extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
UserStore.setCurrentUser(global.window.mm_user || {});
|
|
||||||
|
|
||||||
this.state = {text: ''};
|
|
||||||
const errorState = {text: '## 404'};
|
|
||||||
|
|
||||||
if (props.site) {
|
|
||||||
$.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => {
|
|
||||||
this.setState({text: response});
|
|
||||||
}, () => {
|
|
||||||
this.setState(errorState);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState(errorState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Docs.defaultProps = {
|
|
||||||
site: ''
|
|
||||||
};
|
|
||||||
Docs.propTypes = {
|
|
||||||
site: React.PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
import Textbox from './textbox.jsx';
|
import Textbox from './textbox.jsx';
|
||||||
import BrowserStore from '../stores/browser_store.jsx';
|
import BrowserStore from '../stores/browser_store.jsx';
|
||||||
import PostStore from '../stores/post_store.jsx';
|
import PostStore from '../stores/post_store.jsx';
|
||||||
@@ -45,7 +45,7 @@ class EditPostModal extends React.Component {
|
|||||||
delete tempState.editText;
|
delete tempState.editText;
|
||||||
BrowserStore.setItem('edit_state_transfer', tempState);
|
BrowserStore.setItem('edit_state_transfer', tempState);
|
||||||
$('#edit_post').modal('hide');
|
$('#edit_post').modal('hide');
|
||||||
EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
|
|
||||||
|
|
||||||
export default class EmailVerify extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleResend = this.handleResend.bind(this);
|
|
||||||
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
handleResend() {
|
|
||||||
const newAddress = window.location.href.replace('&resend_success=true', '');
|
|
||||||
window.location.href = newAddress + '&resend=true';
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
var title = '';
|
|
||||||
var body = '';
|
|
||||||
var resend = '';
|
|
||||||
var resendConfirm = '';
|
|
||||||
if (this.props.isVerified === 'true') {
|
|
||||||
title = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='email_verify.verified'
|
|
||||||
defaultMessage='{siteName} Email Verified'
|
|
||||||
values={{
|
|
||||||
siteName: global.window.mm_config.SiteName
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
body = (
|
|
||||||
<FormattedHTMLMessage
|
|
||||||
id='email_verify.verifiedBody'
|
|
||||||
defaultMessage='<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>'
|
|
||||||
values={{
|
|
||||||
url: this.props.teamURL + '?email=' + this.props.userEmail
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
title = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='email_verify.almost'
|
|
||||||
defaultMessage='{siteName}: You are almost done'
|
|
||||||
values={{
|
|
||||||
siteName: global.window.mm_config.SiteName
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
body = (
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id='email_verify.notVerifiedBody'
|
|
||||||
defaultMessage='Please verify your email address. Check your inbox for an email.'
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
resend = (
|
|
||||||
<button
|
|
||||||
onClick={this.handleResend}
|
|
||||||
className='btn btn-primary'
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='email_verify.resend'
|
|
||||||
defaultMessage='Resend Email'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
if (this.props.resendSuccess) {
|
|
||||||
resendConfirm = (
|
|
||||||
<div><br/><p className='alert alert-success'><i className='fa fa-check'></i>
|
|
||||||
<FormattedMessage
|
|
||||||
id='email_verify.sent'
|
|
||||||
defaultMessage=' Verification email sent.'
|
|
||||||
/>
|
|
||||||
</p></div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='col-sm-12'>
|
|
||||||
<div className='signup-team__container'>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<div>
|
|
||||||
{body}
|
|
||||||
{resend}
|
|
||||||
{resendConfirm}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EmailVerify.defaultProps = {
|
|
||||||
isVerified: 'false',
|
|
||||||
teamURL: '',
|
|
||||||
userEmail: '',
|
|
||||||
resendSuccess: 'false'
|
|
||||||
};
|
|
||||||
EmailVerify.propTypes = {
|
|
||||||
isVerified: React.PropTypes.string,
|
|
||||||
teamURL: React.PropTypes.string,
|
|
||||||
userEmail: React.PropTypes.string,
|
|
||||||
resendSuccess: React.PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -43,7 +43,7 @@ class FileAttachment extends React.Component {
|
|||||||
|
|
||||||
if (type === 'image') {
|
if (type === 'image') {
|
||||||
var self = this; // Need this reference since we use the given "this"
|
var self = this; // Need this reference since we use the given "this"
|
||||||
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
|
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
|
||||||
return function loader() {
|
return function loader() {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
if (name in self.refs) {
|
if (name in self.refs) {
|
||||||
@@ -114,7 +114,7 @@ class FileAttachment extends React.Component {
|
|||||||
var re3 = new RegExp('\\)', 'g');
|
var re3 = new RegExp('\\)', 'g');
|
||||||
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||||
|
|
||||||
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
|
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeBackgroundImage(name) {
|
removeBackgroundImage(name) {
|
||||||
@@ -185,6 +185,7 @@ class FileAttachment extends React.Component {
|
|||||||
data-toggle='tooltip'
|
data-toggle='tooltip'
|
||||||
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
|
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
|
||||||
className='post-image__name'
|
className='post-image__name'
|
||||||
|
target='_blank'
|
||||||
>
|
>
|
||||||
{trimmedFilename}
|
{trimmedFilename}
|
||||||
</a>
|
</a>
|
||||||
@@ -193,6 +194,7 @@ class FileAttachment extends React.Component {
|
|||||||
href={fileUrl}
|
href={fileUrl}
|
||||||
download={filenameString}
|
download={filenameString}
|
||||||
className='post-image__download'
|
className='post-image__download'
|
||||||
|
target='_blank'
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className='fa fa-download'
|
className='fa fa-download'
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import * as utils from '../utils/utils.jsx';
|
|
||||||
import * as client from '../utils/client.jsx';
|
|
||||||
|
|
||||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
|
||||||
|
|
||||||
var holders = defineMessages({
|
|
||||||
submitError: {
|
|
||||||
id: 'find_team.submitError',
|
|
||||||
defaultMessage: 'Please enter a valid email address'
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
id: 'find_team.placeholder',
|
|
||||||
defaultMessage: 'you@domain.com'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class FindTeam extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var state = { };
|
|
||||||
|
|
||||||
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
|
|
||||||
if (!email || !utils.isEmail(email)) {
|
|
||||||
state.email_error = this.props.intl.formatMessage(holders.submitError);
|
|
||||||
this.setState(state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.email_error = '';
|
|
||||||
|
|
||||||
client.findTeamsSendEmail(email,
|
|
||||||
function success() {
|
|
||||||
state.sent = true;
|
|
||||||
this.setState(state);
|
|
||||||
}.bind(this),
|
|
||||||
function fail(err) {
|
|
||||||
state.email_error = err.message;
|
|
||||||
this.setState(state);
|
|
||||||
}.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var emailError = null;
|
|
||||||
var emailErrorClass = 'form-group';
|
|
||||||
|
|
||||||
if (this.state.email_error) {
|
|
||||||
emailError = <label className='control-label'>{this.state.email_error}</label>;
|
|
||||||
emailErrorClass = 'form-group has-error';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.sent) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.findTitle'
|
|
||||||
defaultMessage='Find Your Team'
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.findDescription'
|
|
||||||
defaultMessage='An email was sent with links to any teams to which you are a member.'
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.findTitle'
|
|
||||||
defaultMessage='Find Your Team'
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.getLinks'
|
|
||||||
defaultMessage='Get an email with links to any teams to which you are a member.'
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label className='control-label'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.email'
|
|
||||||
defaultMessage='Email'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className={emailErrorClass}>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
ref='email'
|
|
||||||
className='form-control'
|
|
||||||
placeholder={this.props.intl.formatMessage(holders.placeholder)}
|
|
||||||
maxLength='128'
|
|
||||||
spellCheck='false'
|
|
||||||
/>
|
|
||||||
{emailError}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className='btn btn-md btn-primary'
|
|
||||||
type='submit'
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='find_team.send'
|
|
||||||
defaultMessage='Send'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FindTeam.propTypes = {
|
|
||||||
intl: intlShape.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(FindTeam);
|
|
||||||
@@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx';
|
|||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
import ModalStore from '../stores/modal_store.jsx';
|
import ModalStore from '../stores/modal_store.jsx';
|
||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../stores/user_store.jsx';
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
@@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component {
|
|||||||
showGetTeamInviteLinkModal() {
|
showGetTeamInviteLinkModal() {
|
||||||
this.handleHide(false);
|
this.handleHide(false);
|
||||||
|
|
||||||
EventHelpers.showGetTeamInviteLinkModal();
|
GlobalActions.showGetTeamInviteLinkModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
224
web/react/components/logged_in.jsx
Обычный файл
224
web/react/components/logged_in.jsx
Обычный файл
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
import SocketStore from '../stores/socket_store.jsx';
|
||||||
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
import PreferenceStore from '../stores/preference_store.jsx';
|
||||||
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
import Constants from '../utils/constants.jsx';
|
||||||
|
import ErrorBar from '../components/error_bar.jsx';
|
||||||
|
|
||||||
|
import {browserHistory} from 'react-router';
|
||||||
|
|
||||||
|
import SidebarRight from '../components/sidebar_right.jsx';
|
||||||
|
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
import GetPostLinkModal from '../components/get_post_link_modal.jsx';
|
||||||
|
import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
|
||||||
|
import EditPostModal from '../components/edit_post_modal.jsx';
|
||||||
|
import DeletePostModal from '../components/delete_post_modal.jsx';
|
||||||
|
import MoreChannelsModal from '../components/more_channels.jsx';
|
||||||
|
import TeamSettingsModal from '../components/team_settings_modal.jsx';
|
||||||
|
import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx';
|
||||||
|
import RegisterAppModal from '../components/register_app_modal.jsx';
|
||||||
|
import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx';
|
||||||
|
import InviteMemberModal from '../components/invite_member_modal.jsx';
|
||||||
|
import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
|
||||||
|
|
||||||
|
const CLIENT_STATUS_INTERVAL = 30000;
|
||||||
|
const BACKSPACE_CHAR = 8;
|
||||||
|
|
||||||
|
export default class LoggedIn extends React.Component {
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
|
||||||
|
this.onUserChanged = this.onUserChanged.bind(this);
|
||||||
|
}
|
||||||
|
onUserChanged() {
|
||||||
|
// Grab the current user
|
||||||
|
const user = UserStore.getCurrentUser();
|
||||||
|
|
||||||
|
// Update segment indentify
|
||||||
|
if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') {
|
||||||
|
global.window.analytics.identify(user.id, {
|
||||||
|
name: user.nickname,
|
||||||
|
email: user.email,
|
||||||
|
createdAt: user.create_at,
|
||||||
|
username: user.username,
|
||||||
|
team_id: user.team_id,
|
||||||
|
id: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update CSS classes to match user theme
|
||||||
|
if (user) {
|
||||||
|
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
|
||||||
|
Utils.applyTheme(user.theme_props);
|
||||||
|
} else {
|
||||||
|
Utils.applyTheme(Constants.THEMES.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSocketChange(msg) {
|
||||||
|
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
|
||||||
|
UserStore.setStatus(msg.user_id, 'online');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillMount() {
|
||||||
|
// Emit view action
|
||||||
|
GlobalActions.viewLoggedIn();
|
||||||
|
|
||||||
|
// Listen for user
|
||||||
|
UserStore.addChangeListener(this.onUserChanged);
|
||||||
|
|
||||||
|
// Add listner for socker store
|
||||||
|
SocketStore.addChangeListener(this.onSocketChange);
|
||||||
|
|
||||||
|
// Get all statuses regularally. (Soon to be switched to websocket)
|
||||||
|
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
|
||||||
|
|
||||||
|
// Force logout of all tabs if one tab is logged out
|
||||||
|
$(window).bind('storage', (e) => {
|
||||||
|
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
|
||||||
|
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
|
||||||
|
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
|
||||||
|
if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('detected logout from a different tab'); //eslint-disable-line no-console
|
||||||
|
browserHistory.push('/' + this.props.params.team);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
|
||||||
|
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
|
||||||
|
if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('detected login from a different tab'); //eslint-disable-line no-console
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Because current CSS requires the root tag to have specific stuff
|
||||||
|
$('#root').attr('class', 'channel-view');
|
||||||
|
|
||||||
|
// ???
|
||||||
|
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
|
||||||
|
if (ev.type === 'mouseenter') {
|
||||||
|
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
|
||||||
|
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
|
||||||
|
} else {
|
||||||
|
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
|
||||||
|
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
|
||||||
|
if (ev.type === 'mouseenter') {
|
||||||
|
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
|
||||||
|
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
|
||||||
|
} else {
|
||||||
|
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
|
||||||
|
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
|
||||||
|
if (ev.type === 'mouseenter') {
|
||||||
|
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
|
||||||
|
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
|
||||||
|
} else {
|
||||||
|
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
|
||||||
|
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Device tracking setup
|
||||||
|
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
|
||||||
|
if (iOS) {
|
||||||
|
$('body').addClass('ios');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up tracking for whether the window is active
|
||||||
|
window.isActive = true;
|
||||||
|
$(window).on('focus', () => {
|
||||||
|
AsyncClient.updateLastViewedAt();
|
||||||
|
ChannelStore.resetCounts(ChannelStore.getCurrentId());
|
||||||
|
ChannelStore.emitChange();
|
||||||
|
window.isActive = true;
|
||||||
|
});
|
||||||
|
$(window).on('blur', () => {
|
||||||
|
window.isActive = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
|
||||||
|
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
|
||||||
|
Utils.applyFont(selectedFont);
|
||||||
|
|
||||||
|
// Pervent backspace from navigating back a page
|
||||||
|
$(window).on('keydown.preventBackspace', (e) => {
|
||||||
|
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
$('#root').attr('class', '');
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
|
||||||
|
$(window).off('focus');
|
||||||
|
$(window).off('blur');
|
||||||
|
|
||||||
|
SocketStore.removeChangeListener(this.onSocketChange);
|
||||||
|
UserStore.removeChangeListener(this.onUserChanged);
|
||||||
|
|
||||||
|
$('body').off('click.userpopover');
|
||||||
|
$('body').off('mouseenter mouseleave', '.post');
|
||||||
|
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
|
||||||
|
|
||||||
|
$('.modal').off('show.bs.modal');
|
||||||
|
|
||||||
|
$(window).off('keydown.preventBackspace');
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='channel-view'>
|
||||||
|
<ErrorBar/>
|
||||||
|
<div className='container-fluid'>
|
||||||
|
<SidebarRight/>
|
||||||
|
<SidebarRightMenu/>
|
||||||
|
{this.props.sidebar}
|
||||||
|
{this.props.center}
|
||||||
|
|
||||||
|
<GetPostLinkModal/>
|
||||||
|
<GetTeamInviteLinkModal/>
|
||||||
|
<InviteMemberModal/>
|
||||||
|
<ImportThemeModal/>
|
||||||
|
<TeamSettingsModal/>
|
||||||
|
<MoreChannelsModal/>
|
||||||
|
<EditPostModal/>
|
||||||
|
<DeletePostModal/>
|
||||||
|
<RemovedFromChannelModal/>
|
||||||
|
<RegisterAppModal/>
|
||||||
|
<SelectTeamModal/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggedIn.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
LoggedIn.propTypes = {
|
||||||
|
children: React.PropTypes.object,
|
||||||
|
sidebar: React.PropTypes.object,
|
||||||
|
center: React.PropTypes.object,
|
||||||
|
params: React.PropTypes.object
|
||||||
|
};
|
||||||
@@ -6,82 +6,120 @@ import LoginUsername from './login_username.jsx';
|
|||||||
import LoginLdap from './login_ldap.jsx';
|
import LoginLdap from './login_ldap.jsx';
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
import * as Client from '../utils/client.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
import TeamStore from '../stores/team_store.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
import {browserHistory} from 'react-router';
|
||||||
|
|
||||||
export default class Login extends React.Component {
|
export default class Login extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {};
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
|
this.onTeamChange = this.onTeamChange.bind(this);
|
||||||
|
|
||||||
|
this.state = this.getStateFromStores();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
TeamStore.addChangeListener(this.onTeamChange);
|
||||||
|
Client.getMeLoggedIn((data) => {
|
||||||
|
if (data && data.logged_in !== 'false') {
|
||||||
|
browserHistory.push('/' + this.props.params.team + '/channels/town-square');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
TeamStore.removeChangeListener(this.onTeamChange);
|
||||||
|
}
|
||||||
|
getStateFromStores() {
|
||||||
|
return {
|
||||||
|
currentTeam: TeamStore.getByName(this.props.params.team)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
onTeamChange() {
|
||||||
|
this.setState(this.getStateFromStores());
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const teamDisplayName = this.props.teamDisplayName;
|
const currentTeam = this.state.currentTeam;
|
||||||
const teamName = this.props.teamName;
|
if (currentTeam == null) {
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamDisplayName = currentTeam.display_name;
|
||||||
|
const teamName = currentTeam.name;
|
||||||
|
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
|
||||||
|
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
|
||||||
|
|
||||||
let loginMessage = [];
|
let loginMessage = [];
|
||||||
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
|
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
|
||||||
loginMessage.push(
|
loginMessage.push(
|
||||||
<a
|
<a
|
||||||
className='btn btn-custom-login gitlab'
|
className='btn btn-custom-login gitlab'
|
||||||
key='gitlab'
|
key='gitlab'
|
||||||
href={'/' + teamName + '/login/gitlab'}
|
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
|
||||||
>
|
>
|
||||||
<span className='icon'/>
|
<span className='icon'/>
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='login.gitlab'
|
id='login.gitlab'
|
||||||
defaultMessage='with GitLab'
|
defaultMessage='with GitLab'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
||||||
loginMessage.push(
|
loginMessage.push(
|
||||||
<a
|
<a
|
||||||
className='btn btn-custom-login google'
|
className='btn btn-custom-login google'
|
||||||
key='google'
|
key='google'
|
||||||
href={'/' + teamName + '/login/google'}
|
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
|
||||||
>
|
>
|
||||||
<span className='icon'/>
|
<span className='icon'/>
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='login.google'
|
id='login.google'
|
||||||
defaultMessage='with Google Apps'
|
defaultMessage='with Google Apps'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraParam = Utils.getUrlParameter('extra');
|
const extraParam = Utils.getUrlParameter('extra');
|
||||||
let extraBox = '';
|
let extraBox = '';
|
||||||
if (extraParam) {
|
if (extraParam) {
|
||||||
let msg;
|
|
||||||
if (extraParam === Constants.SIGNIN_CHANGE) {
|
if (extraParam === Constants.SIGNIN_CHANGE) {
|
||||||
msg = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='login.changed'
|
|
||||||
defaultMessage=' Sign-in method changed successfully'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
|
|
||||||
msg = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='login.verified'
|
|
||||||
defaultMessage=' Email Verified'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg != null) {
|
|
||||||
extraBox = (
|
extraBox = (
|
||||||
<div className='alert alert-success'>
|
<div className='alert alert-success'>
|
||||||
<i className='fa fa-check'/>
|
<i className='fa fa-check'/>
|
||||||
{msg}
|
<FormattedMessage
|
||||||
|
id='login.changed'
|
||||||
|
defaultMessage=' Sign-in method changed successfully'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
|
||||||
|
extraBox = (
|
||||||
|
<div className='alert alert-success'>
|
||||||
|
<i className='fa fa-check'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='login.verified'
|
||||||
|
defaultMessage=' Email Verified'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (extraParam === Constants.SESSION_EXPIRED) {
|
||||||
|
extraBox = (
|
||||||
|
<div className='alert alert-warning'>
|
||||||
|
<i className='fa fa-exclamation-triangle'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='login.session_expired'
|
||||||
|
defaultMessage=' Your session has expired. Please login again.'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,7 +129,7 @@ export default class Login extends React.Component {
|
|||||||
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
|
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
|
||||||
emailSignup = (
|
emailSignup = (
|
||||||
<LoginEmail
|
<LoginEmail
|
||||||
teamName={this.props.teamName}
|
teamName={teamName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +163,7 @@ export default class Login extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let userSignUp = null;
|
let userSignUp = null;
|
||||||
if (this.props.inviteId) {
|
if (currentTeam.allow_open_invite) {
|
||||||
userSignUp = (
|
userSignUp = (
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
@@ -134,7 +172,7 @@ export default class Login extends React.Component {
|
|||||||
defaultMessage="Don't have an account? "
|
defaultMessage="Don't have an account? "
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
href={'/signup_user_complete/?id=' + this.props.inviteId}
|
href={'/signup_user_complete/?id=' + currentTeam.invite_id}
|
||||||
className='signup-team-login'
|
className='signup-team-login'
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -168,22 +206,23 @@ export default class Login extends React.Component {
|
|||||||
if (global.window.mm_config.EnableLdap === 'true') {
|
if (global.window.mm_config.EnableLdap === 'true') {
|
||||||
ldapLogin = (
|
ldapLogin = (
|
||||||
<LoginLdap
|
<LoginLdap
|
||||||
teamName={this.props.teamName}
|
teamName={teamName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let findTeams = null;
|
if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
|
||||||
if (!Utils.isMobileApp()) {
|
ldapLogin = (
|
||||||
findTeams = (
|
<div>
|
||||||
<div className='form-group margin--extra form-group--small'>
|
<div className='or__container'>
|
||||||
<span>
|
<FormattedMessage
|
||||||
<a href='/find_team'>
|
id='login.or'
|
||||||
<FormattedMessage
|
defaultMessage='or'
|
||||||
id='login.find'
|
/>
|
||||||
defaultMessage='Find your other teams'
|
</div>
|
||||||
/>
|
<LoginLdap
|
||||||
</a></span>
|
teamName={teamName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,49 +231,72 @@ export default class Login extends React.Component {
|
|||||||
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
|
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
|
||||||
usernameLogin = (
|
usernameLogin = (
|
||||||
<LoginUsername
|
<LoginUsername
|
||||||
teamName={this.props.teamName}
|
teamName={teamName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
|
||||||
|
usernameLogin = (
|
||||||
|
<div>
|
||||||
|
<div className='or__container'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='login.or'
|
||||||
|
defaultMessage='or'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LoginUsername
|
||||||
|
teamName={teamName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='signup-team__container'>
|
<div>
|
||||||
<h5 className='margin--less'>
|
<div className='signup-header'>
|
||||||
<FormattedMessage
|
<a href='/'>
|
||||||
id='login.signTo'
|
<span className='fa fa-chevron-left'/>
|
||||||
defaultMessage='Sign in to:'
|
<FormattedMessage
|
||||||
/>
|
id='web.header.back'
|
||||||
</h5>
|
/>
|
||||||
<h2 className='signup-team__name'>{teamDisplayName}</h2>
|
</a>
|
||||||
<h2 className='signup-team__subdomain'>
|
</div>
|
||||||
<FormattedMessage
|
<div className='col-sm-12'>
|
||||||
id='login.on'
|
<div className='signup-team__container'>
|
||||||
defaultMessage='on {siteName}'
|
<h5 className='margin--less'>
|
||||||
values={{
|
<FormattedMessage
|
||||||
siteName: global.window.mm_config.SiteName
|
id='login.signTo'
|
||||||
}}
|
defaultMessage='Sign in to:'
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h5>
|
||||||
{extraBox}
|
<h2 className='signup-team__name'>{teamDisplayName}</h2>
|
||||||
{loginMessage}
|
<h2 className='signup-team__subdomain'>
|
||||||
{emailSignup}
|
<FormattedMessage
|
||||||
{usernameLogin}
|
id='login.on'
|
||||||
{ldapLogin}
|
defaultMessage='on {siteName}'
|
||||||
{userSignUp}
|
values={{
|
||||||
{findTeams}
|
siteName: global.window.mm_config.SiteName
|
||||||
{forgotPassword}
|
}}
|
||||||
{teamSignUp}
|
/>
|
||||||
|
</h2>
|
||||||
|
{extraBox}
|
||||||
|
{loginMessage}
|
||||||
|
{emailSignup}
|
||||||
|
{usernameLogin}
|
||||||
|
{ldapLogin}
|
||||||
|
{userSignUp}
|
||||||
|
{forgotPassword}
|
||||||
|
{teamSignUp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Login.defaultProps = {
|
Login.defaultProps = {
|
||||||
teamName: '',
|
|
||||||
teamDisplayName: ''
|
|
||||||
};
|
};
|
||||||
Login.propTypes = {
|
Login.propTypes = {
|
||||||
teamName: React.PropTypes.string,
|
params: React.PropTypes.object.isRequired
|
||||||
teamDisplayName: React.PropTypes.string,
|
|
||||||
inviteId: React.PropTypes.string
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
import {browserHistory} from 'react-router';
|
||||||
|
|
||||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
@@ -72,13 +73,7 @@ class LoginEmail extends React.Component {
|
|||||||
Client.loginByEmail(name, email, password,
|
Client.loginByEmail(name, email, password,
|
||||||
() => {
|
() => {
|
||||||
UserStore.setLastEmail(email);
|
UserStore.setLastEmail(email);
|
||||||
|
browserHistory.push('/' + name + '/channels/town-square');
|
||||||
const redirect = Utils.getUrlParameter('redirect');
|
|
||||||
if (redirect) {
|
|
||||||
window.location.href = decodeURIComponent(redirect);
|
|
||||||
} else {
|
|
||||||
window.location.href = '/' + name + '/channels/town-square';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.id === 'api.user.login.not_verified.app_error') {
|
if (err.id === 'api.user.login.not_verified.app_error') {
|
||||||
@@ -167,4 +162,4 @@ LoginEmail.propTypes = {
|
|||||||
teamName: React.PropTypes.string.isRequired
|
teamName: React.PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(LoginEmail);
|
export default injectIntl(LoginEmail);
|
||||||
|
|||||||
@@ -56,9 +56,13 @@ export default class Navbar extends React.Component {
|
|||||||
return {
|
return {
|
||||||
channel: ChannelStore.getCurrent(),
|
channel: ChannelStore.getCurrent(),
|
||||||
member: ChannelStore.getCurrentMember(),
|
member: ChannelStore.getCurrentMember(),
|
||||||
users: ChannelStore.getCurrentExtraInfo().members
|
users: ChannelStore.getCurrentExtraInfo().members,
|
||||||
|
currentUser: UserStore.getCurrentUser()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
stateValid() {
|
||||||
|
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ChannelStore.addChangeListener(this.onChange);
|
ChannelStore.addChangeListener(this.onChange);
|
||||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||||
@@ -201,7 +205,7 @@ export default class Navbar extends React.Component {
|
|||||||
<ToggleModalButton
|
<ToggleModalButton
|
||||||
role='menuitem'
|
role='menuitem'
|
||||||
dialogType={ChannelInviteModal}
|
dialogType={ChannelInviteModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar.addMembers'
|
id='navbar.addMembers'
|
||||||
@@ -286,7 +290,11 @@ export default class Navbar extends React.Component {
|
|||||||
<ToggleModalButton
|
<ToggleModalButton
|
||||||
role='menuitem'
|
role='menuitem'
|
||||||
dialogType={ChannelNotificationsModal}
|
dialogType={ChannelNotificationsModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{
|
||||||
|
channel,
|
||||||
|
channelMember: this.state.member,
|
||||||
|
currentUser: this.state.currentUser
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar.preferences'
|
id='navbar.preferences'
|
||||||
@@ -412,7 +420,11 @@ export default class Navbar extends React.Component {
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
var currentId = UserStore.getCurrentId();
|
if (!this.stateValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentId = this.state.currentUser.id;
|
||||||
var channel = this.state.channel;
|
var channel = this.state.channel;
|
||||||
var channelTitle = this.props.teamDisplayName;
|
var channelTitle = this.props.teamDisplayName;
|
||||||
var popoverContent;
|
var popoverContent;
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
import * as client from '../utils/client.jsx';
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import TeamStore from '../stores/team_store.jsx';
|
|
||||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
|
||||||
|
|
||||||
import AboutBuildModal from './about_build_modal.jsx';
|
import AboutBuildModal from './about_build_modal.jsx';
|
||||||
import TeamMembersModal from './team_members_modal.jsx';
|
import TeamMembersModal from './team_members_modal.jsx';
|
||||||
@@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx';
|
|||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
import {Link} from 'react-router';
|
||||||
function getStateFromStores() {
|
|
||||||
const teams = [];
|
|
||||||
const teamsObject = UserStore.getTeams();
|
|
||||||
for (const teamId in teamsObject) {
|
|
||||||
if (teamsObject.hasOwnProperty(teamId)) {
|
|
||||||
teams.push(teamsObject[teamId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
teams.sort(Utils.sortByDisplayName);
|
|
||||||
return {teams};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NavbarDropdown extends React.Component {
|
export default class NavbarDropdown extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.blockToggle = false;
|
this.blockToggle = false;
|
||||||
|
|
||||||
this.handleLogoutClick = this.handleLogoutClick.bind(this);
|
|
||||||
this.handleAboutModal = this.handleAboutModal.bind(this);
|
this.handleAboutModal = this.handleAboutModal.bind(this);
|
||||||
this.onListenerChange = this.onListenerChange.bind(this);
|
|
||||||
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
|
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
|
||||||
|
|
||||||
const state = getStateFromStores();
|
this.state = {
|
||||||
state.showUserSettingsModal = false;
|
showUserSettingsModal: false,
|
||||||
state.showAboutModal = false;
|
showAboutModal: false
|
||||||
this.state = state;
|
};
|
||||||
}
|
|
||||||
handleLogoutClick(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
client.logout();
|
|
||||||
}
|
}
|
||||||
handleAboutModal() {
|
handleAboutModal() {
|
||||||
this.setState({showAboutModal: true});
|
this.setState({showAboutModal: true});
|
||||||
@@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
this.setState({showAboutModal: false});
|
this.setState({showAboutModal: false});
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
UserStore.addTeamsChangeListener(this.onListenerChange);
|
|
||||||
TeamStore.addChangeListener(this.onListenerChange);
|
|
||||||
|
|
||||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||||
$('.sidebar--left .dropdown-menu').scrollTop(0);
|
$('.sidebar--left .dropdown-menu').scrollTop(0);
|
||||||
this.blockToggle = true;
|
this.blockToggle = true;
|
||||||
@@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
UserStore.removeTeamsChangeListener(this.onListenerChange);
|
|
||||||
TeamStore.removeChangeListener(this.onListenerChange);
|
|
||||||
|
|
||||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
|
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
|
||||||
}
|
}
|
||||||
onListenerChange() {
|
|
||||||
var newState = getStateFromStores();
|
|
||||||
if (!Utils.areObjectsEqual(newState, this.state)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
render() {
|
||||||
var teamLink = '';
|
var teamLink = '';
|
||||||
var inviteLink = '';
|
var inviteLink = '';
|
||||||
var manageLink = '';
|
var manageLink = '';
|
||||||
var sysAdminLink = '';
|
var sysAdminLink = '';
|
||||||
var adminDivider = '';
|
var adminDivider = '';
|
||||||
var currentUser = UserStore.getCurrentUser();
|
var currentUser = this.props.currentUser;
|
||||||
var isAdmin = false;
|
var isAdmin = false;
|
||||||
var isSystemAdmin = false;
|
var isSystemAdmin = false;
|
||||||
var teamSettings = null;
|
var teamSettings = null;
|
||||||
@@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
onClick={EventHelpers.showInviteMemberModal}
|
onClick={GlobalActions.showInviteMemberModal}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar_dropdown.inviteMember'
|
id='navbar_dropdown.inviteMember'
|
||||||
@@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
onClick={EventHelpers.showGetTeamInviteLinkModal}
|
onClick={GlobalActions.showGetTeamInviteLinkModal}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar_dropdown.teamLink'
|
id='navbar_dropdown.teamLink'
|
||||||
@@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
sysAdminLink = (
|
sysAdminLink = (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={'/admin_console?' + Utils.getSessionIndex()}
|
href={'/admin_console'}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar_dropdown.console'
|
id='navbar_dropdown.console'
|
||||||
@@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
|
|
||||||
var teams = [];
|
var teams = [];
|
||||||
|
|
||||||
if (this.state.teams.length > 1) {
|
|
||||||
teams.push(
|
|
||||||
<li
|
|
||||||
className='divider'
|
|
||||||
key='div'
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state.teams.forEach((team) => {
|
|
||||||
if (team.name !== this.props.teamName) {
|
|
||||||
teams.push(
|
|
||||||
<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>
|
|
||||||
<FormattedMessage
|
|
||||||
id='navbar_dropdown.switchTeam'
|
|
||||||
defaultMessage='Switch to {team}'
|
|
||||||
values={{
|
|
||||||
team: team.display_name
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</a></li>);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.window.mm_config.EnableTeamCreation === 'true') {
|
if (global.window.mm_config.EnableTeamCreation === 'true') {
|
||||||
teams.push(
|
teams.push(
|
||||||
<li key='newTeam_li'>
|
<li key='newTeam_li'>
|
||||||
@@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
{inviteLink}
|
{inviteLink}
|
||||||
{teamLink}
|
{teamLink}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<Link to={'/' + this.props.teamName + '/logout'}>
|
||||||
href='#'
|
|
||||||
onClick={this.handleLogoutClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='navbar_dropdown.logout'
|
id='navbar_dropdown.logout'
|
||||||
defaultMessage='Logout'
|
defaultMessage='Logout'
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{adminDivider}
|
{adminDivider}
|
||||||
{teamSettings}
|
{teamSettings}
|
||||||
@@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = {
|
|||||||
NavbarDropdown.propTypes = {
|
NavbarDropdown.propTypes = {
|
||||||
teamType: React.PropTypes.string,
|
teamType: React.PropTypes.string,
|
||||||
teamDisplayName: React.PropTypes.string,
|
teamDisplayName: React.PropTypes.string,
|
||||||
teamName: React.PropTypes.string
|
teamName: React.PropTypes.string,
|
||||||
|
currentUser: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|||||||
20
web/react/components/needs_team.jsx
Обычный файл
20
web/react/components/needs_team.jsx
Обычный файл
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||||
|
|
||||||
|
export default class NeedsTeam extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
GlobalActions.loadTeamRequiredPage();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NeedsTeam.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
NeedsTeam.propTypes = {
|
||||||
|
children: React.PropTypes.object
|
||||||
|
};
|
||||||
70
web/react/components/not_logged_in.jsx
Обычный файл
70
web/react/components/not_logged_in.jsx
Обычный файл
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
export default class NotLoggedIn extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
$('body').attr('class', 'white');
|
||||||
|
$('#root').attr('class', 'container-fluid');
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
$('body').attr('class', '');
|
||||||
|
$('#root').attr('class', '');
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='inner__wrap'>
|
||||||
|
<div className='row content'>
|
||||||
|
{this.props.children}
|
||||||
|
<div className='footer-push'></div>
|
||||||
|
</div>
|
||||||
|
<div className='row footer'>
|
||||||
|
<div className='footer-pane col-xs-12'>
|
||||||
|
<div className='col-xs-12'>
|
||||||
|
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-xs-12'>
|
||||||
|
<span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
|
||||||
|
<a
|
||||||
|
id='help_link'
|
||||||
|
className='pull-right footer-link'
|
||||||
|
href={global.window.mm_config.HelpLink}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='web.footer.help'/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id='terms_link'
|
||||||
|
className='pull-right footer-link'
|
||||||
|
href={global.window.mm_config.TermsOfServiceLink}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='web.footer.terms'/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id='privacy_link'
|
||||||
|
className='pull-right footer-link'
|
||||||
|
href={global.window.mm_config.PrivacyPolicyLink}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='web.footer.privacy'/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id='about_link'
|
||||||
|
className='pull-right footer-link'
|
||||||
|
href={global.window.mm_config.AboutLink}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='web.footer.about'/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotLoggedIn.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
NotLoggedIn.propTypes = {
|
||||||
|
children: React.PropTypes.object
|
||||||
|
};
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import PasswordResetSendLink from './password_reset_send_link.jsx';
|
|
||||||
import PasswordResetForm from './password_reset_form.jsx';
|
|
||||||
|
|
||||||
export default class PasswordReset extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
if (this.props.isReset === 'false') {
|
|
||||||
return (
|
|
||||||
<PasswordResetSendLink
|
|
||||||
teamDisplayName={this.props.teamDisplayName}
|
|
||||||
teamName={this.props.teamName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PasswordResetForm
|
|
||||||
teamDisplayName={this.props.teamDisplayName}
|
|
||||||
teamName={this.props.teamName}
|
|
||||||
hash={this.props.hash}
|
|
||||||
data={this.props.data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PasswordReset.defaultProps = {
|
|
||||||
isReset: '',
|
|
||||||
teamName: '',
|
|
||||||
teamDisplayName: '',
|
|
||||||
hash: '',
|
|
||||||
data: ''
|
|
||||||
};
|
|
||||||
PasswordReset.propTypes = {
|
|
||||||
isReset: React.PropTypes.string,
|
|
||||||
teamName: React.PropTypes.string,
|
|
||||||
teamDisplayName: React.PropTypes.string,
|
|
||||||
hash: React.PropTypes.string,
|
|
||||||
data: React.PropTypes.string
|
|
||||||
};
|
|
||||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user