mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' into PLT-2115
This commit is contained in:
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,5 +1,130 @@
|
||||
# Mattermost Changelog
|
||||
|
||||
## Release v2.1.0
|
||||
|
||||
Expected release date: 2016-03-16
|
||||
|
||||
### Highlights
|
||||
|
||||
- New Android application now available.
|
||||
- New desktop applications for Windows, Mac and Linux now in beta.
|
||||
- Brazilian Portuguese translation added.
|
||||
|
||||
### New Features
|
||||
|
||||
Android Application
|
||||
|
||||
- New [Mattermost Android App](https://github.com/mattermost/android) supporting push notifications available for devices running Android 4.4.2+. Requires Mattermost server 2.1 and higher. See [list of tested devices](https://github.com/mattermost/android/blob/master/DEVICES.md).
|
||||
|
||||
Desktop Application
|
||||
|
||||
- New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release.
|
||||
|
||||
Languages
|
||||
|
||||
- Added Portuguese language translation (Beta) available from **Account Settings** > **Display**.
|
||||
|
||||
### Improvements
|
||||
|
||||
System Console
|
||||
|
||||
- Removed unused “Disable File Storage” option from the System Console as it is no longer relevant.
|
||||
- Added a warning message if a system admin demotes themselves.
|
||||
- System Console statistics now use a client store instead of fetching data and storing it in state.
|
||||
|
||||
Messaging
|
||||
|
||||
- Custom slash commands now support temporary messages that appear only to the user that issued the command.
|
||||
- Username autocomplete list no longer suggests inactive users.
|
||||
|
||||
Mobile
|
||||
|
||||
- Significant responsiveness and speed improvements using [fastclick](https://github.com/ftlabs/fastclick).
|
||||
- Team name and username are now shown in the LHS header.
|
||||
- Added a button to go back to the team URL page from the login page.
|
||||
|
||||
Files and Images
|
||||
|
||||
- Increased the maximum size of image uploads to 24 megapixels.
|
||||
|
||||
User Interface
|
||||
|
||||
- Custom theme color selectors are now organized into categories.
|
||||
- Add Members and Manage Members dialogs can now be filtered using a search bar.
|
||||
- Deactivated members no longer appear in the channel members list.
|
||||
- Keyboard focus is set to the text input box in the RHS if a user clicks the reply icon.
|
||||
- Permalinks are now displayed in a Copy Permalink dialog instead of a popover.
|
||||
- Permalink option is now available from the [...] menu on messages and comments in the RHS.
|
||||
- Reply icon now only appears on-hover for messages that don’t have replies.
|
||||
- Scroll bar now appears in the center channel.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- System console user management tab now shows username and email on different lines.
|
||||
- Yellow text box error no longer appears when the system is connected.
|
||||
- Wildcard search on MySQL databases is now fixed.
|
||||
- Usernames in the center channel no longer appear as “...” on login.
|
||||
- Deleted messages now delete in the RHS and center channel without requiring a page refresh.
|
||||
- Contact us email address in the footer of notification emails now uses the SupportEmail config setting instead of FeedbackEmail.
|
||||
- Email addresses are now required to have at least one letter before and after the @ sign.
|
||||
- Firefox desktop notifications are now fixed for some users experiencing missed notifications.
|
||||
- “User is typing” message containing long usernames no longer causes text wrapping.
|
||||
- Usernames appearing as “...” in the RHS when performing a search is fixed.
|
||||
- Links that end in image extensions but do not actually link to raw images no longer generate a blank image preview.
|
||||
- Channel handle field in the Rename Channel dialog is now visible on themes with dark backgrounds.
|
||||
- Autolinked images no longer persist after the post containing the link is deleted.
|
||||
- Code theme selector on IE11 now only shows one dropdown arrow and clicking directly on the arrow opens the dropdown.
|
||||
- Save/Cancel buttons for language selection in Account Settings are now formatted the same as other settings.
|
||||
- Inconsistent field spacing in the Channel Info dialog is fixed.
|
||||
- Recent mentions icon no longer jumps to the left of the search bar when the RHS is opened.
|
||||
- Custom slash command hints now show up in the autocomplete list.
|
||||
- GIF links inside code blocks no longer auto-post the GIFs.
|
||||
- Changing usernames no longer adds the old username to “words that trigger mentions”.
|
||||
- Notification email footer is now translated based on the sender’s language setting.
|
||||
- Slash command `/me` now posts as the user instead of a webhook message.
|
||||
- Logout slash command now forces logout.
|
||||
- Public links to file attachments on deleted posts no longer work.
|
||||
- Error message is now shown in IE11 when uploading more than 5 files or a file over 50 MB.
|
||||
|
||||
|
||||
### Compatibility
|
||||
Changes from v2.0 to v2.1:
|
||||
|
||||
**Android**
|
||||
Mattermost Android Application is for use with Mattermost platform v2.1 and higher.
|
||||
|
||||
#### Known Issues
|
||||
|
||||
- File name tooltip stays open after clicking to download.
|
||||
- Unable to paste images into the text box on Firefox, Safari, and IE11.
|
||||
- Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh.
|
||||
- First load of an empty channel does not display the introduction message.
|
||||
- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks.
|
||||
- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator.
|
||||
- Hashtags containing a dash incorrectly highlight in the search results.
|
||||
- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected.
|
||||
- Incorrect formatting when a new line is added directly after a list.
|
||||
- Timestamps are displayed in 12-hour format when set to 24-hour format.
|
||||
- Syntax highlighting code block is missing the label for Latex documents.
|
||||
- Posts from webhooks do not fire notifications to the user who created the webhook.
|
||||
- Theme color vector is not updated after making custom changes to a default theme.
|
||||
- Search term highlighting doesn't update on IE11 when search terms change but return the same posts.
|
||||
- Team creation via SSO fails when email domain is restricted.
|
||||
|
||||
#### Contributors
|
||||
|
||||
Many thanks to all our external contributors. In no particular order:
|
||||
|
||||
- [rodrigocorsi2](https://github.com/rodrigocorsi2)
|
||||
- [enahum](https://github.com/enahum)
|
||||
- [khoa-le](https://github.com/khoa-le)
|
||||
- [alanmoo](https://github.com/alanmoo)
|
||||
- [daizenberg](https://github.com/daizenberg)
|
||||
- [GuillaumeAmat](https://github.com/GuillaumeAmat)
|
||||
- [kernicPanel](https://github.com/kernicPanel)
|
||||
- [timlyo](https://github.com/timlyo)
|
||||
- [ttyniwa](https://github.com/ttyniwa)
|
||||
|
||||
## Release v2.0.0
|
||||
|
||||
Expected Release date: 2016-02-16
|
||||
|
||||
25
Makefile
25
Makefile
@@ -127,10 +127,9 @@ package:
|
||||
cp -RL web/static/help $(DIST_PATH)/web/static
|
||||
cp -RL web/static/images $(DIST_PATH)/web/static
|
||||
cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
|
||||
cp -RL web/templates $(DIST_PATH)/web
|
||||
cp -RL templates $(DIST_PATH)
|
||||
|
||||
mkdir -p $(DIST_PATH)/api
|
||||
cp -RL api/templates $(DIST_PATH)/api
|
||||
cp -RL i18n $(DIST_PATH)
|
||||
|
||||
cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
|
||||
@@ -140,17 +139,17 @@ package:
|
||||
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
|
||||
mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
|
||||
|
||||
sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
rm $(DIST_PATH)/web/templates/*.bak
|
||||
sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
|
||||
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
|
||||
rm $(DIST_PATH)/templates/*.bak
|
||||
|
||||
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
|
||||
|
||||
|
||||
43
api/api.go
43
api/api.go
@@ -4,47 +4,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
_ "github.com/cloudfoundry/jibber_jabber"
|
||||
_ "github.com/nicksnyder/go-i18n/i18n"
|
||||
)
|
||||
|
||||
var ServerTemplates *template.Template
|
||||
|
||||
type ServerTemplatePage Page
|
||||
|
||||
func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage {
|
||||
return &ServerTemplatePage{
|
||||
TemplateName: templateName,
|
||||
Props: make(map[string]string),
|
||||
Extra: make(map[string]string),
|
||||
Html: make(map[string]template.HTML),
|
||||
ClientCfg: utils.ClientCfg,
|
||||
Locale: locale,
|
||||
}
|
||||
}
|
||||
|
||||
func (me *ServerTemplatePage) Render() string {
|
||||
var text bytes.Buffer
|
||||
|
||||
T := utils.GetUserTranslations(me.Locale)
|
||||
me.Props["Footer"] = T("api.templates.email_footer")
|
||||
me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
|
||||
map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]}))
|
||||
|
||||
if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil {
|
||||
l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err)
|
||||
}
|
||||
|
||||
return text.String()
|
||||
}
|
||||
|
||||
func InitApi() {
|
||||
r := Srv.Router.PathPrefix("/api/v1").Subrouter()
|
||||
InitUser(r)
|
||||
@@ -60,12 +28,7 @@ func InitApi() {
|
||||
InitPreference(r)
|
||||
InitLicense(r)
|
||||
|
||||
templatesDir := utils.FindDir("api/templates")
|
||||
l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir)
|
||||
var err error
|
||||
if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
|
||||
l4g.Error(utils.T("api.api.init.parsing_templates.error"), err)
|
||||
}
|
||||
utils.InitHTML()
|
||||
}
|
||||
|
||||
func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
137
api/context.go
137
api/context.go
@@ -5,11 +5,9 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
@@ -31,33 +29,16 @@ var allowedMethods []string = []string{
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Session model.Session
|
||||
RequestId string
|
||||
IpAddress string
|
||||
Path string
|
||||
Err *model.AppError
|
||||
teamURLValid bool
|
||||
teamURL string
|
||||
siteURL string
|
||||
SessionTokenIndex int64
|
||||
T goi18n.TranslateFunc
|
||||
Locale string
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
TemplateName string
|
||||
Props map[string]string
|
||||
Extra map[string]string
|
||||
Html map[string]template.HTML
|
||||
ClientCfg map[string]string
|
||||
ClientLicense map[string]string
|
||||
User *model.User
|
||||
Team *model.Team
|
||||
Channel *model.Channel
|
||||
Preferences *model.Preferences
|
||||
PostID string
|
||||
SessionTokenIndex int64
|
||||
Locale string
|
||||
Session model.Session
|
||||
RequestId string
|
||||
IpAddress string
|
||||
Path string
|
||||
Err *model.AppError
|
||||
teamURLValid bool
|
||||
teamURL string
|
||||
siteURL string
|
||||
T goi18n.TranslateFunc
|
||||
Locale string
|
||||
}
|
||||
|
||||
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
@@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
tokens := GetMultiSessionCookieTokens(r)
|
||||
if len(tokens) > 0 {
|
||||
// If there is only 1 token in the cookie then just use it like normal
|
||||
if len(tokens) == 1 {
|
||||
token = tokens[0]
|
||||
} else {
|
||||
// If it is a multi-session token then find the correct session
|
||||
sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX)
|
||||
sessionTokenIndex := int64(-1)
|
||||
if len(sessionTokenIndexStr) > 0 {
|
||||
if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
|
||||
sessionTokenIndex = index
|
||||
}
|
||||
} else {
|
||||
sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX)
|
||||
if len(sessionTokenIndexStr) > 0 {
|
||||
if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
|
||||
sessionTokenIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) {
|
||||
token = tokens[sessionTokenIndex]
|
||||
c.SessionTokenIndex = sessionTokenIndex
|
||||
} else {
|
||||
c.SessionTokenIndex = -1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.SessionTokenIndex = -1
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if session == nil || session.IsExpired() {
|
||||
c.RemoveSessionCookie(w, r)
|
||||
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
if h.requireUser || h.requireSystemAdmin {
|
||||
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
}
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
@@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool {
|
||||
}
|
||||
|
||||
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// multiToken := ""
|
||||
// if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
// multiToken = oldMultiCookie.Value
|
||||
// }
|
||||
|
||||
// multiCookie := &http.Cookie{
|
||||
// Name: model.SESSION_COOKIE_TOKEN,
|
||||
// Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)),
|
||||
// Path: "/",
|
||||
// MaxAge: model.SESSION_TIME_WEB_IN_SECS,
|
||||
// HttpOnly: true,
|
||||
// }
|
||||
|
||||
//http.SetCookie(w, multiCookie)
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: model.SESSION_COOKIE_TOKEN,
|
||||
Value: "",
|
||||
@@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool {
|
||||
}
|
||||
|
||||
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
|
||||
props := make(map[string]string)
|
||||
props["Message"] = err.Message
|
||||
props["Details"] = err.DetailedError
|
||||
T, locale := utils.GetTranslationsAndLocale(w, r)
|
||||
page := utils.NewHTMLTemplate("error", locale)
|
||||
page.Props["Message"] = err.Message
|
||||
page.Props["Details"] = err.DetailedError
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) > 1 {
|
||||
props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
|
||||
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
|
||||
} else {
|
||||
props["SiteURL"] = GetProtocol(r) + "://" + r.Host
|
||||
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host
|
||||
}
|
||||
|
||||
T, _ := utils.GetTranslationsAndLocale(w, r)
|
||||
props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
||||
props["Link"] = T("api.templates.error.link")
|
||||
page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
||||
page.Props["Link"] = T("api.templates.error.link")
|
||||
|
||||
w.WriteHeader(err.StatusCode)
|
||||
ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg})
|
||||
if rErr := page.RenderToWriter(w); rErr != nil {
|
||||
l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -588,29 +528,6 @@ func GetSession(token string) *model.Session {
|
||||
return session
|
||||
}
|
||||
|
||||
func GetMultiSessionCookieTokens(r *http.Request) []string {
|
||||
if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
multiToken := multiCookie.Value
|
||||
|
||||
if len(multiToken) > 0 {
|
||||
return strings.Split(multiToken, " ")
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) {
|
||||
for index, token := range GetMultiSessionCookieTokens(r) {
|
||||
s := GetSession(token)
|
||||
if s != nil && !s.IsExpired() && s.TeamId == teamId {
|
||||
return int64(index), s
|
||||
}
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func AddSessionToCache(session *model.Session) {
|
||||
sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
|
||||
}
|
||||
|
||||
35
api/file.go
35
api/file.go
@@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveFile(oldPath, newPath string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
fileData := make(chan []byte)
|
||||
getFileAndForget(oldPath, fileData)
|
||||
fileBytes := <-fileData
|
||||
|
||||
if fileBytes == nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "")
|
||||
}
|
||||
|
||||
var auth aws.Auth
|
||||
auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
|
||||
s := s3.New(auth, awsRegion())
|
||||
bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
|
||||
|
||||
if err := bucket.Del(oldPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := writeFile(fileBytes, newPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
|
||||
}
|
||||
} else {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFileLocally(f []byte, path string) *model.AppError {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
|
||||
return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error())
|
||||
|
||||
@@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) {
|
||||
sr := r.PathPrefix("/license").Subrouter()
|
||||
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
|
||||
sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST")
|
||||
sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET")
|
||||
}
|
||||
|
||||
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
rdata["status"] = "ok"
|
||||
w.Write([]byte(model.MapToJson(rdata)))
|
||||
}
|
||||
|
||||
func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
config := utils.ClientLicense
|
||||
|
||||
var etag string
|
||||
if config["IsLicensed"] == "false" {
|
||||
etag = model.Etag(config["IsLicensed"])
|
||||
} else {
|
||||
etag = model.Etag(config["IsLicensed"], config["IssuedAt"])
|
||||
}
|
||||
|
||||
if HandleEtag(etag, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
||||
|
||||
w.Write([]byte(model.MapToJson(config)))
|
||||
}
|
||||
|
||||
22
api/license_test.go
Normal file
22
api/license_test.go
Normal file
@@ -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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func InitOAuth(r *mux.Router) {
|
||||
@@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) {
|
||||
|
||||
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
|
||||
sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
|
||||
sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
|
||||
sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET")
|
||||
sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET")
|
||||
sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
|
||||
sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
|
||||
|
||||
// Also handle this a the old routes remove soon apiv2?
|
||||
mr := Srv.Router
|
||||
mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
|
||||
mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
|
||||
mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
|
||||
}
|
||||
|
||||
func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData {
|
||||
return result.Data.(*model.AuthData)
|
||||
}
|
||||
}
|
||||
|
||||
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
service := params["service"]
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
|
||||
|
||||
if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
action := props["action"]
|
||||
switch action {
|
||||
case model.OAUTH_ACTION_SIGNUP:
|
||||
CreateOAuthUser(c, w, r, service, body, team)
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
case model.OAUTH_ACTION_LOGIN:
|
||||
LoginByOAuth(c, w, r, service, body, team)
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
case model.OAUTH_ACTION_EMAIL_TO_SSO:
|
||||
CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
case model.OAUTH_ACTION_SSO_TO_EMAIL:
|
||||
LoginByOAuth(c, w, r, service, body, team)
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
default:
|
||||
LoginByOAuth(c, w, r, service, body, team)
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
responseType := r.URL.Query().Get("response_type")
|
||||
clientId := r.URL.Query().Get("client_id")
|
||||
redirect := r.URL.Query().Get("redirect_uri")
|
||||
scope := r.URL.Query().Get("scope")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
var app *model.OAuthApp
|
||||
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
page := utils.NewHTMLTemplate("authorize", c.Locale)
|
||||
page.Props["Title"] = c.T("web.authorize_oauth.title")
|
||||
page.Props["TeamName"] = team.Name
|
||||
page.Props["AppName"] = app.Name
|
||||
page.Props["ResponseType"] = responseType
|
||||
page.Props["ClientId"] = clientId
|
||||
page.Props["RedirectUri"] = redirect
|
||||
page.Props["Scope"] = scope
|
||||
page.Props["State"] = state
|
||||
if err := page.RenderToWriter(w); err != nil {
|
||||
c.SetUnknownError(page.TemplateName, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
c.LogAudit("attempt")
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
grantType := r.FormValue("grant_type")
|
||||
if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
clientId := r.FormValue("client_id")
|
||||
if len(clientId) != 26 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
secret := r.FormValue("client_secret")
|
||||
if len(secret) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
code := r.FormValue("code")
|
||||
if len(code) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
redirectUri := r.FormValue("redirect_uri")
|
||||
|
||||
achan := Srv.Store.OAuth().GetApp(clientId)
|
||||
tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code)
|
||||
|
||||
authData := GetAuthData(code)
|
||||
|
||||
if authData == nil {
|
||||
c.LogAudit("fail - invalid auth code")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
uchan := Srv.Store.User().Get(authData.UserId)
|
||||
|
||||
if authData.IsExpired() {
|
||||
c.LogAudit("fail - auth code expired")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if authData.RedirectUri != redirectUri {
|
||||
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
|
||||
c.LogAudit("fail - auth code is invalid")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
var app *model.OAuthApp
|
||||
if result := <-achan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
}
|
||||
|
||||
if !model.ComparePassword(app.ClientSecret, secret) {
|
||||
c.LogAudit("fail - invalid client credentials")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
callback := redirectUri
|
||||
if len(callback) == 0 {
|
||||
callback = app.CallbackUrls[0]
|
||||
}
|
||||
|
||||
if result := <-tchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "")
|
||||
return
|
||||
} else if result.Data != nil {
|
||||
c.LogAudit("fail - auth code has been used previously")
|
||||
accessData := result.Data.(*model.AccessData)
|
||||
|
||||
// Revoke access token, related auth code, and session from DB as well as from cache
|
||||
if err := RevokeAccessToken(accessData.Token); err != nil {
|
||||
l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message)
|
||||
}
|
||||
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if result := <-uchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
|
||||
|
||||
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
session = result.Data.(*model.Session)
|
||||
AddSessionToCache(session)
|
||||
}
|
||||
|
||||
accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
|
||||
|
||||
if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
|
||||
l4g.Error(result.Err)
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
|
||||
w.Write([]byte(accessRsp.ToJson()))
|
||||
}
|
||||
|
||||
func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
service := params["service"]
|
||||
loginHint := r.URL.Query().Get("login_hint")
|
||||
teamName := r.URL.Query().Get("team")
|
||||
|
||||
if len(teamName) == 0 {
|
||||
c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure team exists
|
||||
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
}
|
||||
|
||||
stateProps := map[string]string{}
|
||||
stateProps["action"] = model.OAUTH_ACTION_LOGIN
|
||||
|
||||
if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
http.Redirect(w, r, authUrl, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
service := params["service"]
|
||||
teamName := r.URL.Query().Get("team")
|
||||
|
||||
if !utils.Cfg.TeamSettings.EnableUserCreation {
|
||||
c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if len(teamName) == 0 {
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
if IsVerifyHashRequired(nil, team, hash) {
|
||||
data := r.URL.Query().Get("d")
|
||||
props := model.MapFromJson(strings.NewReader(data))
|
||||
|
||||
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
t, err := strconv.ParseInt(props["time"], 10, 64)
|
||||
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if team.Id != props["id"] {
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stateProps := map[string]string{}
|
||||
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
|
||||
|
||||
if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
http.Redirect(w, r, authUrl, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
|
||||
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale}
|
||||
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale}
|
||||
|
||||
if text, ok := respProps["text"]; ok {
|
||||
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
|
||||
@@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
|
||||
year := fmt.Sprintf("%d", tm.Year())
|
||||
zone, _ := tm.Zone()
|
||||
|
||||
subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale)
|
||||
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
|
||||
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||
"Month": month[:3], "Day": day, "Year": year})
|
||||
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
|
||||
|
||||
bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
|
||||
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
|
||||
@@ -1094,6 +1095,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
message.Add("post", post.ToJson())
|
||||
|
||||
PublishAndForget(message)
|
||||
DeletePostFilesAndForget(c.Session.TeamId, post)
|
||||
|
||||
result := make(map[string]string)
|
||||
result["id"] = postId
|
||||
@@ -1101,6 +1103,23 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func DeletePostFilesAndForget(teamId string, post *model.Post) {
|
||||
go func() {
|
||||
if len(post.Filenames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/"
|
||||
for _, filename := range post.Filenames {
|
||||
splitUrl := strings.Split(filename, "/")
|
||||
oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1]
|
||||
newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1]
|
||||
moveFile(oldPath, newPath)
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
getPostsBeforeOrAfter(c, w, r, true)
|
||||
}
|
||||
|
||||
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_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
|
||||
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
|
||||
sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
|
||||
sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET")
|
||||
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
|
||||
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
|
||||
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
|
||||
sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
|
||||
sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
|
||||
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
|
||||
sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
|
||||
// These should be moved to the global admain console
|
||||
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
|
||||
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
|
||||
@@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject",
|
||||
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("signup_team_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
|
||||
bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button")
|
||||
@@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if !utils.Cfg.EmailSettings.RequireEmailVerification {
|
||||
m["follow_link"] = bodyPage.Props["Link"]
|
||||
m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash))
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Origin", " *")
|
||||
@@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service}
|
||||
data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name}
|
||||
w.Write([]byte(model.MapToJson(data)))
|
||||
|
||||
}
|
||||
@@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool {
|
||||
}
|
||||
|
||||
func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.HasSystemAdminPermissions("getLogs") {
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
@@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
m := make(map[string]*model.Team)
|
||||
for _, v := range teams {
|
||||
m[v.Id] = v
|
||||
if !c.IsSystemAdmin() {
|
||||
m[v.Id].SanitizeForNotLoggedIn()
|
||||
}
|
||||
}
|
||||
|
||||
w.Write([]byte(model.TeamMapToJson(m)))
|
||||
@@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
m := model.MapFromJson(r.Body)
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(m["email"]))
|
||||
|
||||
if email == "" {
|
||||
c.SetInvalidParam("findTeam", "email")
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
teams := result.Data.([]*model.Team)
|
||||
m := make(map[string]*model.Team)
|
||||
for _, v := range teams {
|
||||
v.Sanitize()
|
||||
m[v.Id] = v
|
||||
}
|
||||
|
||||
w.Write([]byte(model.TeamMapToJson(m)))
|
||||
}
|
||||
}
|
||||
|
||||
func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
m := model.MapFromJson(r.Body)
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(m["email"]))
|
||||
|
||||
if email == "" {
|
||||
c.SetInvalidParam("findTeam", "email")
|
||||
return
|
||||
}
|
||||
|
||||
siteURL := c.GetSiteURL()
|
||||
subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject",
|
||||
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("find_teams_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title")
|
||||
bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found")
|
||||
bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found")
|
||||
|
||||
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
} else {
|
||||
teams := result.Data.([]*model.Team)
|
||||
|
||||
// the template expects Props to be a map with team names as the keys and the team url as the value
|
||||
props := make(map[string]string)
|
||||
for _, team := range teams {
|
||||
props[team.Name] = c.GetTeamURLFromTeam(team)
|
||||
}
|
||||
bodyPage.Extra = props
|
||||
|
||||
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
|
||||
l4g.Error(utils.T("api.team.email_teams.sending.error"), err)
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJson(m)))
|
||||
}
|
||||
}
|
||||
|
||||
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
invites := model.InvitesFromJson(r.Body)
|
||||
if len(invites.Invites) == 0 {
|
||||
@@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
|
||||
senderRole = c.T("api.team.invite_members.member")
|
||||
}
|
||||
|
||||
subjectPage := NewServerTemplatePage("invite_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.invite_subject",
|
||||
map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("invite_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
|
||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info",
|
||||
@@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(model.MapToJson(result)))
|
||||
}
|
||||
}
|
||||
|
||||
func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
m := model.MapFromJson(r.Body)
|
||||
inviteId := m["invite_id"]
|
||||
|
||||
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team := result.Data.(*model.Team)
|
||||
if !(team.Type == model.TEAM_OPEN) {
|
||||
c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]string{}
|
||||
result["display_name"] = team.DisplayName
|
||||
result["name"] = team.Name
|
||||
result["id"] = team.Id
|
||||
w.Write([]byte(model.MapToJson(result)))
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
@@ -147,8 +120,22 @@ func TestGetAllTeams(t *testing.T) {
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if _, err := Client.GetAllTeams(); err == nil {
|
||||
t.Fatal("you shouldn't have permissions")
|
||||
enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.EnableTeamListing = true
|
||||
|
||||
if r1, err := Client.GetAllTeams(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
teams := r1.Data.(map[string]*model.Team)
|
||||
if teams[team.Id].Name != team.Name {
|
||||
t.Fatal()
|
||||
}
|
||||
if teams[team.Id].Email != "" {
|
||||
t.Fatal("Non admin users shoudn't get full listings")
|
||||
}
|
||||
}
|
||||
|
||||
c := &Context{}
|
||||
@@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) {
|
||||
if teams[team.Id].Name != team.Name {
|
||||
t.Fatal()
|
||||
}
|
||||
if teams[team.Id].Email != team.Email {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) {
|
||||
Client.ClearOAuthToken()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
XXXXXX investigate and fix failing test
|
||||
|
||||
func TestFindTeamByDomain(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
val := r1.Data.(bool)
|
||||
if !val {
|
||||
t.Fatal("should be a valid domain")
|
||||
}
|
||||
}
|
||||
|
||||
if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
val := r1.Data.(bool)
|
||||
if !val {
|
||||
t.Fatal("should be a valid domain")
|
||||
}
|
||||
}
|
||||
|
||||
if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
val := r1.Data.(bool)
|
||||
if val {
|
||||
t.Fatal("shouldn't be a valid domain")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func TestFindTeamByEmailSend(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if _, err := Client.FindTeamsSendEmail(user.Email); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
}
|
||||
|
||||
if _, err := Client.FindTeamsSendEmail("missing"); err != nil {
|
||||
|
||||
// It should actually succeed at sending the email since it doesn't exist
|
||||
if !strings.Contains(err.DetailedError, "Failed to add to email address") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteMembers(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
|
||||
@@ -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("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
|
||||
sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST")
|
||||
sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
|
||||
sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST")
|
||||
|
||||
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
||||
|
||||
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
|
||||
sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET")
|
||||
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
|
||||
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
|
||||
sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
|
||||
@@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
|
||||
func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
|
||||
go func() {
|
||||
|
||||
subjectPage := NewServerTemplatePage("welcome_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
||||
|
||||
bodyPage := NewServerTemplatePage("welcome_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
||||
bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info")
|
||||
@@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN
|
||||
bodyPage.Props["TeamURL"] = teamURL
|
||||
|
||||
if !verified {
|
||||
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
|
||||
link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
|
||||
bodyPage.Props["VerifyUrl"] = link
|
||||
}
|
||||
|
||||
@@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) {
|
||||
func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
|
||||
go func() {
|
||||
|
||||
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
|
||||
link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
|
||||
|
||||
subjectPage := NewServerTemplatePage("verify_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.verify_subject",
|
||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("verify_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
||||
bodyPage.Props["Info"] = c.T("api.templates.verify_body.info")
|
||||
@@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
|
||||
|
||||
w.Header().Set(model.HEADER_TOKEN, session.Token)
|
||||
|
||||
tokens := GetMultiSessionCookieTokens(r)
|
||||
multiToken := ""
|
||||
seen := make(map[string]string)
|
||||
seen[session.TeamId] = session.TeamId
|
||||
for _, token := range tokens {
|
||||
s := GetSession(token)
|
||||
if s != nil && !s.IsExpired() && seen[s.TeamId] == "" {
|
||||
multiToken += " " + token
|
||||
seen[s.TeamId] = s.TeamId
|
||||
}
|
||||
}
|
||||
|
||||
multiToken = strings.TrimSpace(multiToken + " " + session.Token)
|
||||
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
|
||||
|
||||
multiSessionCookie := &http.Cookie{
|
||||
sessionCookie := &http.Cookie{
|
||||
Name: model.SESSION_COOKIE_TOKEN,
|
||||
Value: multiToken,
|
||||
Value: session.Token,
|
||||
Path: "/",
|
||||
MaxAge: maxAge,
|
||||
Expires: expiresAt,
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
http.SetCookie(w, multiSessionCookie)
|
||||
http.SetCookie(w, sessionCookie)
|
||||
|
||||
c.Session = *session
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
@@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
data := make(map[string]string)
|
||||
data["logged_in"] = "false"
|
||||
data["team_name"] = ""
|
||||
|
||||
if len(c.Session.UserId) != 0 {
|
||||
teamChan := Srv.Store.Team().Get(c.Session.TeamId)
|
||||
var team *model.Team
|
||||
if tr := <-teamChan; tr.Err != nil {
|
||||
c.Err = tr.Err
|
||||
return
|
||||
} else {
|
||||
team = tr.Data.(*model.Team)
|
||||
}
|
||||
data["logged_in"] = "true"
|
||||
data["team_name"] = team.Name
|
||||
}
|
||||
w.Write([]byte(model.MapToJson(data)))
|
||||
}
|
||||
|
||||
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
id := params["id"]
|
||||
@@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
data := model.MapToJson(newProps)
|
||||
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt))
|
||||
|
||||
link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
|
||||
link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
|
||||
|
||||
subjectPage := NewServerTemplatePage("reset_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.reset_subject")
|
||||
|
||||
bodyPage := NewServerTemplatePage("reset_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
|
||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
|
||||
@@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
|
||||
go func() {
|
||||
|
||||
subjectPage := NewServerTemplatePage("password_change_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject",
|
||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("password_change_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
|
||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
|
||||
@@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR
|
||||
func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
|
||||
go func() {
|
||||
|
||||
subjectPage := NewServerTemplatePage("email_change_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject",
|
||||
map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
||||
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
|
||||
|
||||
bodyPage := NewServerTemplatePage("email_change_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
|
||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info",
|
||||
@@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN
|
||||
|
||||
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)
|
||||
|
||||
subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject",
|
||||
map[string]interface{}{"TeamDisplayName": teamDisplayName})
|
||||
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
|
||||
|
||||
bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
|
||||
bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
|
||||
@@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string
|
||||
props["team"] = teamName
|
||||
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
|
||||
|
||||
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
|
||||
redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
|
||||
|
||||
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
|
||||
|
||||
@@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
|
||||
go func() {
|
||||
|
||||
subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale)
|
||||
subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject",
|
||||
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
|
||||
|
||||
bodyPage := NewServerTemplatePage("signin_change_body", c.Locale)
|
||||
bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
|
||||
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info",
|
||||
@@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL,
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
userId := props["uid"]
|
||||
if len(userId) != 26 {
|
||||
c.SetInvalidParam("verifyEmail", "uid")
|
||||
return
|
||||
}
|
||||
|
||||
hashedId := props["hid"]
|
||||
if len(hashedId) == 0 {
|
||||
c.SetInvalidParam("verifyEmail", "hid")
|
||||
return
|
||||
}
|
||||
|
||||
if model.ComparePassword(hashedId, userId) {
|
||||
if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
|
||||
return
|
||||
} else {
|
||||
c.LogAudit("Email Verified")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
}
|
||||
|
||||
func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
teamName := props["team_name"]
|
||||
if len(teamName) == 0 {
|
||||
c.SetInvalidParam("resendVerification", "team_name")
|
||||
return
|
||||
}
|
||||
|
||||
email := props["email"]
|
||||
if len(email) == 0 {
|
||||
c.SetInvalidParam("resendVerification", "email")
|
||||
return
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
|
||||
if user.LastActivityAt > 0 {
|
||||
SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
|
||||
} else {
|
||||
SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) {
|
||||
t.Fatal("should have failed - wrong user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeLoggedIn(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
rteam, _ := Client.CreateTeam(&team)
|
||||
|
||||
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
|
||||
|
||||
Client.AuthToken = "invalid"
|
||||
|
||||
if result, err := Client.GetMeLoggedIn(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
meLoggedIn := result.Data.(map[string]string)
|
||||
|
||||
if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" {
|
||||
t.Fatal("Got: " + val)
|
||||
}
|
||||
}
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, user.Password)
|
||||
|
||||
if result, err := Client.GetMeLoggedIn(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
meLoggedIn := result.Data.(map[string]string)
|
||||
|
||||
if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" {
|
||||
t.Fatal("Got: " + val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
|
||||
WORKDIR /mattermost
|
||||
|
||||
# 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
|
||||
ADD config_docker.json /
|
||||
ADD docker-entry.sh /
|
||||
|
||||
@@ -34,8 +34,8 @@ VOLUME /var/lib/mysql
|
||||
WORKDIR /mattermost
|
||||
|
||||
# Copy over files
|
||||
ADD https://github.com/mattermost/platform/releases/download/v1.4.0/mattermost.tar.gz /
|
||||
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /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-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 docker-entry.sh /
|
||||
|
||||
BIN
docker/2.1/Dockerrun.aws.zip
Normal file
BIN
docker/2.1/Dockerrun.aws.zip
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"AWSEBDockerrunVersion": "1",
|
||||
"Image": {
|
||||
"Name": "mattermost/platform:1.4",
|
||||
"Name": "mattermost/platform:2.1",
|
||||
"Update": "true"
|
||||
},
|
||||
"Ports": [
|
||||
@@ -7,11 +7,14 @@
|
||||
"EnableOAuthServiceProvider": false,
|
||||
"EnableIncomingWebhooks": false,
|
||||
"EnableOutgoingWebhooks": false,
|
||||
"EnableCommands": false,
|
||||
"EnableOnlyAdminIntegrations": true,
|
||||
"EnablePostUsernameOverride": false,
|
||||
"EnablePostIconOverride": false,
|
||||
"EnableTesting": false,
|
||||
"EnableDeveloper": false,
|
||||
"EnableSecurityFixAlert": true,
|
||||
"EnableInsecureOutgoingConnections": false,
|
||||
"SessionLengthWebInDays" : 30,
|
||||
"SessionLengthMobileInDays" : 30,
|
||||
"SessionLengthSSOInDays" : 30,
|
||||
@@ -66,6 +69,8 @@
|
||||
},
|
||||
"EmailSettings": {
|
||||
"EnableSignUpWithEmail": true,
|
||||
"EnableSignInWithEmail": true,
|
||||
"EnableSignInWithUsername": false,
|
||||
"SendEmailNotifications": false,
|
||||
"RequireEmailVerification": false,
|
||||
"FeedbackName": "",
|
||||
48
i18n/en.json
48
i18n/en.json
@@ -456,8 +456,8 @@
|
||||
"translation": "S3 is not supported for local storage export."
|
||||
},
|
||||
{
|
||||
"id": "api.export.write_file.app_error",
|
||||
"translation": "Unable to write to export file"
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
"translation": "File exceeds max image size."
|
||||
},
|
||||
{
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
@@ -507,6 +507,22 @@
|
||||
"id": "api.file.init.debug",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "Couldn't upload profile image"
|
||||
},
|
||||
{
|
||||
"id": "api.user.verify_email.bad_link.app_error",
|
||||
"translation": "Bad verify email link."
|
||||
},
|
||||
{
|
||||
"id": "api.web_conn.new_web_conn.last_activity.error",
|
||||
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
|
||||
@@ -3404,22 +3424,6 @@
|
||||
"id": "web.find_team.title",
|
||||
"translation": "Find Team"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.about",
|
||||
"translation": "About"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.help",
|
||||
"translation": "Help"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.privacy",
|
||||
"translation": "Privacy"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.terms",
|
||||
"translation": "Terms"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.bad_client_id.app_error",
|
||||
"translation": "invalid_request: Bad client_id"
|
||||
@@ -3552,10 +3556,6 @@
|
||||
"id": "web.root.home_title",
|
||||
"translation": "Home"
|
||||
},
|
||||
{
|
||||
"id": "web.root.singup_info",
|
||||
"translation": "All team communication in one place, searchable and accessible anywhere"
|
||||
},
|
||||
{
|
||||
"id": "web.root.singup_title",
|
||||
"translation": "Signup"
|
||||
@@ -3611,5 +3611,9 @@
|
||||
{
|
||||
"id": "web.watcher_fail.error",
|
||||
"translation": "Failed to add directory to watcher %v"
|
||||
},
|
||||
{
|
||||
"id": "api.team.get_invite_info.not_open_team",
|
||||
"translation": "Invite is invalid because this is not an open team."
|
||||
}
|
||||
]
|
||||
|
||||
40
i18n/es.json
40
i18n/es.json
@@ -456,8 +456,8 @@
|
||||
"translation": "S3 no está soportado para exportar al almacenamiento local."
|
||||
},
|
||||
{
|
||||
"id": "api.export.write_file.app_error",
|
||||
"translation": "No se puede escribir al archivo a exportar"
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
"translation": "El archivo excede el tamaño máximo para una imagen."
|
||||
},
|
||||
{
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
@@ -507,6 +507,22 @@
|
||||
"id": "api.file.init.debug",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "Encontrar Equipo"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.about",
|
||||
"translation": "Acerca"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.help",
|
||||
"translation": "Ayuda"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.privacy",
|
||||
"translation": "Privacidad"
|
||||
},
|
||||
{
|
||||
"id": "web.footer.terms",
|
||||
"translation": "Términos"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.bad_client_id.app_error",
|
||||
"translation": "invalid_request: client_id malo"
|
||||
@@ -3531,10 +3531,6 @@
|
||||
"id": "web.root.home_title",
|
||||
"translation": "Inicio"
|
||||
},
|
||||
{
|
||||
"id": "web.root.singup_info",
|
||||
"translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
|
||||
},
|
||||
{
|
||||
"id": "web.root.singup_title",
|
||||
"translation": "Registrar"
|
||||
|
||||
64
i18n/pt.json
64
i18n/pt.json
@@ -456,8 +456,8 @@
|
||||
"translation": "S3 não é suportado para o armazenamento local de exportação."
|
||||
},
|
||||
{
|
||||
"id": "api.export.write_file.app_error",
|
||||
"translation": "Não é possível gravar para exportar arquivo"
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
"translation": "Arquivo excede o máximo de tamanho de imagem."
|
||||
},
|
||||
{
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
@@ -507,6 +507,22 @@
|
||||
"id": "api.file.init.debug",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "Você atualizou seu método de acesso"
|
||||
"translation": "Você atualizou seu método de login"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.signup_team_body.button",
|
||||
@@ -1225,7 +1241,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -1257,7 +1273,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -1333,7 +1349,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -1357,7 +1373,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -1373,11 +1389,11 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
@@ -2407,6 +2423,10 @@
|
||||
"id": "store.sql.pinging.info",
|
||||
"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",
|
||||
"translation": "Falha ao remover o índice %v"
|
||||
@@ -2647,6 +2667,10 @@
|
||||
"id": "store.sql_channel.update_member.app_error",
|
||||
"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",
|
||||
"translation": "Não foi possível deletar o comando"
|
||||
@@ -2675,10 +2699,6 @@
|
||||
"id": "store.sql_command.save.update.app_error",
|
||||
"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",
|
||||
"translation": "Encontramos um erro ao obter a licença"
|
||||
@@ -2899,6 +2919,10 @@
|
||||
"id": "store.sql_preference.update.app_error",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "Encontramos um erro ao procurar as propriedades de sistema"
|
||||
@@ -2967,6 +2987,10 @@
|
||||
"id": "store.sql_system.update.app_error",
|
||||
"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",
|
||||
"translation": "Não foi possível encontrar a equipe existente"
|
||||
@@ -3035,10 +3059,6 @@
|
||||
"id": "store.sql_team.update_display_name.app_error",
|
||||
"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",
|
||||
"translation": "Não foi possível obter o número de usuários únicos"
|
||||
@@ -3587,4 +3607,4 @@
|
||||
"id": "web.watcher_fail.error",
|
||||
"translation": "Falha ao adicionar diretório observador %v"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -16,19 +16,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HEADER_REQUEST_ID = "X-Request-ID"
|
||||
HEADER_VERSION_ID = "X-Version-ID"
|
||||
HEADER_ETAG_SERVER = "ETag"
|
||||
HEADER_ETAG_CLIENT = "If-None-Match"
|
||||
HEADER_FORWARDED = "X-Forwarded-For"
|
||||
HEADER_REAL_IP = "X-Real-IP"
|
||||
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||
HEADER_TOKEN = "token"
|
||||
HEADER_BEARER = "BEARER"
|
||||
HEADER_AUTH = "Authorization"
|
||||
HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex"
|
||||
SESSION_TOKEN_INDEX = "session_token_index"
|
||||
API_URL_SUFFIX = "/api/v1"
|
||||
HEADER_REQUEST_ID = "X-Request-ID"
|
||||
HEADER_VERSION_ID = "X-Version-ID"
|
||||
HEADER_ETAG_SERVER = "ETag"
|
||||
HEADER_ETAG_CLIENT = "If-None-Match"
|
||||
HEADER_FORWARDED = "X-Forwarded-For"
|
||||
HEADER_REAL_IP = "X-Real-IP"
|
||||
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||
HEADER_TOKEN = "token"
|
||||
HEADER_BEARER = "BEARER"
|
||||
HEADER_AUTH = "Authorization"
|
||||
API_URL_SUFFIX = "/api/v1"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
@@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindTeams(email string) (*Result, *AppError) {
|
||||
m := make(map[string]string)
|
||||
m["email"] = email
|
||||
if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
|
||||
m := make(map[string]string)
|
||||
m["email"] = email
|
||||
if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
|
||||
return nil, err
|
||||
@@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
|
||||
if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
|
||||
if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
@@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) {
|
||||
c.AuthToken = sessionToken
|
||||
c.AuthType = HEADER_BEARER
|
||||
}
|
||||
|
||||
func (c *Client) GetClientLicenceConfig() (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetMeLoggedIn() (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SESSION_COOKIE_TOKEN = "MMTOKEN"
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_CACHE_SIZE = 10000
|
||||
SESSION_PROP_PLATFORM = "platform"
|
||||
SESSION_PROP_OS = "os"
|
||||
|
||||
@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
|
||||
o.Email = ""
|
||||
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
|
||||
// release at the front of the list.
|
||||
var versions = []string{
|
||||
"2.1.0",
|
||||
"2.0.0",
|
||||
"1.4.0",
|
||||
"1.3.0",
|
||||
|
||||
1
templates/email_change_subject.html
Normal file
1
templates/email_change_subject.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||
1
templates/email_change_verify_subject.html
Normal file
1
templates/email_change_verify_subject.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||
24
templates/error.html
Normal file
24
templates/error.html
Normal file
@@ -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
Normal file
92
templates/head.html
Normal file
@@ -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
Normal file
1
templates/post_subject.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||
12
templates/root.html
Normal file
12
templates/root.html
Normal file
@@ -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-hashtag*
|
||||
*#markdown* **#markdown** ~~#markdown~~
|
||||
##### #markdown
|
||||
|
||||
97
utils/html.go
Normal file
97
utils/html.go
Normal file
@@ -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 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-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn
|
||||
0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5
|
||||
VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6
|
||||
PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN
|
||||
K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ
|
||||
NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
|
||||
1wIDAQAB
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
|
||||
tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
|
||||
EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
|
||||
HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
|
||||
1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
|
||||
a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
|
||||
hwIDAQAB
|
||||
-----END PUBLIC KEY-----`)
|
||||
|
||||
func LoadLicense(licenseBytes []byte) {
|
||||
|
||||
@@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) {
|
||||
|
||||
emitPostRecievedEvent(post);
|
||||
}
|
||||
|
||||
export function loadTeamRequiredPage() {
|
||||
AsyncClient.getAllTeams();
|
||||
}
|
||||
|
||||
export function newLocalizationSelected(locale) {
|
||||
Client.getTranslations(
|
||||
locale,
|
||||
(data) => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_LOCALE,
|
||||
locale,
|
||||
translations: data
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'getTranslations');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function viewLoggedIn() {
|
||||
AsyncClient.getChannels();
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
AsyncClient.getMyTeam();
|
||||
AsyncClient.getMe();
|
||||
|
||||
// Clear pending posts (shouldn't have pending posts if we are loading)
|
||||
PostStore.clearPendingPosts();
|
||||
}
|
||||
@@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component {
|
||||
|
||||
let title = (
|
||||
<FormattedMessage
|
||||
id='about.teamEdtion'
|
||||
defaultMessage='Team Edition'
|
||||
id='about.teamEditiont0'
|
||||
defaultMessage='Team Edition T0'
|
||||
/>
|
||||
);
|
||||
|
||||
let licensee;
|
||||
if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
|
||||
if (config.BuildEnterpriseReady === 'true') {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='about.enterpriseEdition'
|
||||
defaultMessage='Enterprise Edition'
|
||||
id='about.teamEditiont1'
|
||||
defaultMessage='Team Edition T1'
|
||||
/>
|
||||
);
|
||||
licensee = (
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.licensed'
|
||||
defaultMessage='Licensed by:'
|
||||
/>
|
||||
if (license.IsLicensed === 'true') {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='about.enterpriseEditione1'
|
||||
defaultMessage='Enterprise Edition E1'
|
||||
/>
|
||||
);
|
||||
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 className='col-sm-9'>{license.Company}</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal;
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl';
|
||||
|
||||
export default class ActivityLogModal extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component {
|
||||
id='activity_log.firstTime'
|
||||
defaultMessage='First time active: {date}, {time}'
|
||||
values={{
|
||||
date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
|
||||
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={firstAccessTime}
|
||||
day='2-digit'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>
|
||||
),
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={firstAccessTime}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component {
|
||||
id='activity_log.lastActivity'
|
||||
defaultMessage='Last activity: {date}, {time}'
|
||||
values={{
|
||||
date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
|
||||
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={lastAccessTime}
|
||||
day='2-digit'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>
|
||||
),
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={lastAccessTime}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx';
|
||||
import TeamStore from '../../stores/team_store.jsx';
|
||||
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||
import LoadingScreen from '../loading_screen.jsx';
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
|
||||
import EmailSettingsTab from './email_settings.jsx';
|
||||
import LogSettingsTab from './log_settings.jsx';
|
||||
@@ -50,11 +49,6 @@ export default class AdminController extends React.Component {
|
||||
selected: props.tab || 'system_analytics',
|
||||
selectedTeam: props.teamId || null
|
||||
};
|
||||
|
||||
if (!props.tab) {
|
||||
var tokenIndex = Utils.getUrlParameter('session_token_index');
|
||||
history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -63,6 +57,9 @@ export default class AdminController extends React.Component {
|
||||
|
||||
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
|
||||
AsyncClient.getAllTeams();
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -175,7 +172,7 @@ export default class AdminController extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id='admin_controller'>
|
||||
<div
|
||||
className='sidebar--menu'
|
||||
id='sidebar-menu'
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
import * as Client from '../../utils/client.jsx';
|
||||
import TeamStore from '../../stores/team_store.jsx';
|
||||
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
import {Link} from 'react-router';
|
||||
|
||||
function getStateFromStores() {
|
||||
return {currentTeam: TeamStore.getCurrent()};
|
||||
}
|
||||
@@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component {
|
||||
super(props);
|
||||
this.blockToggle = false;
|
||||
|
||||
this.handleLogoutClick = this.handleLogoutClick.bind(this);
|
||||
|
||||
this.state = getStateFromStores();
|
||||
}
|
||||
|
||||
handleLogoutClick(e) {
|
||||
e.preventDefault();
|
||||
Client.logout();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||
this.blockToggle = true;
|
||||
@@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component {
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleLogoutClick}
|
||||
>
|
||||
<Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
|
||||
<FormattedMessage
|
||||
id='admin.nav.logout'
|
||||
defaultMessage='Logout'
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className='divider'></li>
|
||||
<li>
|
||||
@@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component {
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import AdminSidebarHeader from './admin_sidebar_header.jsx';
|
||||
import SelectTeamModal from './select_team_modal.jsx';
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
@@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component {
|
||||
handleClick(name, teamId, e) {
|
||||
e.preventDefault();
|
||||
this.props.selectTab(name, teamId);
|
||||
var tokenIndex = Utils.getUrlParameter('session_token_index');
|
||||
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
|
||||
}
|
||||
|
||||
isSelected(name, teamId) {
|
||||
@@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component {
|
||||
}
|
||||
|
||||
teamSelectedModal(teamId) {
|
||||
this.props.selectedTeams[teamId] = 'true';
|
||||
this.setState({showSelectModal: false});
|
||||
this.props.addSelectedTeam(teamId);
|
||||
this.forceUpdate();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
|
||||
import UserStore from '../../stores/user_store.jsx';
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
@@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component {
|
||||
profilePicture = (
|
||||
<img
|
||||
className='user__picture'
|
||||
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
|
||||
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ var holders = defineMessages({
|
||||
},
|
||||
baseEx: {
|
||||
id: 'admin.ldap.baseEx',
|
||||
defaultMessage: 'Ex "dc=mydomain,dc=com"'
|
||||
defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
|
||||
},
|
||||
firstnameAttrEx: {
|
||||
id: 'admin.ldap.firstnameAttrEx',
|
||||
@@ -32,7 +32,7 @@ var holders = defineMessages({
|
||||
},
|
||||
emailAttrEx: {
|
||||
id: 'admin.ldap.emailAttrEx',
|
||||
defaultMessage: 'Ex "mail"'
|
||||
defaultMessage: 'Ex "mail" or "userPrincipalName"'
|
||||
},
|
||||
usernameAttrEx: {
|
||||
id: 'admin.ldap.usernameAttrEx',
|
||||
@@ -581,4 +581,4 @@ LdapSettings.propTypes = {
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(LdapSettings);
|
||||
export default injectIntl(LdapSettings);
|
||||
|
||||
@@ -27,6 +27,7 @@ class LicenseSettings extends React.Component {
|
||||
|
||||
this.state = {
|
||||
fileSelected: false,
|
||||
fileName: null,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
@@ -34,7 +35,7 @@ class LicenseSettings extends React.Component {
|
||||
handleChange() {
|
||||
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
|
||||
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]);
|
||||
$('#upload-button').button('reset');
|
||||
this.setState({serverError: null});
|
||||
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||
window.location.reload(true);
|
||||
},
|
||||
(error) => {
|
||||
Utils.clearFileInput(element[0]);
|
||||
$('#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(
|
||||
() => {
|
||||
$('#remove-button').button('reset');
|
||||
this.setState({serverError: null});
|
||||
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||
window.location.reload(true);
|
||||
},
|
||||
(error) => {
|
||||
$('#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 = (
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
className='pull-left'
|
||||
ref='fileInput'
|
||||
type='file'
|
||||
accept='.mattermost-license'
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<div className='file__upload'>
|
||||
<button className='btn btn-default'>
|
||||
<FormattedMessage
|
||||
id='admin.license.choose'
|
||||
defaultMessage='Choose File'
|
||||
/>
|
||||
</button>
|
||||
<input
|
||||
ref='fileInput'
|
||||
type='file'
|
||||
accept='.mattermost-license'
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={btnClass + ' pull-left'}
|
||||
className={btnClass}
|
||||
disabled={!this.state.fileSelected}
|
||||
onClick={this.handleSubmit}
|
||||
id='upload-button'
|
||||
@@ -193,11 +213,12 @@ class LicenseSettings extends React.Component {
|
||||
defaultMessage='Upload'
|
||||
/>
|
||||
</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<div className='help-text no-margin'>
|
||||
{fileName}
|
||||
</div>
|
||||
<br/>
|
||||
{serverError}
|
||||
<p className='help-text'>
|
||||
<p className='help-text no-margin'>
|
||||
<FormattedHTMLMessage
|
||||
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.'
|
||||
|
||||
@@ -366,7 +366,7 @@ export default class UserItem extends React.Component {
|
||||
<td className='row member-div padding--equal'>
|
||||
<img
|
||||
className='post-profile-img pull-left'
|
||||
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
|
||||
src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
|
||||
height='36'
|
||||
width='36'
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
sessionRevoked: {
|
||||
@@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) {
|
||||
}
|
||||
|
||||
const date = new Date(audit.create_at);
|
||||
let auditInfo = {};
|
||||
auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
|
||||
const auditInfo = {};
|
||||
auditInfo.timestamp = (
|
||||
<div>
|
||||
<FormattedDate
|
||||
value={date}
|
||||
day='2-digit'
|
||||
month='short'
|
||||
year='numeric'
|
||||
/>
|
||||
{' - '}
|
||||
<FormattedTime
|
||||
value={date}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
auditInfo.userId = audit.user_id;
|
||||
auditInfo.desc = auditDesc;
|
||||
auditInfo.ip = audit.ip_address;
|
||||
|
||||
@@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.validState = this.validState.bind(this);
|
||||
this.onStoresChange = this.onStoresChange.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
getStateFromStores() {
|
||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||
this.state = {
|
||||
showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS,
|
||||
return {
|
||||
showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
|
||||
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
|
||||
user: UserStore.getCurrentUser(),
|
||||
channel: ChannelStore.getCurrent(),
|
||||
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
|
||||
};
|
||||
}
|
||||
validState() {
|
||||
return this.state.user && this.state.channel && this.state.profiles;
|
||||
}
|
||||
onStoresChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
componentDidMount() {
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
PreferenceStore.addChangeListener(this.onStoresChange);
|
||||
ChannelStore.addChangeListener(this.onStoresChange);
|
||||
UserStore.addChangeListener(this.onStoresChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
}
|
||||
onPreferenceChange() {
|
||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||
this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
|
||||
}
|
||||
onChannelChange() {
|
||||
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
|
||||
}
|
||||
onUserChange() {
|
||||
this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
|
||||
PreferenceStore.removeChangeListener(this.onStoresChange);
|
||||
ChannelStore.removeChangeListener(this.onStoresChange);
|
||||
UserStore.removeChangeListener(this.onStoresChange);
|
||||
}
|
||||
render() {
|
||||
const channel = ChannelStore.getCurrent();
|
||||
if (!this.validState()) {
|
||||
return null;
|
||||
}
|
||||
const channel = this.state.channel;
|
||||
var handleClick = null;
|
||||
let postsContainer;
|
||||
let createPost;
|
||||
|
||||
@@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component {
|
||||
memberChannel: ChannelStore.getCurrentMember(),
|
||||
users: extraInfo.members,
|
||||
userCount: extraInfo.member_count,
|
||||
searchVisible: SearchStore.getSearchResults() !== null
|
||||
searchVisible: SearchStore.getSearchResults() !== null,
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
}
|
||||
validState() {
|
||||
if (!this.state.channel ||
|
||||
!this.state.memberChannel ||
|
||||
!this.state.users ||
|
||||
!this.state.userCount ||
|
||||
!this.state.currentUser) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||
PreferenceStore.addChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||
PreferenceStore.removeChangeListener(this.onListenerChange);
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
const newState = this.getStateFromStores();
|
||||
@@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component {
|
||||
searchMentions(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const user = this.props.user;
|
||||
const user = this.state.currentUser;
|
||||
|
||||
let terms = '';
|
||||
if (user.notify_props && user.notify_props.mention_keys) {
|
||||
@@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component {
|
||||
});
|
||||
}
|
||||
render() {
|
||||
if (this.state.channel === null) {
|
||||
if (!this.validState()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component {
|
||||
</Popover>
|
||||
);
|
||||
let channelTitle = channel.display_name;
|
||||
const currentId = this.props.user.id;
|
||||
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles);
|
||||
const currentId = this.state.currentUser.id;
|
||||
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
|
||||
const isDirect = (this.state.channel.type === 'D');
|
||||
|
||||
if (isDirect) {
|
||||
@@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component {
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel}}
|
||||
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='chanel_header.addMembers'
|
||||
@@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component {
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{channel}}
|
||||
dialogProps={{
|
||||
channel,
|
||||
channelMember: this.state.memberChannel,
|
||||
currentUser: this.state.currentUser
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.notificationPreferences'
|
||||
@@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component {
|
||||
}
|
||||
|
||||
ChannelHeader.propTypes = {
|
||||
user: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import FilteredUserList from './filtered_user_list.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
@@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl';
|
||||
const Modal = ReactBootstrap.Modal;
|
||||
|
||||
export default class ChannelInviteModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleInvite = this.handleInvite.bind(this);
|
||||
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.createInviteButton = this.createInviteButton.bind(this);
|
||||
|
||||
// the state gets populated when the modal is shown
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!this.props.show && !nextProps.show) {
|
||||
@@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const currentMember = ChannelStore.getCurrentMember();
|
||||
if (!currentMember) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const memberIds = extraInfo.members.map((user) => user.id);
|
||||
|
||||
var nonmembers = [];
|
||||
@@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component {
|
||||
|
||||
return {
|
||||
nonmembers,
|
||||
loading: false
|
||||
loading: false,
|
||||
currentUser,
|
||||
currentMember
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component {
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
var newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(this.state, newState)) {
|
||||
@@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component {
|
||||
if (Utils.windowHeight() <= 1200) {
|
||||
maxHeight = Utils.windowHeight() - 300;
|
||||
}
|
||||
|
||||
content = (
|
||||
<FilteredUserList
|
||||
style={{maxHeight}}
|
||||
|
||||
@@ -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 * as Client from '../utils/client.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
@@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.updateSection = this.updateSection.bind(this);
|
||||
|
||||
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
|
||||
@@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
|
||||
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
|
||||
|
||||
const member = ChannelStore.getMember(props.channel.id);
|
||||
this.state = {
|
||||
notifyLevel: member.notify_props.desktop,
|
||||
markUnreadLevel: member.notify_props.mark_unread,
|
||||
channelId: ChannelStore.getCurrentId(),
|
||||
activeSection: ''
|
||||
activeSection: '',
|
||||
notifyLevel: '',
|
||||
unreadLevel: ''
|
||||
};
|
||||
}
|
||||
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) {
|
||||
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() {
|
||||
var channelId = this.state.channelId;
|
||||
var channelId = this.props.channel.id;
|
||||
var notifyLevel = this.state.notifyLevel;
|
||||
|
||||
if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
|
||||
if (this.props.channelMember.notify_props.desktop === notifyLevel) {
|
||||
this.updateSection('');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data.channel_id = channelId;
|
||||
data.user_id = UserStore.getCurrentId();
|
||||
data.user_id = this.props.currentUser.id;
|
||||
data.desktop = notifyLevel;
|
||||
|
||||
//TODO: This should be moved to event_helpers
|
||||
Client.updateNotifyProps(data,
|
||||
() => {
|
||||
// YUCK
|
||||
var member = ChannelStore.getMember(channelId);
|
||||
member.notify_props.desktop = notifyLevel;
|
||||
ChannelStore.setChannelMember(member);
|
||||
@@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
this.setState({notifyLevel});
|
||||
}
|
||||
createNotifyLevelSection(serverError) {
|
||||
var handleUpdateSection;
|
||||
|
||||
const user = UserStore.getCurrentUser();
|
||||
const globalNotifyLevel = user.notify_props.desktop;
|
||||
|
||||
// Get glabal user setting for notifications
|
||||
const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
|
||||
let globalNotifyLevelName;
|
||||
if (globalNotifyLevel === 'all') {
|
||||
globalNotifyLevelName = (
|
||||
@@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
/>
|
||||
);
|
||||
|
||||
const notificationLevel = this.state.notifyLevel;
|
||||
|
||||
if (this.state.activeSection === 'desktop') {
|
||||
var notifyActive = [false, false, false, false];
|
||||
if (this.state.notifyLevel === 'default') {
|
||||
const notifyActive = [false, false, false, false];
|
||||
if (notificationLevel === 'default') {
|
||||
notifyActive[0] = true;
|
||||
} else if (this.state.notifyLevel === 'all') {
|
||||
} else if (notificationLevel === 'all') {
|
||||
notifyActive[1] = true;
|
||||
} else if (this.state.notifyLevel === 'mention') {
|
||||
} else if (notificationLevel === 'mention') {
|
||||
notifyActive[2] = true;
|
||||
} else {
|
||||
notifyActive[3] = true;
|
||||
@@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
handleUpdateSection = function updateSection(e) {
|
||||
const handleUpdateSection = function updateSection(e) {
|
||||
this.updateSection('');
|
||||
this.onListenerChange();
|
||||
e.preventDefault();
|
||||
@@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
}
|
||||
|
||||
var describe;
|
||||
if (this.state.notifyLevel === 'default') {
|
||||
if (notificationLevel === 'default') {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.globalDefault'
|
||||
@@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.notifyLevel === 'mention') {
|
||||
} else if (notificationLevel === 'mention') {
|
||||
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
|
||||
} else if (this.state.notifyLevel === 'all') {
|
||||
} else if (notificationLevel === 'all') {
|
||||
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
|
||||
} else {
|
||||
describe = (<FormattedMessage id='channel_notifications.never'/>);
|
||||
}
|
||||
|
||||
handleUpdateSection = function updateSection(e) {
|
||||
this.updateSection('desktop');
|
||||
e.preventDefault();
|
||||
}.bind(this);
|
||||
|
||||
return (
|
||||
<SettingItemMin
|
||||
title={sendDesktop}
|
||||
describe={describe}
|
||||
updateSection={handleUpdateSection}
|
||||
updateSection={() => {
|
||||
this.updateSection('desktop');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmitMarkUnreadLevel() {
|
||||
const channelId = this.state.channelId;
|
||||
const markUnreadLevel = this.state.markUnreadLevel;
|
||||
const channelId = this.props.channel.id;
|
||||
const markUnreadLevel = this.state.unreadLevel;
|
||||
|
||||
if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
|
||||
if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
|
||||
this.updateSection('');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
channel_id: channelId,
|
||||
user_id: UserStore.getCurrentId(),
|
||||
user_id: this.props.currentUser.id,
|
||||
mark_unread: markUnreadLevel
|
||||
};
|
||||
|
||||
//TODO: This should be fixed, moved to event_helpers
|
||||
Client.updateNotifyProps(data,
|
||||
() => {
|
||||
// Yuck...
|
||||
var member = ChannelStore.getMember(channelId);
|
||||
member.notify_props.mark_unread = markUnreadLevel;
|
||||
ChannelStore.setChannelMember(member);
|
||||
@@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
handleUpdateMarkUnreadLevel(markUnreadLevel) {
|
||||
this.setState({markUnreadLevel});
|
||||
handleUpdateMarkUnreadLevel(unreadLevel) {
|
||||
this.setState({unreadLevel});
|
||||
}
|
||||
|
||||
createMarkUnreadLevelSection(serverError) {
|
||||
@@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={this.state.markUnreadLevel === 'all'}
|
||||
checked={this.state.unreadLevel === 'all'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
@@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={this.state.markUnreadLevel === 'mention'}
|
||||
checked={this.state.unreadLevel === 'mention'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
||||
/>
|
||||
<FormattedMessage id='channel_notifications.onlyMentions'/>
|
||||
@@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
} else {
|
||||
let describe;
|
||||
|
||||
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
|
||||
if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.allUnread'
|
||||
@@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
||||
ChannelNotificationsModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
channelMember: React.PropTypes.object.isRequired,
|
||||
currentUser: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -2,34 +2,11 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import CenterPanel from '../components/center_panel.jsx';
|
||||
import Sidebar from '../components/sidebar.jsx';
|
||||
import SidebarRight from '../components/sidebar_right.jsx';
|
||||
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
|
||||
|
||||
export default class ChannelView extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='container-fluid'>
|
||||
<div
|
||||
className='sidebar--right'
|
||||
id='sidebar-right'
|
||||
>
|
||||
<SidebarRight/>
|
||||
</div>
|
||||
<div
|
||||
className='sidebar--menu'
|
||||
id='sidebar-menu'
|
||||
>
|
||||
<SidebarRightMenu/>
|
||||
</div>
|
||||
<div
|
||||
className='sidebar--left'
|
||||
id='sidebar-left'
|
||||
>
|
||||
<Sidebar/>
|
||||
</div>
|
||||
<CenterPanel/>
|
||||
</div>
|
||||
<CenterPanel/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,4 +14,5 @@ ChannelView.defaultProps = {
|
||||
};
|
||||
|
||||
ChannelView.propTypes = {
|
||||
params: React.PropTypes.object
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import EmailToSSO from './email_to_sso.jsx';
|
||||
import SSOToEmail from './sso_to_email.jsx';
|
||||
import TeamStore from '../../stores/team_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
@@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onTeamChange = this.onTeamChange.bind(this);
|
||||
this.updateStateFromStores = this.updateStateFromStores.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
email: this.props.location.query.email,
|
||||
newType: this.props.location.query.new_type,
|
||||
oldType: this.props.location.query.old_type,
|
||||
teamName: this.props.params.team,
|
||||
teamDisplayName: ''
|
||||
});
|
||||
this.updateStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
TeamStore.addChangeListener(this.onTeamChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
TeamStore.removeChangeListener(this.onTeamChange);
|
||||
}
|
||||
updateStateFromStores() {
|
||||
const team = TeamStore.getByName(this.state.teamName);
|
||||
let displayName = '';
|
||||
if (team) {
|
||||
displayName = team.displayName;
|
||||
}
|
||||
this.setState({
|
||||
teamDisplayName: displayName
|
||||
});
|
||||
}
|
||||
onTeamChange() {
|
||||
this.updateStateFromStores();
|
||||
}
|
||||
render() {
|
||||
if (this.state.teamDisplayName === '') {
|
||||
return (<div/>);
|
||||
}
|
||||
let content;
|
||||
if (this.props.email === '') {
|
||||
if (this.state.email === '') {
|
||||
content = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
@@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component {
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (this.props.currentType === '' && this.props.newType !== '') {
|
||||
} else if (this.state.oldType === '' && this.state.newType !== '') {
|
||||
content = (
|
||||
<EmailToSSO
|
||||
email={this.props.email}
|
||||
type={this.props.newType}
|
||||
teamName={this.props.teamName}
|
||||
teamDisplayName={this.props.teamDisplayName}
|
||||
email={this.state.email}
|
||||
type={this.state.newType}
|
||||
teamName={this.state.teamName}
|
||||
teamDisplayName={this.state.teamDisplayName}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<SSOToEmail
|
||||
email={this.props.email}
|
||||
currentType={this.props.currentType}
|
||||
teamName={this.props.teamName}
|
||||
teamDisplayName={this.props.teamDisplayName}
|
||||
email={this.state.email}
|
||||
currentType={this.state.oldType}
|
||||
teamName={this.state.teamName}
|
||||
teamDisplayName={this.state.teamDisplayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
return (
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<img
|
||||
className='signup-team-logo'
|
||||
src='/static/images/logo.png'
|
||||
/>
|
||||
<div id='claim'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClaimAccount.defaultProps = {
|
||||
};
|
||||
ClaimAccount.propTypes = {
|
||||
currentType: React.PropTypes.string.isRequired,
|
||||
newType: React.PropTypes.string.isRequired,
|
||||
email: React.PropTypes.string.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired,
|
||||
teamDisplayName: React.PropTypes.string.isRequired
|
||||
params: React.PropTypes.object.isRequired,
|
||||
location: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -159,7 +159,7 @@ SSOToEmail.propTypes = {
|
||||
currentType: React.PropTypes.string.isRequired,
|
||||
email: React.PropTypes.string.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired,
|
||||
teamDisplayName: React.PropTypes.string.isRequired
|
||||
teamDisplayName: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(SSOToEmail);
|
||||
|
||||
@@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
|
||||
import TutorialTip from './tutorial/tutorial_tip.jsx';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import * as AsyncClient from '../utils/async_client.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
@@ -165,7 +165,7 @@ class CreatePost extends React.Component {
|
||||
|
||||
const channel = ChannelStore.get(this.state.channelId);
|
||||
|
||||
EventHelpers.emitUserPostedEvent(post);
|
||||
GlobalActions.emitUserPostedEvent(post);
|
||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||
|
||||
Client.createPost(post, channel,
|
||||
@@ -177,7 +177,7 @@ class CreatePost extends React.Component {
|
||||
member.last_viewed_at = Date.now();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
EventHelpers.emitPostRecievedEvent(data);
|
||||
GlobalActions.emitPostRecievedEvent(data);
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||
|
||||
@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
export default class DeleteChannelModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
|
||||
Client.deleteChannel(
|
||||
this.props.channel.id,
|
||||
() => {
|
||||
AsyncClient.getChannels(true);
|
||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'handleDelete');
|
||||
|
||||
82
web/react/components/do_verify_email.jsx
Normal file
82
web/react/components/do_verify_email.jsx
Normal file
@@ -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 AsyncClient from '../utils/async_client.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||
import Textbox from './textbox.jsx';
|
||||
import BrowserStore from '../stores/browser_store.jsx';
|
||||
import PostStore from '../stores/post_store.jsx';
|
||||
@@ -45,7 +45,7 @@ class EditPostModal extends React.Component {
|
||||
delete tempState.editText;
|
||||
BrowserStore.setItem('edit_state_transfer', tempState);
|
||||
$('#edit_post').modal('hide');
|
||||
EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
||||
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
var self = this; // Need this reference since we use the given "this"
|
||||
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
|
||||
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
|
||||
return function loader() {
|
||||
$(this).remove();
|
||||
if (name in self.refs) {
|
||||
@@ -114,7 +114,7 @@ class FileAttachment extends React.Component {
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
|
||||
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
|
||||
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
|
||||
}
|
||||
}
|
||||
removeBackgroundImage(name) {
|
||||
@@ -185,6 +185,7 @@ class FileAttachment extends React.Component {
|
||||
data-toggle='tooltip'
|
||||
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
|
||||
className='post-image__name'
|
||||
target='_blank'
|
||||
>
|
||||
{trimmedFilename}
|
||||
</a>
|
||||
@@ -193,6 +194,7 @@ class FileAttachment extends React.Component {
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
className='post-image__download'
|
||||
target='_blank'
|
||||
>
|
||||
<span
|
||||
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';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||
import ModalStore from '../stores/modal_store.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
@@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component {
|
||||
showGetTeamInviteLinkModal() {
|
||||
this.handleHide(false);
|
||||
|
||||
EventHelpers.showGetTeamInviteLinkModal();
|
||||
GlobalActions.showGetTeamInviteLinkModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
224
web/react/components/logged_in.jsx
Normal file
224
web/react/components/logged_in.jsx
Normal file
@@ -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 * as Utils from '../utils/utils.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import Constants from '../utils/constants.jsx';
|
||||
import TeamStore from '../stores/team_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
export default class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.onTeamChange = this.onTeamChange.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
TeamStore.addChangeListener(this.onTeamChange);
|
||||
Client.getMeLoggedIn((data) => {
|
||||
if (data && data.logged_in !== 'false') {
|
||||
browserHistory.push('/' + this.props.params.team + '/channels/town-square');
|
||||
}
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
TeamStore.removeChangeListener(this.onTeamChange);
|
||||
}
|
||||
getStateFromStores() {
|
||||
return {
|
||||
currentTeam: TeamStore.getByName(this.props.params.team)
|
||||
};
|
||||
}
|
||||
onTeamChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
render() {
|
||||
const teamDisplayName = this.props.teamDisplayName;
|
||||
const teamName = this.props.teamName;
|
||||
const currentTeam = this.state.currentTeam;
|
||||
if (currentTeam == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
const teamDisplayName = currentTeam.display_name;
|
||||
const teamName = currentTeam.name;
|
||||
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
|
||||
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
|
||||
|
||||
let loginMessage = [];
|
||||
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
|
||||
loginMessage.push(
|
||||
<a
|
||||
className='btn btn-custom-login gitlab'
|
||||
key='gitlab'
|
||||
href={'/' + teamName + '/login/gitlab'}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.gitlab'
|
||||
defaultMessage='with GitLab'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className='btn btn-custom-login gitlab'
|
||||
key='gitlab'
|
||||
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.gitlab'
|
||||
defaultMessage='with GitLab'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
||||
loginMessage.push(
|
||||
<a
|
||||
className='btn btn-custom-login google'
|
||||
key='google'
|
||||
href={'/' + teamName + '/login/google'}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.google'
|
||||
defaultMessage='with Google Apps'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
<a
|
||||
className='btn btn-custom-login google'
|
||||
key='google'
|
||||
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.google'
|
||||
defaultMessage='with Google Apps'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const extraParam = Utils.getUrlParameter('extra');
|
||||
let extraBox = '';
|
||||
if (extraParam) {
|
||||
let msg;
|
||||
if (extraParam === Constants.SIGNIN_CHANGE) {
|
||||
msg = (
|
||||
<FormattedMessage
|
||||
id='login.changed'
|
||||
defaultMessage=' Sign-in method changed successfully'
|
||||
/>
|
||||
);
|
||||
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
|
||||
msg = (
|
||||
<FormattedMessage
|
||||
id='login.verified'
|
||||
defaultMessage=' Email Verified'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
extraBox = (
|
||||
<div className='alert alert-success'>
|
||||
<i className='fa fa-check'/>
|
||||
{msg}
|
||||
<FormattedMessage
|
||||
id='login.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>
|
||||
);
|
||||
}
|
||||
@@ -91,7 +129,7 @@ export default class Login extends React.Component {
|
||||
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
|
||||
emailSignup = (
|
||||
<LoginEmail
|
||||
teamName={this.props.teamName}
|
||||
teamName={teamName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -125,7 +163,7 @@ export default class Login extends React.Component {
|
||||
}
|
||||
|
||||
let userSignUp = null;
|
||||
if (this.props.inviteId) {
|
||||
if (currentTeam.allow_open_invite) {
|
||||
userSignUp = (
|
||||
<div>
|
||||
<span>
|
||||
@@ -134,7 +172,7 @@ export default class Login extends React.Component {
|
||||
defaultMessage="Don't have an account? "
|
||||
/>
|
||||
<a
|
||||
href={'/signup_user_complete/?id=' + this.props.inviteId}
|
||||
href={'/signup_user_complete/?id=' + currentTeam.invite_id}
|
||||
className='signup-team-login'
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -168,22 +206,23 @@ export default class Login extends React.Component {
|
||||
if (global.window.mm_config.EnableLdap === 'true') {
|
||||
ldapLogin = (
|
||||
<LoginLdap
|
||||
teamName={this.props.teamName}
|
||||
teamName={teamName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let findTeams = null;
|
||||
if (!Utils.isMobileApp()) {
|
||||
findTeams = (
|
||||
<div className='form-group margin--extra form-group--small'>
|
||||
<span>
|
||||
<a href='/find_team'>
|
||||
<FormattedMessage
|
||||
id='login.find'
|
||||
defaultMessage='Find your other teams'
|
||||
/>
|
||||
</a></span>
|
||||
if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
|
||||
ldapLogin = (
|
||||
<div>
|
||||
<div className='or__container'>
|
||||
<FormattedMessage
|
||||
id='login.or'
|
||||
defaultMessage='or'
|
||||
/>
|
||||
</div>
|
||||
<LoginLdap
|
||||
teamName={teamName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -192,49 +231,72 @@ export default class Login extends React.Component {
|
||||
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
|
||||
usernameLogin = (
|
||||
<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 (
|
||||
<div className='signup-team__container'>
|
||||
<h5 className='margin--less'>
|
||||
<FormattedMessage
|
||||
id='login.signTo'
|
||||
defaultMessage='Sign in to:'
|
||||
/>
|
||||
</h5>
|
||||
<h2 className='signup-team__name'>{teamDisplayName}</h2>
|
||||
<h2 className='signup-team__subdomain'>
|
||||
<FormattedMessage
|
||||
id='login.on'
|
||||
defaultMessage='on {siteName}'
|
||||
values={{
|
||||
siteName: global.window.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
{extraBox}
|
||||
{loginMessage}
|
||||
{emailSignup}
|
||||
{usernameLogin}
|
||||
{ldapLogin}
|
||||
{userSignUp}
|
||||
{findTeams}
|
||||
{forgotPassword}
|
||||
{teamSignUp}
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<h5 className='margin--less'>
|
||||
<FormattedMessage
|
||||
id='login.signTo'
|
||||
defaultMessage='Sign in to:'
|
||||
/>
|
||||
</h5>
|
||||
<h2 className='signup-team__name'>{teamDisplayName}</h2>
|
||||
<h2 className='signup-team__subdomain'>
|
||||
<FormattedMessage
|
||||
id='login.on'
|
||||
defaultMessage='on {siteName}'
|
||||
values={{
|
||||
siteName: global.window.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
{extraBox}
|
||||
{loginMessage}
|
||||
{emailSignup}
|
||||
{usernameLogin}
|
||||
{ldapLogin}
|
||||
{userSignUp}
|
||||
{forgotPassword}
|
||||
{teamSignUp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Login.defaultProps = {
|
||||
teamName: '',
|
||||
teamDisplayName: ''
|
||||
};
|
||||
Login.propTypes = {
|
||||
teamName: React.PropTypes.string,
|
||||
teamDisplayName: React.PropTypes.string,
|
||||
inviteId: React.PropTypes.string
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
||||
|
||||
@@ -72,13 +73,7 @@ class LoginEmail extends React.Component {
|
||||
Client.loginByEmail(name, email, password,
|
||||
() => {
|
||||
UserStore.setLastEmail(email);
|
||||
|
||||
const redirect = Utils.getUrlParameter('redirect');
|
||||
if (redirect) {
|
||||
window.location.href = decodeURIComponent(redirect);
|
||||
} else {
|
||||
window.location.href = '/' + name + '/channels/town-square';
|
||||
}
|
||||
browserHistory.push('/' + name + '/channels/town-square');
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.user.login.not_verified.app_error') {
|
||||
@@ -167,4 +162,4 @@ LoginEmail.propTypes = {
|
||||
teamName: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(LoginEmail);
|
||||
export default injectIntl(LoginEmail);
|
||||
|
||||
@@ -56,9 +56,13 @@ export default class Navbar extends React.Component {
|
||||
return {
|
||||
channel: ChannelStore.getCurrent(),
|
||||
member: ChannelStore.getCurrentMember(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members
|
||||
users: ChannelStore.getCurrentExtraInfo().members,
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
}
|
||||
stateValid() {
|
||||
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||
@@ -201,7 +205,7 @@ export default class Navbar extends React.Component {
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel}}
|
||||
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.addMembers'
|
||||
@@ -286,7 +290,11 @@ export default class Navbar extends React.Component {
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{channel}}
|
||||
dialogProps={{
|
||||
channel,
|
||||
channelMember: this.state.member,
|
||||
currentUser: this.state.currentUser
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.preferences'
|
||||
@@ -412,7 +420,11 @@ export default class Navbar extends React.Component {
|
||||
return buttons;
|
||||
}
|
||||
render() {
|
||||
var currentId = UserStore.getCurrentId();
|
||||
if (!this.stateValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentId = this.state.currentUser.id;
|
||||
var channel = this.state.channel;
|
||||
var channelTitle = this.props.teamDisplayName;
|
||||
var popoverContent;
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import * as client from '../utils/client.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import TeamStore from '../stores/team_store.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as GlobalActions from '../action_creators/global_actions.jsx';
|
||||
|
||||
import AboutBuildModal from './about_build_modal.jsx';
|
||||
import TeamMembersModal from './team_members_modal.jsx';
|
||||
@@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx';
|
||||
import Constants from '../utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
function getStateFromStores() {
|
||||
const teams = [];
|
||||
const teamsObject = UserStore.getTeams();
|
||||
for (const teamId in teamsObject) {
|
||||
if (teamsObject.hasOwnProperty(teamId)) {
|
||||
teams.push(teamsObject[teamId]);
|
||||
}
|
||||
}
|
||||
|
||||
teams.sort(Utils.sortByDisplayName);
|
||||
return {teams};
|
||||
}
|
||||
import {Link} from 'react-router';
|
||||
|
||||
export default class NavbarDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockToggle = false;
|
||||
|
||||
this.handleLogoutClick = this.handleLogoutClick.bind(this);
|
||||
this.handleAboutModal = this.handleAboutModal.bind(this);
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
|
||||
|
||||
const state = getStateFromStores();
|
||||
state.showUserSettingsModal = false;
|
||||
state.showAboutModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
handleLogoutClick(e) {
|
||||
e.preventDefault();
|
||||
client.logout();
|
||||
this.state = {
|
||||
showUserSettingsModal: false,
|
||||
showAboutModal: false
|
||||
};
|
||||
}
|
||||
handleAboutModal() {
|
||||
this.setState({showAboutModal: true});
|
||||
@@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component {
|
||||
this.setState({showAboutModal: false});
|
||||
}
|
||||
componentDidMount() {
|
||||
UserStore.addTeamsChangeListener(this.onListenerChange);
|
||||
TeamStore.addChangeListener(this.onListenerChange);
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||
$('.sidebar--left .dropdown-menu').scrollTop(0);
|
||||
this.blockToggle = true;
|
||||
@@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component {
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
UserStore.removeTeamsChangeListener(this.onListenerChange);
|
||||
TeamStore.removeChangeListener(this.onListenerChange);
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
|
||||
}
|
||||
onListenerChange() {
|
||||
var newState = getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var teamLink = '';
|
||||
var inviteLink = '';
|
||||
var manageLink = '';
|
||||
var sysAdminLink = '';
|
||||
var adminDivider = '';
|
||||
var currentUser = UserStore.getCurrentUser();
|
||||
var currentUser = this.props.currentUser;
|
||||
var isAdmin = false;
|
||||
var isSystemAdmin = false;
|
||||
var teamSettings = null;
|
||||
@@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={EventHelpers.showInviteMemberModal}
|
||||
onClick={GlobalActions.showInviteMemberModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.inviteMember'
|
||||
@@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={EventHelpers.showGetTeamInviteLinkModal}
|
||||
onClick={GlobalActions.showGetTeamInviteLinkModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.teamLink'
|
||||
@@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
sysAdminLink = (
|
||||
<li>
|
||||
<a
|
||||
href={'/admin_console?' + Utils.getSessionIndex()}
|
||||
href={'/admin_console'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.console'
|
||||
@@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component {
|
||||
|
||||
var teams = [];
|
||||
|
||||
if (this.state.teams.length > 1) {
|
||||
teams.push(
|
||||
<li
|
||||
className='divider'
|
||||
key='div'
|
||||
>
|
||||
</li>
|
||||
);
|
||||
|
||||
this.state.teams.forEach((team) => {
|
||||
if (team.name !== this.props.teamName) {
|
||||
teams.push(
|
||||
<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.switchTeam'
|
||||
defaultMessage='Switch to {team}'
|
||||
values={{
|
||||
team: team.display_name
|
||||
}}
|
||||
/>
|
||||
</a></li>);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (global.window.mm_config.EnableTeamCreation === 'true') {
|
||||
teams.push(
|
||||
<li key='newTeam_li'>
|
||||
@@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component {
|
||||
{inviteLink}
|
||||
{teamLink}
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleLogoutClick}
|
||||
>
|
||||
<Link to={'/' + this.props.teamName + '/logout'}>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.logout'
|
||||
defaultMessage='Logout'
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
{adminDivider}
|
||||
{teamSettings}
|
||||
@@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = {
|
||||
NavbarDropdown.propTypes = {
|
||||
teamType: React.PropTypes.string,
|
||||
teamDisplayName: React.PropTypes.string,
|
||||
teamName: React.PropTypes.string
|
||||
teamName: React.PropTypes.string,
|
||||
currentUser: React.PropTypes.object
|
||||
};
|
||||
|
||||
20
web/react/components/needs_team.jsx
Normal file
20
web/react/components/needs_team.jsx
Normal file
@@ -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
Normal file
70
web/react/components/not_logged_in.jsx
Normal file
@@ -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
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user