From d7cdcf082fab6c0cb7c2fe4bed821bd1a8000e69 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 8 Feb 2016 07:26:10 -0500 Subject: Convering client to react-router. --- Makefile | 25 +- api/api.go | 43 +- api/context.go | 137 +-- api/license.go | 20 + api/license_test.go | 22 + api/oauth.go | 343 +++++- api/post.go | 7 +- api/team.go | 114 +- api/team_test.go | 111 +- api/templates/email_change_body.html | 41 - api/templates/email_change_subject.html | 1 - api/templates/email_change_verify_body.html | 44 - api/templates/email_change_verify_subject.html | 1 - api/templates/email_footer.html | 13 - api/templates/email_info.html | 7 - api/templates/error.html | 37 - api/templates/find_teams_body.html | 52 - api/templates/find_teams_subject.html | 1 - api/templates/invite_body.html | 46 - api/templates/invite_subject.html | 1 - api/templates/password_change_body.html | 43 - api/templates/password_change_subject.html | 1 - api/templates/post_body.html | 45 - api/templates/post_subject.html | 1 - api/templates/reset_body.html | 46 - api/templates/reset_subject.html | 1 - api/templates/signin_change_body.html | 43 - api/templates/signin_change_subject.html | 1 - api/templates/signup_team_body.html | 44 - api/templates/signup_team_subject.html | 1 - api/templates/verify_body.html | 44 - api/templates/verify_subject.html | 1 - api/templates/welcome_body.html | 51 - api/templates/welcome_subject.html | 1 - api/user.go | 146 ++- api/user_test.go | 35 + i18n/en.json | 28 +- i18n/es.json | 20 - model/client.go | 67 +- model/session.go | 2 +- model/team.go | 7 + templates/authorize.html | 12 + templates/email_change_body.html | 41 + templates/email_change_subject.html | 1 + templates/email_change_verify_body.html | 44 + templates/email_change_verify_subject.html | 1 + templates/email_footer.html | 13 + templates/email_info.html | 7 + templates/error.html | 24 + templates/head.html | 92 ++ templates/invite_body.html | 46 + templates/invite_subject.html | 1 + templates/password_change_body.html | 43 + templates/password_change_subject.html | 1 + templates/post_body.html | 45 + templates/post_subject.html | 1 + templates/reset_body.html | 46 + templates/reset_subject.html | 1 + templates/root.html | 12 + templates/signin_change_body.html | 43 + templates/signin_change_subject.html | 1 + templates/signup_team_body.html | 44 + templates/signup_team_subject.html | 1 + templates/verify_body.html | 44 + templates/verify_subject.html | 1 + templates/welcome_body.html | 51 + templates/welcome_subject.html | 1 + utils/html.go | 97 ++ utils/license.go | 2 +- web/react/action_creators/global_actions.jsx | 252 +++++ web/react/components/activity_log_modal.jsx | 36 +- .../components/admin_console/admin_controller.jsx | 11 +- .../admin_console/admin_navbar_dropdown.jsx | 19 +- .../components/admin_console/admin_sidebar.jsx | 4 - .../admin_console/admin_sidebar_header.jsx | 5 +- web/react/components/admin_console/user_item.jsx | 2 +- web/react/components/audit_table.jsx | 21 +- web/react/components/center_panel.jsx | 47 +- web/react/components/channel_header.jsx | 32 +- web/react/components/channel_invite_modal.jsx | 37 +- web/react/components/channel_loader.jsx | 204 ---- .../components/channel_notifications_modal.jsx | 107 +- web/react/components/channel_view.jsx | 26 +- web/react/components/claim/claim_account.jsx | 87 +- web/react/components/claim/sso_to_email.jsx | 2 +- web/react/components/create_post.jsx | 6 +- web/react/components/delete_channel_modal.jsx | 4 +- web/react/components/do_verify_email.jsx | 82 ++ web/react/components/docs.jsx | 41 - web/react/components/edit_post_modal.jsx | 4 +- web/react/components/email_verify.jsx | 108 -- web/react/components/file_attachment.jsx | 4 +- web/react/components/find_team.jsx | 135 --- web/react/components/invite_member_modal.jsx | 4 +- web/react/components/logged_in.jsx | 224 ++++ web/react/components/login.jsx | 222 ++-- web/react/components/login_email.jsx | 11 +- web/react/components/navbar.jsx | 20 +- web/react/components/navbar_dropdown.jsx | 88 +- web/react/components/needs_team.jsx | 20 + web/react/components/not_logged_in.jsx | 70 ++ web/react/components/password_reset.jsx | 47 - web/react/components/password_reset_form.jsx | 105 +- web/react/components/password_reset_send_link.jsx | 186 ++-- web/react/components/popover_list_members.jsx | 2 +- web/react/components/post.jsx | 19 +- web/react/components/post_body.jsx | 10 +- web/react/components/post_focus_view.jsx | 22 +- web/react/components/post_header.jsx | 17 +- web/react/components/post_info.jsx | 56 +- web/react/components/posts_view.jsx | 17 +- web/react/components/posts_view_container.jsx | 19 +- web/react/components/rhs_comment.jsx | 8 +- web/react/components/rhs_root_post.jsx | 8 +- web/react/components/root.jsx | 90 ++ web/react/components/search_results_item.jsx | 9 +- web/react/components/should_verify_email.jsx | 111 ++ web/react/components/sidebar.jsx | 30 +- web/react/components/sidebar_header.jsx | 14 +- web/react/components/sidebar_right.jsx | 9 +- web/react/components/sidebar_right_menu.jsx | 39 +- web/react/components/signup_team.jsx | 159 +-- web/react/components/signup_team_complete.jsx | 121 -- .../components/signup_team_complete.jsx | 79 ++ .../components/team_signup_display_name_page.jsx | 136 +++ .../components/team_signup_email_item.jsx | 86 ++ .../components/team_signup_finished.jsx | 15 + .../components/team_signup_password_page.jsx | 215 ++++ .../components/team_signup_send_invites_page.jsx | 210 ++++ .../components/team_signup_url_page.jsx | 205 ++++ .../components/team_signup_username_page.jsx | 164 +++ .../components/team_signup_welcome_page.jsx | 234 ++++ web/react/components/signup_team_confirm.jsx | 47 +- web/react/components/signup_user_complete.jsx | 331 +++--- .../components/suggestion/at_mention_provider.jsx | 2 +- web/react/components/suggestion/suggestion_box.jsx | 12 +- .../components/suggestion/suggestion_list.jsx | 4 +- web/react/components/team_members_modal.jsx | 32 +- web/react/components/team_settings.jsx | 3 + .../components/team_signup_display_name_page.jsx | 136 --- web/react/components/team_signup_email_item.jsx | 86 -- web/react/components/team_signup_password_page.jsx | 214 ---- .../components/team_signup_send_invites_page.jsx | 210 ---- web/react/components/team_signup_url_page.jsx | 205 ---- web/react/components/team_signup_username_page.jsx | 164 --- web/react/components/team_signup_welcome_page.jsx | 232 ---- web/react/components/team_signup_with_email.jsx | 7 +- web/react/components/user_list_row.jsx | 2 +- web/react/components/user_profile.jsx | 29 +- .../components/user_settings/manage_languages.jsx | 3 +- .../user_settings/user_settings_developer.jsx | 4 +- .../user_settings/user_settings_general.jsx | 15 +- .../user_settings/user_settings_modal.jsx | 16 +- .../user_settings/user_settings_security.jsx | 29 +- web/react/dispatcher/event_helpers.jsx | 222 ---- web/react/package.json | 2 + web/react/pages/admin_console.jsx | 71 -- web/react/pages/channel.jsx | 97 -- web/react/pages/claim_account.jsx | 68 -- web/react/pages/docs.jsx | 64 -- web/react/pages/find_team.jsx | 62 -- web/react/pages/home.jsx | 16 - web/react/pages/login.jsx | 66 -- web/react/pages/password_reset.jsx | 68 -- web/react/pages/root.jsx | 290 +++++ web/react/pages/signup_team.jsx | 76 -- web/react/pages/signup_team_complete.jsx | 66 -- web/react/pages/signup_team_confirm.jsx | 64 -- web/react/pages/signup_user_complete.jsx | 69 -- web/react/pages/verify.jsx | 67 -- web/react/stores/admin_store.jsx | 10 +- web/react/stores/analytics_store.jsx | 4 - web/react/stores/browser_store.jsx | 6 +- web/react/stores/channel_store.jsx | 4 - web/react/stores/file_store.jsx | 7 +- web/react/stores/localization_store.jsx | 60 + web/react/stores/modal_store.jsx | 4 - web/react/stores/post_store.jsx | 4 - web/react/stores/search_store.jsx | 4 - web/react/stores/socket_store.jsx | 14 +- web/react/stores/suggestion_store.jsx | 7 +- web/react/stores/team_store.jsx | 42 +- web/react/stores/user_store.jsx | 66 +- web/react/utils/async_client.jsx | 55 +- web/react/utils/channel_intro_messages.jsx | 49 +- web/react/utils/client.jsx | 179 ++- web/react/utils/constants.jsx | 5 +- web/react/utils/utils.jsx | 33 +- web/sass-files/sass/partials/_sidebar--left.scss | 6 - web/static/help/Messaging_en.md | 47 - web/static/help/Messaging_es.md | 37 - web/static/i18n/en.json | 17 +- web/static/i18n/es.json | 14 +- web/static/i18n/pt.json | 5 +- web/templates/admin_console.html | 21 - web/templates/authorize.html | 12 - web/templates/channel.html | 21 - web/templates/claim_account.html | 30 - web/templates/docs.html | 27 - web/templates/find_team.html | 30 - web/templates/footer.html | 39 - web/templates/head.html | 191 ---- web/templates/home.html | 24 - web/templates/login.html | 27 - web/templates/password_reset.html | 30 - web/templates/signup_team.html | 29 - web/templates/signup_team_complete.html | 29 - web/templates/signup_team_confirm.html | 26 - web/templates/signup_user_complete.html | 29 - web/templates/verify.html | 30 - web/web.go | 1158 +------------------- 211 files changed, 5356 insertions(+), 7014 deletions(-) create mode 100644 api/license_test.go delete mode 100644 api/templates/email_change_body.html delete mode 100644 api/templates/email_change_subject.html delete mode 100644 api/templates/email_change_verify_body.html delete mode 100644 api/templates/email_change_verify_subject.html delete mode 100644 api/templates/email_footer.html delete mode 100644 api/templates/email_info.html delete mode 100644 api/templates/error.html delete mode 100644 api/templates/find_teams_body.html delete mode 100644 api/templates/find_teams_subject.html delete mode 100644 api/templates/invite_body.html delete mode 100644 api/templates/invite_subject.html delete mode 100644 api/templates/password_change_body.html delete mode 100644 api/templates/password_change_subject.html delete mode 100644 api/templates/post_body.html delete mode 100644 api/templates/post_subject.html delete mode 100644 api/templates/reset_body.html delete mode 100644 api/templates/reset_subject.html delete mode 100644 api/templates/signin_change_body.html delete mode 100644 api/templates/signin_change_subject.html delete mode 100644 api/templates/signup_team_body.html delete mode 100644 api/templates/signup_team_subject.html delete mode 100644 api/templates/verify_body.html delete mode 100644 api/templates/verify_subject.html delete mode 100644 api/templates/welcome_body.html delete mode 100644 api/templates/welcome_subject.html create mode 100644 templates/authorize.html create mode 100644 templates/email_change_body.html create mode 100644 templates/email_change_subject.html create mode 100644 templates/email_change_verify_body.html create mode 100644 templates/email_change_verify_subject.html create mode 100644 templates/email_footer.html create mode 100644 templates/email_info.html create mode 100644 templates/error.html create mode 100644 templates/head.html create mode 100644 templates/invite_body.html create mode 100644 templates/invite_subject.html create mode 100644 templates/password_change_body.html create mode 100644 templates/password_change_subject.html create mode 100644 templates/post_body.html create mode 100644 templates/post_subject.html create mode 100644 templates/reset_body.html create mode 100644 templates/reset_subject.html create mode 100644 templates/root.html create mode 100644 templates/signin_change_body.html create mode 100644 templates/signin_change_subject.html create mode 100644 templates/signup_team_body.html create mode 100644 templates/signup_team_subject.html create mode 100644 templates/verify_body.html create mode 100644 templates/verify_subject.html create mode 100644 templates/welcome_body.html create mode 100644 templates/welcome_subject.html create mode 100644 utils/html.go create mode 100644 web/react/action_creators/global_actions.jsx delete mode 100644 web/react/components/channel_loader.jsx create mode 100644 web/react/components/do_verify_email.jsx delete mode 100644 web/react/components/docs.jsx delete mode 100644 web/react/components/email_verify.jsx delete mode 100644 web/react/components/find_team.jsx create mode 100644 web/react/components/logged_in.jsx create mode 100644 web/react/components/needs_team.jsx create mode 100644 web/react/components/not_logged_in.jsx delete mode 100644 web/react/components/password_reset.jsx create mode 100644 web/react/components/root.jsx create mode 100644 web/react/components/should_verify_email.jsx delete mode 100644 web/react/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_email_item.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_finished.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_password_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_url_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_username_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx delete mode 100644 web/react/components/team_signup_display_name_page.jsx delete mode 100644 web/react/components/team_signup_email_item.jsx delete mode 100644 web/react/components/team_signup_password_page.jsx delete mode 100644 web/react/components/team_signup_send_invites_page.jsx delete mode 100644 web/react/components/team_signup_url_page.jsx delete mode 100644 web/react/components/team_signup_username_page.jsx delete mode 100644 web/react/components/team_signup_welcome_page.jsx delete mode 100644 web/react/dispatcher/event_helpers.jsx delete mode 100644 web/react/pages/admin_console.jsx delete mode 100644 web/react/pages/channel.jsx delete mode 100644 web/react/pages/claim_account.jsx delete mode 100644 web/react/pages/docs.jsx delete mode 100644 web/react/pages/find_team.jsx delete mode 100644 web/react/pages/home.jsx delete mode 100644 web/react/pages/login.jsx delete mode 100644 web/react/pages/password_reset.jsx create mode 100644 web/react/pages/root.jsx delete mode 100644 web/react/pages/signup_team.jsx delete mode 100644 web/react/pages/signup_team_complete.jsx delete mode 100644 web/react/pages/signup_team_confirm.jsx delete mode 100644 web/react/pages/signup_user_complete.jsx delete mode 100644 web/react/pages/verify.jsx create mode 100644 web/react/stores/localization_store.jsx delete mode 100644 web/static/help/Messaging_en.md delete mode 100644 web/static/help/Messaging_es.md delete mode 100644 web/templates/admin_console.html delete mode 100644 web/templates/authorize.html delete mode 100644 web/templates/channel.html delete mode 100644 web/templates/claim_account.html delete mode 100644 web/templates/docs.html delete mode 100644 web/templates/find_team.html delete mode 100644 web/templates/footer.html delete mode 100644 web/templates/head.html delete mode 100644 web/templates/home.html delete mode 100644 web/templates/login.html delete mode 100644 web/templates/password_reset.html delete mode 100644 web/templates/signup_team.html delete mode 100644 web/templates/signup_team_complete.html delete mode 100644 web/templates/signup_team_confirm.html delete mode 100644 web/templates/signup_user_complete.html delete mode 100644 web/templates/verify.html diff --git a/Makefile b/Makefile index a7c277e4c..57a28bf3a 100644 --- a/Makefile +++ b/Makefile @@ -127,10 +127,9 @@ package: cp -RL web/static/help $(DIST_PATH)/web/static cp -RL web/static/images $(DIST_PATH)/web/static cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/ - cp -RL web/templates $(DIST_PATH)/web + cp -RL templates $(DIST_PATH) mkdir -p $(DIST_PATH)/api - cp -RL api/templates $(DIST_PATH)/api cp -RL i18n $(DIST_PATH) cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH) @@ -140,17 +139,17 @@ package: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - rm $(DIST_PATH)/web/templates/*.bak + sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + rm $(DIST_PATH)/templates/*.bak sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv' diff --git a/api/api.go b/api/api.go index 4fecd3dd4..20f77e558 100644 --- a/api/api.go +++ b/api/api.go @@ -4,47 +4,15 @@ package api import ( - "bytes" - l4g "github.com/alecthomas/log4go" + "net/http" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "html/template" - "net/http" _ "github.com/cloudfoundry/jibber_jabber" _ "github.com/nicksnyder/go-i18n/i18n" ) -var ServerTemplates *template.Template - -type ServerTemplatePage Page - -func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage { - return &ServerTemplatePage{ - TemplateName: templateName, - Props: make(map[string]string), - Extra: make(map[string]string), - Html: make(map[string]template.HTML), - ClientCfg: utils.ClientCfg, - Locale: locale, - } -} - -func (me *ServerTemplatePage) Render() string { - var text bytes.Buffer - - T := utils.GetUserTranslations(me.Locale) - me.Props["Footer"] = T("api.templates.email_footer") - me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", - map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]})) - - if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { - l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err) - } - - return text.String() -} - func InitApi() { r := Srv.Router.PathPrefix("/api/v1").Subrouter() InitUser(r) @@ -60,12 +28,7 @@ func InitApi() { InitPreference(r) InitLicense(r) - templatesDir := utils.FindDir("api/templates") - l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir) - var err error - if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("api.api.init.parsing_templates.error"), err) - } + utils.InitHTML() } func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool { diff --git a/api/context.go b/api/context.go index edcdcbfef..eed035daf 100644 --- a/api/context.go +++ b/api/context.go @@ -5,11 +5,9 @@ package api import ( "fmt" - "html/template" "net" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -31,33 +29,16 @@ var allowedMethods []string = []string{ } type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string - SessionTokenIndex int64 - T goi18n.TranslateFunc - Locale string -} - -type Page struct { - TemplateName string - Props map[string]string - Extra map[string]string - Html map[string]template.HTML - ClientCfg map[string]string - ClientLicense map[string]string - User *model.User - Team *model.Team - Channel *model.Channel - Preferences *model.Preferences - PostID string - SessionTokenIndex int64 - Locale string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + T goi18n.TranslateFunc + Locale string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - tokens := GetMultiSessionCookieTokens(r) - if len(tokens) > 0 { - // If there is only 1 token in the cookie then just use it like normal - if len(tokens) == 1 { - token = tokens[0] - } else { - // If it is a multi-session token then find the correct session - sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX) - sessionTokenIndex := int64(-1) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } else { - sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } - } - - if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) { - token = tokens[sessionTokenIndex] - c.SessionTokenIndex = sessionTokenIndex - } else { - c.SessionTokenIndex = -1 - } - } - } else { - c.SessionTokenIndex = -1 + if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + token = cookie.Value } } @@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w, r) - c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) - c.Err.StatusCode = http.StatusUnauthorized + if h.requireUser || h.requireSystemAdmin { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } } else if !session.IsOAuth && isTokenFromQueryString { c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token) c.Err.StatusCode = http.StatusUnauthorized @@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool { } func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - - // multiToken := "" - // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - // multiToken = oldMultiCookie.Value - // } - - // multiCookie := &http.Cookie{ - // Name: model.SESSION_COOKIE_TOKEN, - // Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), - // Path: "/", - // MaxAge: model.SESSION_TIME_WEB_IN_SECS, - // HttpOnly: true, - // } - - //http.SetCookie(w, multiCookie) - cookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, Value: "", @@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool { } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { - props := make(map[string]string) - props["Message"] = err.Message - props["Details"] = err.DetailedError + T, locale := utils.GetTranslationsAndLocale(w, r) + page := utils.NewHTMLTemplate("error", locale) + page.Props["Message"] = err.Message + page.Props["Details"] = err.DetailedError pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) > 1 { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] } else { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host } - T, _ := utils.GetTranslationsAndLocale(w, r) - props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - props["Link"] = T("api.templates.error.link") + page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + page.Props["Link"] = T("api.templates.error.link") w.WriteHeader(err.StatusCode) - ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) + if rErr := page.RenderToWriter(w); rErr != nil { + l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error()) + } } func Handle404(w http.ResponseWriter, r *http.Request) { @@ -588,29 +528,6 @@ func GetSession(token string) *model.Session { return session } -func GetMultiSessionCookieTokens(r *http.Request) []string { - if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - multiToken := multiCookie.Value - - if len(multiToken) > 0 { - return strings.Split(multiToken, " ") - } - } - - return []string{} -} - -func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) { - for index, token := range GetMultiSessionCookieTokens(r) { - s := GetSession(token) - if s != nil && !s.IsExpired() && s.TeamId == teamId { - return int64(index), s - } - } - - return -1, nil -} - func AddSessionToCache(session *model.Session) { sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/api/license.go b/api/license.go index 23e7946c8..542b45e26 100644 --- a/api/license.go +++ b/api/license.go @@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) { sr := r.PathPrefix("/license").Subrouter() sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST") sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST") + sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) { rdata["status"] = "ok" w.Write([]byte(model.MapToJson(rdata))) } + +func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) { + config := utils.ClientLicense + + var etag string + if config["IsLicensed"] == "false" { + etag = model.Etag(config["IsLicensed"]) + } else { + etag = model.Etag(config["IsLicensed"], config["IssuedAt"]) + } + + if HandleEtag(etag, w, r) { + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + + w.Write([]byte(model.MapToJson(config))) +} diff --git a/api/license_test.go b/api/license_test.go new file mode 100644 index 000000000..b34aeb7a6 --- /dev/null +++ b/api/license_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" +) + +func TestGetLicenceConfig(t *testing.T) { + Setup() + + if result, err := Client.GetClientLicenceConfig(); err != nil { + t.Fatal(err) + } else { + cfg := result.Data.(map[string]string) + + if _, ok := cfg["IsLicensed"]; !ok { + t.Fatal(cfg) + } + } +} diff --git a/api/oauth.go b/api/oauth.go index 1ae3dbf78..9b7f3699d 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -5,12 +5,15 @@ package api import ( "fmt" + "net/http" + "net/url" + "strconv" + "strings" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" - "net/url" ) func InitOAuth(r *mux.Router) { @@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) { sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + + // Also handle this a the old routes remove soon apiv2? + mr := Srv.Router + mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData { return result.Data.(*model.AuthData) } } + +func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" + + if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_LOGIN: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + } + } +} + +func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + responseType := r.URL.Query().Get("response_type") + clientId := r.URL.Query().Get("client_id") + redirect := r.URL.Query().Get("redirect_uri") + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := utils.NewHTMLTemplate("authorize", c.Locale) + page.Props["Title"] = c.T("web.authorize_oauth.title") + page.Props["TeamName"] = team.Name + page.Props["AppName"] = app.Name + page.Props["ResponseType"] = responseType + page.Props["ClientId"] = clientId + page.Props["RedirectUri"] = redirect + page.Props["Scope"] = scope + page.Props["State"] = state + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) + } +} + +func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + r.ParseForm() + + grantType := r.FormValue("grant_type") + if grantType != model.ACCESS_TOKEN_GRANT_TYPE { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := Srv.Store.OAuth().GetApp(clientId) + tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + uchan := Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") + return + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + c.LogAudit("fail - auth code is invalid") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") + return + } else if result.Data != nil { + c.LogAudit("fail - auth code has been used previously") + accessData := result.Data.(*model.AccessData) + + // Revoke access token, related auth code, and session from DB as well as from cache + if err := RevokeAccessToken(accessData.Token); err != nil { + l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) + } + + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") + return + } else { + session = result.Data.(*model.Session) + AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(accessRsp.ToJson())) +} + +func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + loginHint := r.URL.Query().Get("login_hint") + teamName := r.URL.Query().Get("team") + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} + +func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := r.URL.Query().Get("team") + + if !utils.Cfg.TeamSettings.EnableUserCreation { + c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + hash := r.URL.Query().Get("h") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if IsVerifyHashRequired(nil, team, hash) { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) + return + } + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} diff --git a/api/post.go b/api/post.go index cd78b16f0..d0ec5826a 100644 --- a/api/post.go +++ b/api/post.go @@ -419,7 +419,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team // copy the context and create a mock session for posting the message mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false} - newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { @@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * year := fmt.Sprintf("%d", tm.Year()) zone, _ := tm.Zone() - subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale) + subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale) subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, "Month": month[:3], "Day": day, "Year": year}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale) + bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name diff --git a/api/team.go b/api/team.go index 2f680dc76..255982522 100644 --- a/api/team.go +++ b/api/team.go @@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) { sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST") sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") - sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET") + sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") - sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") - sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") // These should be moved to the global admain console sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET") @@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signup_team_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title") bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button") @@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { } if !utils.Cfg.EmailSettings.RequireEmailVerification { - m["follow_link"] = bodyPage.Props["Link"] + m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash)) } w.Header().Set("Access-Control-Allow-Origin", " *") @@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name} w.Write([]byte(model.MapToJson(data))) } @@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool { } func getAll(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasSystemAdminPermissions("getLogs") { - return - } - if result := <-Srv.Store.Team().GetAll(); result.Err != nil { c.Err = result.Err return @@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { m := make(map[string]*model.Team) for _, v := range teams { m[v.Id] = v + if !c.IsSystemAdmin() { + m[v.Id].SanitizeForNotLoggedIn() + } } w.Write([]byte(model.TeamMapToJson(m))) @@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool { return false } -func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - m := make(map[string]*model.Team) - for _, v := range teams { - v.Sanitize() - m[v.Id] = v - } - - w.Write([]byte(model.TeamMapToJson(m))) - } -} - -func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - siteURL := c.GetSiteURL() - subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - - bodyPage := NewServerTemplatePage("find_teams_body", c.Locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title") - bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found") - bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found") - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - } else { - teams := result.Data.([]*model.Team) - - // the template expects Props to be a map with team names as the keys and the team url as the value - props := make(map[string]string) - for _, team := range teams { - props[team.Name] = c.GetTeamURLFromTeam(team) - } - bodyPage.Extra = props - - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.team.email_teams.sending.error"), err) - } - - w.Write([]byte(model.MapToJson(m))) - } -} - func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) if len(invites.Invites) == 0 { @@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = c.T("api.team.invite_members.member") } - subjectPage := NewServerTemplatePage("invite_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("invite_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.invite_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info", @@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { + m := model.MapFromJson(r.Body) + inviteId := m["invite_id"] + + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team := result.Data.(*model.Team) + if !(team.Type == model.TEAM_OPEN) { + c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId) + return + } + + result := map[string]string{} + result["display_name"] = team.DisplayName + result["name"] = team.Name + result["id"] = team.Id + w.Write([]byte(model.MapToJson(result))) + } +} diff --git a/api/team_test.go b/api/team_test.go index c942e2e1f..bbbc8385d 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -108,49 +108,36 @@ func TestCreateTeam(t *testing.T) { } } -func TestFindTeamByEmail(t *testing.T) { +func TestGetAllTeams(t *testing.T) { Setup() - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - if r1, err := Client.FindTeams(user.Email); err != nil { + Client.LoginByEmail(team.Name, user.Email, "pwd") + + enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing + defer func() { + *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks + }() + *utils.Cfg.TeamSettings.EnableTeamListing = true + + if r1, err := Client.GetAllTeams(); err != nil { t.Fatal(err) } else { teams := r1.Data.(map[string]*model.Team) if teams[team.Id].Name != team.Name { t.Fatal() } - if teams[team.Id].DisplayName != team.DisplayName { - t.Fatal() + if teams[team.Id].Email != "" { + t.Fatal("Non admin users shoudn't get full listings") } } - if _, err := Client.FindTeams("missing"); err != nil { - t.Fatal(err) - } -} - -func TestGetAllTeams(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.GetAllTeams(); err == nil { - t.Fatal("you shouldn't have permissions") - } - c := &Context{} c.RequestId = model.NewId() c.IpAddress = "cmd_line" @@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) { if teams[team.Id].Name != team.Name { t.Fatal() } + if teams[team.Id].Email != team.Email { + t.Fatal() + } } } @@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) { Client.ClearOAuthToken() } -/* - -XXXXXX investigate and fix failing test - -func TestFindTeamByDomain(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if val { - t.Fatal("shouldn't be a valid domain") - } - } -} - -*/ - -func TestFindTeamByEmailSend(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.FindTeamsSendEmail(user.Email); err != nil { - t.Fatal(err) - } else { - } - - if _, err := Client.FindTeamsSendEmail("missing"); err != nil { - - // It should actually succeed at sending the email since it doesn't exist - if !strings.Contains(err.DetailedError, "Failed to add to email address") { - t.Fatal(err) - } - } -} - func TestInviteMembers(t *testing.T) { Setup() diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html deleted file mode 100644 index 41b1bcd7d..000000000 --- a/api/templates/email_change_body.html +++ /dev/null @@ -1,41 +0,0 @@ -{{define "email_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html deleted file mode 100644 index afabc2191..000000000 --- a/api/templates/email_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html deleted file mode 100644 index 0d0c0aaba..000000000 --- a/api/templates/email_change_verify_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "email_change_verify_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.VerifyButton}} -

-
-
-
-
- -{{end}} diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html deleted file mode 100644 index 4fc4f4846..000000000 --- a/api/templates/email_change_verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_footer.html b/api/templates/email_footer.html deleted file mode 100644 index 6dc7fa483..000000000 --- a/api/templates/email_footer.html +++ /dev/null @@ -1,13 +0,0 @@ -{{define "email_footer"}} - - -

- -

-

- (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- {{.Props.Footer}} -

- - -{{end}} diff --git a/api/templates/email_info.html b/api/templates/email_info.html deleted file mode 100644 index 0a34f18a0..000000000 --- a/api/templates/email_info.html +++ /dev/null @@ -1,7 +0,0 @@ -{{define "email_info"}} - - - {{.Html.EmailInfo}} - - -{{end}} diff --git a/api/templates/error.html b/api/templates/error.html deleted file mode 100644 index 2f588aead..000000000 --- a/api/templates/error.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - <span class='fa fa-chevron-left'></span>Back - Error - - - - - - - - - - - - - - - - - -
-
-
-

{{.Props.Title}}

-

{{ .Props.Message }}

- {{.Props.Link}} -
-
- - - diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html deleted file mode 100644 index 1324091aa..000000000 --- a/api/templates/find_teams_body.html +++ /dev/null @@ -1,52 +0,0 @@ -{{define "find_teams_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{ if .Extra }} - {{.Props.Found}}
- {{range $index, $element := .Extra}} - {{ $index }}
- {{ end }} - {{ else }} - {{.Props.NotFound}} - {{ end }} -

-
-
-
-
- -{{end}} - - - diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html deleted file mode 100644 index ebc339562..000000000 --- a/api/templates/find_teams_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "find_teams_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html deleted file mode 100644 index 2b6bde6d3..000000000 --- a/api/templates/invite_body.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "invite_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-

- {{.Props.Button}} -

-
-

{{.Html.ExtraInfo}}

-
-
-
-
- -{{end}} diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html deleted file mode 100644 index 504915d50..000000000 --- a/api/templates/invite_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "invite_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html deleted file mode 100644 index 2c4ba10ca..000000000 --- a/api/templates/password_change_body.html +++ /dev/null @@ -1,43 +0,0 @@ -{{define "password_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} - - diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html deleted file mode 100644 index 897f1210d..000000000 --- a/api/templates/password_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "password_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html deleted file mode 100644 index 54f34d1dd..000000000 --- a/api/templates/post_body.html +++ /dev/null @@ -1,45 +0,0 @@ -{{define "post_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.BodyText}}

-

{{.Html.Info}}

{{.Props.PostMessage}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} - diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html deleted file mode 100644 index 60daaa432..000000000 --- a/api/templates/post_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html deleted file mode 100644 index 69cd44957..000000000 --- a/api/templates/reset_body.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "reset_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} - - diff --git a/api/templates/reset_subject.html b/api/templates/reset_subject.html deleted file mode 100644 index a2852d332..000000000 --- a/api/templates/reset_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "reset_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html deleted file mode 100644 index af8577f0f..000000000 --- a/api/templates/signin_change_body.html +++ /dev/null @@ -1,43 +0,0 @@ -{{define "signin_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} - - diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html deleted file mode 100644 index 606dc4df3..000000000 --- a/api/templates/signin_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "signin_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html deleted file mode 100644 index 683a9891e..000000000 --- a/api/templates/signup_team_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "signup_team_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

- {{.Props.Button}} -

- {{.Html.Info}}

-
-
-
-
- -{{end}} diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html deleted file mode 100644 index 413a5c8c1..000000000 --- a/api/templates/signup_team_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "signup_team_subject"}}{{.Props.Subject}}{{end}} \ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html deleted file mode 100644 index 2b0d25f94..000000000 --- a/api/templates/verify_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "verify_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html deleted file mode 100644 index ad7fc2aaa..000000000 --- a/api/templates/verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "verify_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html deleted file mode 100644 index b5ca9beb3..000000000 --- a/api/templates/welcome_body.html +++ /dev/null @@ -1,51 +0,0 @@ -{{define "welcome_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - {{if .Props.VerifyUrl }} - - - - {{end}} - - - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.Button}} -

-
-

{{.Props.Info2}}

- {{.Props.TeamURL}} -

{{.Props.Info3}}

-
-
-
-
- -{{end}} - diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html deleted file mode 100644 index 95189b900..000000000 --- a/api/templates/welcome_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "welcome_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/user.go b/api/user.go index b7e6220d8..0841c38aa 100644 --- a/api/user.go +++ b/api/user.go @@ -53,10 +53,13 @@ func InitUser(r *mux.Router) { sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") + sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") @@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) - bodyPage := NewServerTemplatePage("welcome_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") @@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN bodyPage.Props["TeamURL"] = teamURL if !verified { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) bodyPage.Props["VerifyUrl"] = link } @@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) { func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") @@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, w.Header().Set(model.HEADER_TOKEN, session.Token) - tokens := GetMultiSessionCookieTokens(r) - multiToken := "" - seen := make(map[string]string) - seen[session.TeamId] = session.TeamId - for _, token := range tokens { - s := GetSession(token) - if s != nil && !s.IsExpired() && seen[s.TeamId] == "" { - multiToken += " " + token - seen[s.TeamId] = s.TeamId - } - } - - multiToken = strings.TrimSpace(multiToken + " " + session.Token) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) - - multiSessionCookie := &http.Cookie{ + sessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, - Value: multiToken, + Value: session.Token, Path: "/", MaxAge: maxAge, Expires: expiresAt, HttpOnly: true, } - http.SetCookie(w, multiSessionCookie) + http.SetCookie(w, sessionCookie) c.Session = *session c.LogAuditWithUserId(user.Id, "success") @@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) { + data := make(map[string]string) + data["logged_in"] = "false" + data["team_name"] = "" + + if len(c.Session.UserId) != 0 { + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + } + data["logged_in"] = "true" + data["team_name"] = team.Name + } + w.Write([]byte(model.MapToJson(data))) +} + func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] @@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) - link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) + link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") - bodyPage := NewServerTemplatePage("reset_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.reset_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info")) @@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("password_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", @@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", @@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) - subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title") bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info", @@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string props["team"] = teamName state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) + redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signin_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", @@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, }() } + +func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["uid"] + if len(userId) != 26 { + c.SetInvalidParam("verifyEmail", "uid") + return + } + + hashedId := props["hid"] + if len(hashedId) == 0 { + c.SetInvalidParam("verifyEmail", "hid") + return + } + + if model.ComparePassword(hashedId, userId) { + if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { + return + } else { + c.LogAudit("Email Verified") + return + } + } + + c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden +} + +func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("resendVerification", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("resendVerification", "email") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) + + if user.LastActivityAt > 0 { + SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } else { + SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } + } +} diff --git a/api/user_test.go b/api/user_test.go index 1a1cf9634..27f00829f 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) { t.Fatal("should have failed - wrong user") } } + +func TestMeLoggedIn(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.AuthToken = "invalid" + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" { + t.Fatal("Got: " + val) + } + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" { + t.Fatal("Got: " + val) + } + } +} diff --git a/i18n/en.json b/i18n/en.json index bc33fc019..d16de7dbb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1623,6 +1623,10 @@ "id": "api.user.upload_profile_user.upload_profile.app_error", "translation": "Couldn't upload profile image" }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Bad verify email link." + }, { "id": "api.web_conn.new_web_conn.last_activity.error", "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" @@ -3399,22 +3403,6 @@ "id": "web.find_team.title", "translation": "Find Team" }, - { - "id": "web.footer.about", - "translation": "About" - }, - { - "id": "web.footer.help", - "translation": "Help" - }, - { - "id": "web.footer.privacy", - "translation": "Privacy" - }, - { - "id": "web.footer.terms", - "translation": "Terms" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: Bad client_id" @@ -3547,10 +3535,6 @@ "id": "web.root.home_title", "translation": "Home" }, - { - "id": "web.root.singup_info", - "translation": "All team communication in one place, searchable and accessible anywhere" - }, { "id": "web.root.singup_title", "translation": "Signup" @@ -3606,5 +3590,9 @@ { "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Invite is invalid because this is not an open team." } ] diff --git a/i18n/es.json b/i18n/es.json index 4c0c1fd03..93ffb2341 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -3399,22 +3399,6 @@ "id": "web.find_team.title", "translation": "Encontrar Equipo" }, - { - "id": "web.footer.about", - "translation": "Acerca" - }, - { - "id": "web.footer.help", - "translation": "Ayuda" - }, - { - "id": "web.footer.privacy", - "translation": "Privacidad" - }, - { - "id": "web.footer.terms", - "translation": "Términos" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: client_id malo" @@ -3547,10 +3531,6 @@ "id": "web.root.home_title", "translation": "Inicio" }, - { - "id": "web.root.singup_info", - "translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" - }, { "id": "web.root.singup_title", "translation": "Registrar" diff --git a/model/client.go b/model/client.go index 560e47b76..3adcb980d 100644 --- a/model/client.go +++ b/model/client.go @@ -16,19 +16,17 @@ import ( ) const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" - SESSION_TOKEN_INDEX = "session_token_index" - API_URL_SUFFIX = "/api/v1" + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro } } -func (c *Client) FindTeams(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { - return nil, err - } else { - - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil { - return nil, err - } else { - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil - } -} - func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err @@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (* } func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { - if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { + if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER } + +func (c *Client) GetClientLicenceConfig() (*Result, *AppError) { + if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetMeLoggedIn() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} diff --git a/model/session.go b/model/session.go index 5d9424d64..bf0d9531e 100644 --- a/model/session.go +++ b/model/session.go @@ -9,7 +9,7 @@ import ( ) const ( - SESSION_COOKIE_TOKEN = "MMTOKEN" + SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" SESSION_PROP_OS = "os" diff --git a/model/team.go b/model/team.go index 9e9eaa25f..bed7bbd8d 100644 --- a/model/team.go +++ b/model/team.go @@ -232,3 +232,10 @@ func (o *Team) Sanitize() { o.Email = "" o.AllowedDomains = "" } + +func (o *Team) SanitizeForNotLoggedIn() { + o.Email = "" + o.AllowedDomains = "" + o.CompanyName = "" + o.InviteId = "" +} diff --git a/templates/authorize.html b/templates/authorize.html new file mode 100644 index 000000000..0fa36b0ab --- /dev/null +++ b/templates/authorize.html @@ -0,0 +1,12 @@ +{{define "authorize"}} + +{{template "head" . }} + +
+
+ + + +{{end}} diff --git a/templates/email_change_body.html b/templates/email_change_body.html new file mode 100644 index 000000000..41b1bcd7d --- /dev/null +++ b/templates/email_change_body.html @@ -0,0 +1,41 @@ +{{define "email_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html new file mode 100644 index 000000000..540bc6eab --- /dev/null +++ b/templates/email_change_subject.html @@ -0,0 +1 @@ +{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html new file mode 100644 index 000000000..0d0c0aaba --- /dev/null +++ b/templates/email_change_verify_body.html @@ -0,0 +1,44 @@ +{{define "email_change_verify_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.VerifyButton}} +

+
+
+
+
+ +{{end}} diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html new file mode 100644 index 000000000..04da7593c --- /dev/null +++ b/templates/email_change_verify_subject.html @@ -0,0 +1 @@ +{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/email_footer.html b/templates/email_footer.html new file mode 100644 index 000000000..6dc7fa483 --- /dev/null +++ b/templates/email_footer.html @@ -0,0 +1,13 @@ +{{define "email_footer"}} + + +

+ +

+

+ (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
+ {{.Props.Footer}} +

+ + +{{end}} diff --git a/templates/email_info.html b/templates/email_info.html new file mode 100644 index 000000000..0a34f18a0 --- /dev/null +++ b/templates/email_info.html @@ -0,0 +1,7 @@ +{{define "email_info"}} + + + {{.Html.EmailInfo}} + + +{{end}} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 000000000..b86039ca3 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,24 @@ +{{define "error"}} + + +{{template "head" . }} + +
+
+
+ +
+

{{.Props.Title}}

+

{{ .Props.Message }}

+ {{.Props.Link}} +
+
+ + + +{{end}} diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 000000000..a7eacc85f --- /dev/null +++ b/templates/head.html @@ -0,0 +1,92 @@ +{{define "head"}} + + + + + + {{ .Props.Title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/templates/invite_body.html b/templates/invite_body.html new file mode 100644 index 000000000..2b6bde6d3 --- /dev/null +++ b/templates/invite_body.html @@ -0,0 +1,46 @@ +{{define "invite_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+

+ {{.Props.Button}} +

+
+

{{.Html.ExtraInfo}}

+
+
+
+
+ +{{end}} diff --git a/templates/invite_subject.html b/templates/invite_subject.html new file mode 100644 index 000000000..504915d50 --- /dev/null +++ b/templates/invite_subject.html @@ -0,0 +1 @@ +{{define "invite_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/password_change_body.html b/templates/password_change_body.html new file mode 100644 index 000000000..2c4ba10ca --- /dev/null +++ b/templates/password_change_body.html @@ -0,0 +1,43 @@ +{{define "password_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} + + diff --git a/templates/password_change_subject.html b/templates/password_change_subject.html new file mode 100644 index 000000000..897f1210d --- /dev/null +++ b/templates/password_change_subject.html @@ -0,0 +1 @@ +{{define "password_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/post_body.html b/templates/post_body.html new file mode 100644 index 000000000..54f34d1dd --- /dev/null +++ b/templates/post_body.html @@ -0,0 +1,45 @@ +{{define "post_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.BodyText}}

+

{{.Html.Info}}

{{.Props.PostMessage}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} + diff --git a/templates/post_subject.html b/templates/post_subject.html new file mode 100644 index 000000000..9789d4142 --- /dev/null +++ b/templates/post_subject.html @@ -0,0 +1 @@ +{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/reset_body.html b/templates/reset_body.html new file mode 100644 index 000000000..69cd44957 --- /dev/null +++ b/templates/reset_body.html @@ -0,0 +1,46 @@ +{{define "reset_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} + + diff --git a/templates/reset_subject.html b/templates/reset_subject.html new file mode 100644 index 000000000..a2852d332 --- /dev/null +++ b/templates/reset_subject.html @@ -0,0 +1 @@ +{{define "reset_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/root.html b/templates/root.html new file mode 100644 index 000000000..560c7a4b0 --- /dev/null +++ b/templates/root.html @@ -0,0 +1,12 @@ +{{define "root"}} + + +{{template "head" . }} + +
+ + + +{{end}} diff --git a/templates/signin_change_body.html b/templates/signin_change_body.html new file mode 100644 index 000000000..af8577f0f --- /dev/null +++ b/templates/signin_change_body.html @@ -0,0 +1,43 @@ +{{define "signin_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} + + diff --git a/templates/signin_change_subject.html b/templates/signin_change_subject.html new file mode 100644 index 000000000..606dc4df3 --- /dev/null +++ b/templates/signin_change_subject.html @@ -0,0 +1 @@ +{{define "signin_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/signup_team_body.html b/templates/signup_team_body.html new file mode 100644 index 000000000..683a9891e --- /dev/null +++ b/templates/signup_team_body.html @@ -0,0 +1,44 @@ +{{define "signup_team_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

+ {{.Props.Button}} +

+ {{.Html.Info}}

+
+
+
+
+ +{{end}} diff --git a/templates/signup_team_subject.html b/templates/signup_team_subject.html new file mode 100644 index 000000000..413a5c8c1 --- /dev/null +++ b/templates/signup_team_subject.html @@ -0,0 +1 @@ +{{define "signup_team_subject"}}{{.Props.Subject}}{{end}} \ No newline at end of file diff --git a/templates/verify_body.html b/templates/verify_body.html new file mode 100644 index 000000000..2b0d25f94 --- /dev/null +++ b/templates/verify_body.html @@ -0,0 +1,44 @@ +{{define "verify_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} diff --git a/templates/verify_subject.html b/templates/verify_subject.html new file mode 100644 index 000000000..ad7fc2aaa --- /dev/null +++ b/templates/verify_subject.html @@ -0,0 +1 @@ +{{define "verify_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/welcome_body.html b/templates/welcome_body.html new file mode 100644 index 000000000..b5ca9beb3 --- /dev/null +++ b/templates/welcome_body.html @@ -0,0 +1,51 @@ +{{define "welcome_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + {{if .Props.VerifyUrl }} + + + + {{end}} + + + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.Button}} +

+
+

{{.Props.Info2}}

+ {{.Props.TeamURL}} +

{{.Props.Info3}}

+
+
+
+
+ +{{end}} + diff --git a/templates/welcome_subject.html b/templates/welcome_subject.html new file mode 100644 index 000000000..95189b900 --- /dev/null +++ b/templates/welcome_subject.html @@ -0,0 +1 @@ +{{define "welcome_subject"}}{{.Props.Subject}}{{end}} diff --git a/utils/html.go b/utils/html.go new file mode 100644 index 000000000..4203160d5 --- /dev/null +++ b/utils/html.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "bytes" + "html/template" + "net/http" + + l4g "github.com/alecthomas/log4go" + "gopkg.in/fsnotify.v1" +) + +// Global storage for templates +var htmlTemplates *template.Template + +type HTMLTemplate struct { + TemplateName string + Props map[string]string + Html map[string]template.HTML + Locale string +} + +func InitHTML() { + templatesDir := FindDir("templates") + l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) + var err error + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("api.api.init.parsing_templates.error"), err) + } + + // Watch the templates folder for changes. + watcher, err := fsnotify.NewWatcher() + if err != nil { + l4g.Error(T("web.create_dir.error"), err) + } + + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + l4g.Info(T("web.reparse_templates.info"), event.Name) + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("web.parsing_templates.error"), err) + } + } + case err := <-watcher.Errors: + l4g.Error(T("web.dir_fail.error"), err) + } + } + }() + + err = watcher.Add(templatesDir) + if err != nil { + l4g.Error(T("web.watcher_fail.error"), err) + } +} + +func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate { + return &HTMLTemplate{ + TemplateName: templateName, + Props: make(map[string]string), + Html: make(map[string]template.HTML), + Locale: locale, + } +} + +func (t *HTMLTemplate) addDefaultProps() { + T := GetUserTranslations(t.Locale) + t.Props["Footer"] = T("api.templates.email_footer") + t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", + map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName})) +} + +func (t *HTMLTemplate) Render() string { + t.addDefaultProps() + + var text bytes.Buffer + + if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + } + + return text.String() +} + +func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { + t.addDefaultProps() + + if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + return err + } + return nil +} diff --git a/utils/license.go b/utils/license.go index 5c975aec2..b1f15ad92 100644 --- a/utils/license.go +++ b/utils/license.go @@ -20,7 +20,7 @@ import ( var IsLicensed bool = false var License *model.License = &model.License{} -var ClientLicense map[string]string = make(map[string]string) +var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r diff --git a/web/react/action_creators/global_actions.jsx b/web/react/action_creators/global_actions.jsx new file mode 100644 index 000000000..4375d6c87 --- /dev/null +++ b/web/react/action_creators/global_actions.jsx @@ -0,0 +1,252 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PostStore from '../stores/post_store.jsx'; +import SearchStore from '../stores/search_store.jsx'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import * as AsyncClient from '../utils/async_client.jsx'; +import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; + +export function emitChannelClickEvent(channel) { + AsyncClient.getChannels(true); + AsyncClient.getChannelExtraInfo(channel.id); + AsyncClient.updateLastViewedAt(channel.id); + AsyncClient.getPosts(channel.id); + + AppDispatcher.handleViewAction({ + type: ActionTypes.CLICK_CHANNEL, + name: channel.name, + id: channel.id, + prev: ChannelStore.getCurrentId() + }); +} + +export function emitPostFocusEvent(postId) { + Client.getPostById( + postId, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_FOCUSED_POST, + postId, + post_list: data + }); + + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + } + ); +} + +export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { + Client.getPost( + post.channel_id, + post.id, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: post.channel_id, + numRequested: 0, + post_list: data + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: Utils.getRootId(post), + from_search: SearchStore.getSearchTerm() + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: null, + is_mention_search: isMentionSearch + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getPost'); + } + ); +} + +export function emitLoadMorePostsEvent() { + const id = ChannelStore.getCurrentId(); + loadMorePostsTop(id); +} + +export function emitLoadMorePostsFocusedTopEvent() { + const id = PostStore.getFocusedPostId(); + loadMorePostsTop(id); +} + +export function loadMorePostsTop(id) { + const earliestPostId = PostStore.getEarliestPost(id).id; + if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { + AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); + } +} + +export function emitLoadMorePostsFocusedBottomEvent() { + const id = PostStore.getFocusedPostId(); + const latestPostId = PostStore.getLatestPost(id).id; + AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); +} + +export function emitPostRecievedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST, + post + }); +} + +export function emitUserPostedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.CREATE_POST, + post + }); +} + +export function emitPostDeletedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.POST_DELETED, + post + }); +} + +export function showDeletePostModal(post, commentCount = 0) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_DELETE_POST_MODAL, + value: true, + post, + commentCount + }); +} + +export function showGetPostLinkModal(post) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, + value: true, + post + }); +} + +export function showGetTeamInviteLinkModal() { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, + value: true + }); +} + +export function showInviteMemberModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, + value: true + }); +} + +export function showRegisterAppModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, + value: true + }); +} + +export function emitSuggestionPretextChanged(suggestionId, pretext) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, + id: suggestionId, + pretext + }); +} + +export function emitSelectNextSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_NEXT, + id: suggestionId + }); +} + +export function emitSelectPreviousSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, + id: suggestionId + }); +} + +export function emitCompleteWordSuggestion(suggestionId, term = '') { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, + id: suggestionId, + term + }); +} + +export function emitClearSuggestions(suggestionId) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, + id: suggestionId + }); +} + +export function emitPreferenceChangedEvent(preference) { + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECEIVED_PREFERENCE, + preference + }); +} + +export function emitRemovePost(post) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.REMOVE_POST, + post + }); +} + +export function sendEphemeralPost(message, channelId) { + const timestamp = Utils.getTimestamp(); + const post = { + id: Utils.generateId(), + user_id: '0', + channel_id: channelId || ChannelStore.getCurrentId(), + message, + type: Constants.POST_TYPE_EPHEMERAL, + create_at: timestamp, + update_at: timestamp, + filenames: [], + props: {} + }; + + emitPostRecievedEvent(post); +} + +export function loadTeamRequiredPage() { + AsyncClient.getAllTeams(); +} + +export function newLocalizationSelected(locale) { + Client.getTranslations( + locale, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_LOCALE, + locale, + translations: data + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTranslations'); + } + ); +} + +export function viewLoggedIn() { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.getMyTeam(); + AsyncClient.getMe(); + + // Clear pending posts (shouldn't have pending posts if we are loading) + PostStore.clearPendingPosts(); +} diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 95b4caa12..db366f8ed 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal; import LoadingScreen from './loading_screen.jsx'; import * as Utils from '../utils/utils.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; export default class ActivityLogModal extends React.Component { constructor(props) { @@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.firstTime' defaultMessage='First time active: {date}, {time}' values={{ - date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} />
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.lastActivity' defaultMessage='Last activity: {date}, {time}' values={{ - date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} /> diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 32ed70a99..4c4f21f08 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx'; import TeamStore from '../../stores/team_store.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import LoadingScreen from '../loading_screen.jsx'; -import * as Utils from '../../utils/utils.jsx'; import EmailSettingsTab from './email_settings.jsx'; import LogSettingsTab from './log_settings.jsx'; @@ -50,11 +49,6 @@ export default class AdminController extends React.Component { selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; - - if (!props.tab) { - var tokenIndex = Utils.getUrlParameter('session_token_index'); - history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`); - } } componentDidMount() { @@ -63,6 +57,9 @@ export default class AdminController extends React.Component { AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); AsyncClient.getAllTeams(); + + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover(); } componentWillUnmount() { @@ -175,7 +172,7 @@ export default class AdminController extends React.Component { } return ( -
+
+ ); } } @@ -37,4 +14,5 @@ ChannelView.defaultProps = { }; ChannelView.propTypes = { + params: React.PropTypes.object }; diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx index 5b3b584ee..42fd8dafa 100644 --- a/web/react/components/claim/claim_account.jsx +++ b/web/react/components/claim/claim_account.jsx @@ -3,6 +3,7 @@ import EmailToSSO from './email_to_sso.jsx'; import SSOToEmail from './sso_to_email.jsx'; +import TeamStore from '../../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component { constructor(props) { super(props); + this.onTeamChange = this.onTeamChange.bind(this); + this.updateStateFromStores = this.updateStateFromStores.bind(this); + this.state = {}; } + componentWillMount() { + this.setState({ + email: this.props.location.query.email, + newType: this.props.location.query.new_type, + oldType: this.props.location.query.old_type, + teamName: this.props.params.team, + teamDisplayName: '' + }); + this.updateStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + updateStateFromStores() { + const team = TeamStore.getByName(this.state.teamName); + let displayName = ''; + if (team) { + displayName = team.displayName; + } + this.setState({ + teamDisplayName: displayName + }); + } + onTeamChange() { + this.updateStateFromStores(); + } render() { + if (this.state.teamDisplayName === '') { + return (
); + } let content; - if (this.props.email === '') { + if (this.state.email === '') { content = (

); - } else if (this.props.currentType === '' && this.props.newType !== '') { + } else if (this.state.oldType === '' && this.state.newType !== '') { content = ( ); } else { content = ( ); } - return content; + return ( +
+ +
+
+ +
+ {content} +
+
+
+
+ ); } } ClaimAccount.defaultProps = { }; ClaimAccount.propTypes = { - currentType: React.PropTypes.string.isRequired, - newType: React.PropTypes.string.isRequired, - email: React.PropTypes.string.isRequired, - teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx index 74137082a..a16efb57b 100644 --- a/web/react/components/claim/sso_to_email.jsx +++ b/web/react/components/claim/sso_to_email.jsx @@ -159,7 +159,7 @@ SSOToEmail.propTypes = { currentType: React.PropTypes.string.isRequired, email: React.PropTypes.string.isRequired, teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + teamDisplayName: React.PropTypes.string }; export default injectIntl(SSOToEmail); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 62319b1a7..69cc74842 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import * as Utils from '../utils/utils.jsx'; @@ -165,7 +165,7 @@ class CreatePost extends React.Component { const channel = ChannelStore.get(this.state.channelId); - EventHelpers.emitUserPostedEvent(post); + GlobalActions.emitUserPostedEvent(post); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); Client.createPost(post, channel, @@ -177,7 +177,7 @@ class CreatePost extends React.Component { member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); - EventHelpers.emitPostRecievedEvent(data); + GlobalActions.emitPostRecievedEvent(data); }, (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index d9113bc9f..70e7a67a8 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; + export default class DeleteChannelModal extends React.Component { constructor(props) { super(props); @@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component { return; } + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); Client.deleteChannel( this.props.channel.id, () => { AsyncClient.getChannels(true); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; }, (err) => { AsyncClient.dispatchError(err, 'handleDelete'); diff --git a/web/react/components/do_verify_email.jsx b/web/react/components/do_verify_email.jsx new file mode 100644 index 000000000..df98bf463 --- /dev/null +++ b/web/react/components/do_verify_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; +import LoadingScreen from './loading_screen.jsx'; + +import {browserHistory} from 'react-router'; + +export default class DoVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.state = { + verifyStatus: 'pending', + serverError: '' + }; + } + componentWillMount() { + const uid = this.props.location.query.uid; + const hid = this.props.location.query.hid; + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + Client.verifyEmail( + () => { + browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email); + }, + (err) => { + this.setState({verifyStatus: 'failure', serverError: err.message}); + }, + uid, + hid + ); + } + render() { + if (this.state.verifyStatus !== 'failure') { + return (); + } + + return ( +
+ +
+
+

+ +

+
+

+ +

+

+ + {this.state.serverError} +

+
+
+
+
+ ); + } +} + +DoVerifyEmail.defaultProps = { +}; +DoVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx deleted file mode 100644 index 6d3a109c2..000000000 --- a/web/react/components/docs.jsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as TextFormatting from '../utils/text_formatting.jsx'; -import UserStore from '../stores/user_store.jsx'; - -export default class Docs extends React.Component { - constructor(props) { - super(props); - UserStore.setCurrentUser(global.window.mm_user || {}); - - this.state = {text: ''}; - const errorState = {text: '## 404'}; - - if (props.site) { - $.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => { - this.setState({text: response}); - }, () => { - this.setState(errorState); - }); - } else { - this.setState(errorState); - } - } - - render() { - return ( -
-
- ); - } -} - -Docs.defaultProps = { - site: '' -}; -Docs.propTypes = { - site: React.PropTypes.string -}; diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 380ca7bde..f02239fcf 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,7 +3,7 @@ import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Textbox from './textbox.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import PostStore from '../stores/post_store.jsx'; @@ -45,7 +45,7 @@ class EditPostModal extends React.Component { delete tempState.editText; BrowserStore.setItem('edit_state_transfer', tempState); $('#edit_post').modal('hide'); - EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); + GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); return; } diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx deleted file mode 100644 index 702a20eba..000000000 --- a/web/react/components/email_verify.jsx +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -export default class EmailVerify extends React.Component { - constructor(props) { - super(props); - - this.handleResend = this.handleResend.bind(this); - - this.state = {}; - } - handleResend() { - const newAddress = window.location.href.replace('&resend_success=true', ''); - window.location.href = newAddress + '&resend=true'; - } - render() { - var title = ''; - var body = ''; - var resend = ''; - var resendConfirm = ''; - if (this.props.isVerified === 'true') { - title = ( - - ); - body = ( - - ); - body = ( -

- -

- ); - resend = ( - - ); - if (this.props.resendSuccess) { - resendConfirm = ( -

- -

); - } - } - - return ( -
-
-

{title}

-
- {body} - {resend} - {resendConfirm} -
-
-
- ); - } -} - -EmailVerify.defaultProps = { - isVerified: 'false', - teamURL: '', - userEmail: '', - resendSuccess: 'false' -}; -EmailVerify.propTypes = { - isVerified: React.PropTypes.string, - teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string, - resendSuccess: React.PropTypes.string -}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 2f6067b86..8abcac8c3 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -43,7 +43,7 @@ class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { + $('').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -114,7 +114,7 @@ class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); } } removeBackgroundImage(name) { diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx deleted file mode 100644 index 3ff9787ad..000000000 --- a/web/react/components/find_team.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -var holders = defineMessages({ - submitError: { - id: 'find_team.submitError', - defaultMessage: 'Please enter a valid email address' - }, - placeholder: { - id: 'find_team.placeholder', - defaultMessage: 'you@domain.com' - } -}); - -class FindTeam extends React.Component { - constructor(props) { - super(props); - this.state = {}; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - - var state = { }; - - var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!email || !utils.isEmail(email)) { - state.email_error = this.props.intl.formatMessage(holders.submitError); - this.setState(state); - return; - } - - state.email_error = ''; - - client.findTeamsSendEmail(email, - function success() { - state.sent = true; - this.setState(state); - }.bind(this), - function fail(err) { - state.email_error = err.message; - this.setState(state); - }.bind(this) - ); - } - - render() { - var emailError = null; - var emailErrorClass = 'form-group'; - - if (this.state.email_error) { - emailError = ; - emailErrorClass = 'form-group has-error'; - } - - if (this.state.sent) { - return ( -
-

- -

-

- -

-
- ); - } - - return ( -
-

- -

-
-

- -

-
- -
- - {emailError} -
-
- -
-
- ); - } -} - -FindTeam.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(FindTeam); \ No newline at end of file diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 184ba1357..71cd5b8b6 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as Client from '../utils/client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ModalStore from '../stores/modal_store.jsx'; import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component { showGetTeamInviteLinkModal() { this.handleHide(false); - EventHelpers.showGetTeamInviteLinkModal(); + GlobalActions.showGetTeamInviteLinkModal(); } render() { diff --git a/web/react/components/logged_in.jsx b/web/react/components/logged_in.jsx new file mode 100644 index 000000000..1ed3694e9 --- /dev/null +++ b/web/react/components/logged_in.jsx @@ -0,0 +1,224 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as AsyncClient from '../utils/async_client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import UserStore from '../stores/user_store.jsx'; +import SocketStore from '../stores/socket_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; +import ErrorBar from '../components/error_bar.jsx'; + +import {browserHistory} from 'react-router'; + +import SidebarRight from '../components/sidebar_right.jsx'; +import SidebarRightMenu from '../components/sidebar_right_menu.jsx'; + +// Modals +import GetPostLinkModal from '../components/get_post_link_modal.jsx'; +import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; +import EditPostModal from '../components/edit_post_modal.jsx'; +import DeletePostModal from '../components/delete_post_modal.jsx'; +import MoreChannelsModal from '../components/more_channels.jsx'; +import TeamSettingsModal from '../components/team_settings_modal.jsx'; +import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; +import RegisterAppModal from '../components/register_app_modal.jsx'; +import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; +import InviteMemberModal from '../components/invite_member_modal.jsx'; +import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; + +const CLIENT_STATUS_INTERVAL = 30000; +const BACKSPACE_CHAR = 8; + +export default class LoggedIn extends React.Component { + constructor(params) { + super(params); + + this.onUserChanged = this.onUserChanged.bind(this); + } + onUserChanged() { + // Grab the current user + const user = UserStore.getCurrentUser(); + + // Update segment indentify + if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') { + global.window.analytics.identify(user.id, { + name: user.nickname, + email: user.email, + createdAt: user.create_at, + username: user.username, + team_id: user.team_id, + id: user.id + }); + } + + // Update CSS classes to match user theme + if (user) { + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + Utils.applyTheme(user.theme_props); + } else { + Utils.applyTheme(Constants.THEMES.default); + } + } + } + onSocketChange(msg) { + if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { + UserStore.setStatus(msg.user_id, 'online'); + } + } + componentWillMount() { + // Emit view action + GlobalActions.viewLoggedIn(); + + // Listen for user + UserStore.addChangeListener(this.onUserChanged); + + // Add listner for socker store + SocketStore.addChangeListener(this.onSocketChange); + + // Get all statuses regularally. (Soon to be switched to websocket) + this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL); + + // Force logout of all tabs if one tab is logged out + $(window).bind('storage', (e) => { + // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out + if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + return; + } + + console.log('detected logout from a different tab'); //eslint-disable-line no-console + browserHistory.push('/' + this.props.params.team); + } + + if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { + return; + } + + console.log('detected login from a different tab'); //eslint-disable-line no-console + location.reload(); + } + }); + + // Because current CSS requires the root tag to have specific stuff + $('#root').attr('class', 'channel-view'); + + // ??? + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); + } else { + $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); + } + }); + + // Device tracking setup + var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); + if (iOS) { + $('body').addClass('ios'); + } + + // Set up tracking for whether the window is active + window.isActive = true; + $(window).on('focus', () => { + AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); + window.isActive = true; + }); + $(window).on('blur', () => { + window.isActive = false; + }); + + // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx + const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT); + Utils.applyFont(selectedFont); + + // Pervent backspace from navigating back a page + $(window).on('keydown.preventBackspace', (e) => { + if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) { + e.preventDefault(); + } + }); + } + componentWillUnmount() { + $('#root').attr('class', ''); + clearInterval(this.intervalId); + + $(window).off('focus'); + $(window).off('blur'); + + SocketStore.removeChangeListener(this.onSocketChange); + UserStore.removeChangeListener(this.onUserChanged); + + $('body').off('click.userpopover'); + $('body').off('mouseenter mouseleave', '.post'); + $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); + + $('.modal').off('show.bs.modal'); + + $(window).off('keydown.preventBackspace'); + } + render() { + return ( +
+ +
+ + + {this.props.sidebar} + {this.props.center} + + + + + + + + + + + + +
+
+ ); + } +} + +LoggedIn.defaultProps = { +}; + +LoggedIn.propTypes = { + children: React.PropTypes.object, + sidebar: React.PropTypes.object, + center: React.PropTypes.object, + params: React.PropTypes.object +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 581b8e0b5..30c8ffe4f 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,82 +6,118 @@ import LoginUsername from './login_username.jsx'; import LoginLdap from './login_ldap.jsx'; import * as Utils from '../utils/utils.jsx'; +import * as Client from '../utils/client.jsx'; import Constants from '../utils/constants.jsx'; +import TeamStore from '../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; export default class Login extends React.Component { constructor(props) { super(props); - this.state = {}; + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = this.getStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + Client.getMeLoggedIn((data) => { + if (data && data.logged_in !== 'false') { + browserHistory.push('/' + this.props.params.team + '/channels/town-square'); + } + }); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + getStateFromStores() { + return { + currentTeam: TeamStore.getByName(this.props.params.team) + }; + } + onTeamChange() { + this.setState(this.getStateFromStores()); } render() { - const teamDisplayName = this.props.teamDisplayName; - const teamName = this.props.teamName; + const currentTeam = this.state.currentTeam; + if (currentTeam == null) { + return
; + } + + const teamDisplayName = currentTeam.display_name; + const teamName = currentTeam.name; let loginMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( - - - - - - + + + + + + ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { loginMessage.push( - - - - - - - ); + + + + + + + ); } const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { - let msg; if (extraParam === Constants.SIGNIN_CHANGE) { - msg = ( - + extraBox = ( +
+ + +
); } else if (extraParam === Constants.SIGNIN_VERIFIED) { - msg = ( - - ); - } - - if (msg != null) { extraBox = (
- {msg} + +
+ ); + } else if (extraParam === Constants.SESSION_EXPIRED) { + extraBox = ( +
+ +
); } @@ -91,7 +127,7 @@ export default class Login extends React.Component { if (global.window.mm_config.EnableSignInWithEmail === 'true') { emailSignup = ( ); } @@ -125,7 +161,7 @@ export default class Login extends React.Component { } let userSignUp = null; - if (this.props.inviteId) { + if (currentTeam.allow_open_invite) { userSignUp = (
@@ -134,7 +170,7 @@ export default class Login extends React.Component { defaultMessage="Don't have an account? " /> ); } - let findTeams = null; - if (!Utils.isMobileApp()) { - findTeams = ( - - ); - } - let usernameLogin = null; if (global.window.mm_config.EnableSignInWithUsername === 'true') { usernameLogin = ( ); } return ( -
-
- -
-

{teamDisplayName}

-

- -

- {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {findTeams} - {forgotPassword} - {teamSignUp} +
+ +
+
+
+ +
+

{teamDisplayName}

+

+ +

+ {extraBox} + {loginMessage} + {emailSignup} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} +
+
); } } Login.defaultProps = { - teamName: '', - teamDisplayName: '' }; Login.propTypes = { - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - inviteId: React.PropTypes.string + params: React.PropTypes.object.isRequired }; diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx index cf1e1bc40..3e0d8919d 100644 --- a/web/react/components/login_email.jsx +++ b/web/react/components/login_email.jsx @@ -4,6 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; import UserStore from '../stores/user_store.jsx'; +import {browserHistory} from 'react-router'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; @@ -72,13 +73,7 @@ class LoginEmail extends React.Component { Client.loginByEmail(name, email, password, () => { UserStore.setLastEmail(email); - - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - window.location.href = decodeURIComponent(redirect); - } else { - window.location.href = '/' + name + '/channels/town-square'; - } + browserHistory.push('/' + name + '/channels/town-square'); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { @@ -167,4 +162,4 @@ LoginEmail.propTypes = { teamName: React.PropTypes.string.isRequired }; -export default injectIntl(LoginEmail); \ No newline at end of file +export default injectIntl(LoginEmail); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 93fe6c05a..974f026d0 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -56,9 +56,13 @@ export default class Navbar extends React.Component { return { channel: ChannelStore.getCurrent(), member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members + users: ChannelStore.getCurrentExtraInfo().members, + currentUser: UserStore.getCurrentUser() }; } + stateValid() { + return this.state.channel && this.state.member && this.state.users && this.state.currentUser; + } componentDidMount() { ChannelStore.addChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange); @@ -201,7 +205,7 @@ export default class Navbar extends React.Component { { $('.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 {
  • 1) { - teams.push( -
  • -
  • - ); - - this.state.teams.forEach((team) => { - if (team.name !== this.props.teamName) { - teams.push( -
  • - -
  • ); - } - }); - } - if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push(
  • @@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component { {inviteLink} {teamLink}
  • - + - +
  • {adminDivider} {teamSettings} @@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = { NavbarDropdown.propTypes = { teamType: React.PropTypes.string, teamDisplayName: React.PropTypes.string, - teamName: React.PropTypes.string + teamName: React.PropTypes.string, + currentUser: React.PropTypes.object }; diff --git a/web/react/components/needs_team.jsx b/web/react/components/needs_team.jsx new file mode 100644 index 000000000..33b9cd37e --- /dev/null +++ b/web/react/components/needs_team.jsx @@ -0,0 +1,20 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as GlobalActions from '../action_creators/global_actions.jsx'; + +export default class NeedsTeam extends React.Component { + componentWillMount() { + GlobalActions.loadTeamRequiredPage(); + } + render() { + return this.props.children; + } +} + +NeedsTeam.defaultProps = { +}; + +NeedsTeam.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/not_logged_in.jsx b/web/react/components/not_logged_in.jsx new file mode 100644 index 000000000..7af293e77 --- /dev/null +++ b/web/react/components/not_logged_in.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class NotLoggedIn extends React.Component { + componentDidMount() { + $('body').attr('class', 'white'); + $('#root').attr('class', 'container-fluid'); + } + componentWillUnmount() { + $('body').attr('class', ''); + $('#root').attr('class', ''); + } + render() { + return ( +
    +
    + {this.props.children} +
    +
    +
    +
    +
    + {global.window.mm_config.SiteName} +
    +
    + {'© 2015 Mattermost, Inc.'} + + + + + + + + + + + + +
    +
    +
    +
    + ); + } +} + +NotLoggedIn.defaultProps = { +}; + +NotLoggedIn.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx deleted file mode 100644 index 4c9bb6310..000000000 --- a/web/react/components/password_reset.jsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordResetSendLink from './password_reset_send_link.jsx'; -import PasswordResetForm from './password_reset_form.jsx'; - -export default class PasswordReset extends React.Component { - constructor(props) { - super(props); - - this.state = {}; - } - render() { - if (this.props.isReset === 'false') { - return ( - - ); - } - - return ( - - ); - } -} - -PasswordReset.defaultProps = { - isReset: '', - teamName: '', - teamDisplayName: '', - hash: '', - data: '' -}; -PasswordReset.propTypes = { - isReset: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string -}; diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 380dbe973..cfd39e440 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -2,24 +2,11 @@ // See License.txt for license information. import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_form.error', - defaultMessage: 'Please enter at least {chars} characters.' - }, - update: { - id: 'password_form.update', - defaultMessage: 'Your password has been updated successfully.' - }, - pwd: { - id: 'password_form.pwd', - defaultMessage: 'Password' - } -}); +import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; class PasswordResetForm extends React.Component { constructor(props) { @@ -32,51 +19,50 @@ class PasswordResetForm extends React.Component { handlePasswordReset(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; - var state = {}; - - var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { - state.error = formatMessage(holders.error, {chars: Constants.MIN_PASSWORD_LENGTH}); - this.setState(state); + this.setState({ + error: ( + + ) + }); return; } - state.error = null; - this.setState(state); + this.setState({ + error: null + }); - var data = {}; + const data = {}; data.new_password = password; - data.hash = this.props.hash; - data.data = this.props.data; - data.name = this.props.teamName; + data.hash = this.props.location.query.h; + data.data = this.props.location.query.d; + data.name = this.props.params.team; Client.resetPassword(data, - function resetSuccess() { - this.setState({error: null, updateText: formatMessage(holders.update)}); - }.bind(this), - function resetFailure(err) { - this.setState({error: err.message, updateText: null}); - }.bind(this) + () => { + this.setState({error: null}); + browserHistory.push('/' + this.props.params.team + '/login'); + }, + (err) => { + this.setState({error: err.message}); + } ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = (

    ); - } - var error = null; if (this.state.error) { - error =
    ; + error = ( +
    + +
    + ); } var formClass = 'form-group'; @@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return (
    @@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component {

    @@ -111,7 +95,10 @@ class PasswordResetForm extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage( + 'password_form.pwd', + 'Password' + )} spellCheck='false' />

    @@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component { defaultMessage='Change my password' /> - {updateText}
    @@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component { } PasswordResetForm.defaultProps = { - teamName: '', - teamDisplayName: '', - hash: '', - data: '' }; PasswordResetForm.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetForm); \ No newline at end of file +export default PasswordResetForm; diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 8cc8a050d..ce6253e16 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -4,26 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as client from '../utils/client.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_send.error', - defaultMessage: 'Please enter a valid email address.' - }, - link: { - id: 'password_send.link', - defaultMessage: '

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ' - }, - checkInbox: { - id: 'password_send.checkInbox', - defaultMessage: 'Please check your inbox.' - }, - email: { - id: 'password_send.email', - defaultMessage: 'Email' - } -}); +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; class PasswordResetSendLink extends React.Component { constructor(props) { @@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component { this.handleSendLink = this.handleSendLink.bind(this); - this.state = {}; + this.state = { + error: '', + updateText: '' + }; } handleSendLink(e) { e.preventDefault(); - var state = {}; - const {formatMessage} = this.props.intl; var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email || !Utils.isEmail(email)) { - state.error = formatMessage(holders.error); - this.setState(state); + this.setState({ + error: ( + + ) + }); return; } - state.error = null; - this.setState(state); + // End of error checking clear error + this.setState({ + error: '' + }); var data = {}; data.email = email; - data.name = this.props.teamName; - + data.name = this.props.params.team; client.sendPasswordReset(data, - function passwordResetSent() { - this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)}); - $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); - }.bind(this), - function passwordResetFailedToSend(err) { - this.setState({error: err.message, update_text: null, moreUpdateText: null}); - }.bind(this) - ); + () => { + this.setState({ + error: null, + updateText: ( +
    + + +
    + ) + }); + $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); + }, + (err) => { + this.setState({ + error: err.message, + update_text: null + }); + } + ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = ( -
    -
    - ); - } - var error = null; if (this.state.error) { error =
    ; @@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( -
    - +
    +
    +

    -

    -
    - -
    - {error} - - +

    + +

    +
    + +
    + {error} + + +

    ); @@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component { } PasswordResetSendLink.defaultProps = { - teamName: '', - teamDisplayName: '' }; PasswordResetSendLink.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + params: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetSendLink); \ No newline at end of file +export default PasswordResetSendLink; diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index afff78bae..1943fb409 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component { className='profile-img rounded pull-left' width='26px' height='26px' - src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${m.id}/image?time=${m.update_at}`} />
    ); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 2803fe387..966775dad 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -14,16 +14,15 @@ export default class PostHeader extends React.Component { } render() { const post = this.props.post; - const user = this.props.user; - let userProfile = ; + let userProfile = ; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( @@ -54,6 +53,7 @@ export default class PostHeader extends React.Component { allowReply='true' isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} + currentUser={this.props.currentUser} /> @@ -68,10 +68,11 @@ PostHeader.defaultProps = { sameUser: false }; PostHeader.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, user: React.PropTypes.object, - commentCount: React.PropTypes.number, - isLastComment: React.PropTypes.bool, - handleCommentClick: React.PropTypes.func, - sameUser: React.PropTypes.bool + currentUser: React.PropTypes.object.isRequired, + commentCount: React.PropTypes.number.isRequired, + isLastComment: React.PropTypes.bool.isRequired, + handleCommentClick: React.PropTypes.func.isRequired, + sameUser: React.PropTypes.bool.isRequired }; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ffac6eaef..d0a4c828e 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -1,10 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import TimeSince from './time_since.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -27,8 +26,8 @@ export default class PostInfo extends React.Component { } createDropdown() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() === post.user_id; - var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); + var isOwner = this.props.currentUser.id === post.user_id; + var isAdmin = Utils.isAdmin(this.props.currentUser.roles); if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; @@ -47,21 +46,21 @@ export default class PostInfo extends React.Component { if (this.props.allowReply === 'true') { dropdownContents.push( -
  • - - - -
  • +
  • + + + +
  • ); } @@ -93,7 +92,7 @@ export default class PostInfo extends React.Component { EventHelpers.showDeletePostModal(post, dataComments)} + onClick={() => GlobalActions.showDeletePostModal(post, dataComments)} > EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} user={profile} + currentUser={this.props.currentUser} /> ); @@ -525,7 +525,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -535,7 +535,8 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired }; function FloatingTimestamp({isScrolling, post}) { diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 976e03fab..b361779d2 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + currentUser: UserStore.getCurrentUser() }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); + } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component { return PostStore.getVisiblePosts(id); } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsEvent(); + GlobalActions.emitLoadMorePostsEvent(); } handlePostsViewScroll(atBottom) { if (atBottom) { @@ -165,6 +173,10 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); + if (!this.state.currentUser || !channel) { + return null; + } + const postListCtls = []; for (let i = 0; i < channels.length; i++) { const isActive = (channels[i] === currentChannelId); @@ -185,6 +197,7 @@ export default class PostsViewContainer extends React.Component { introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9588809eb..9183b761f 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; @@ -70,7 +70,7 @@ class RhsComment extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -151,7 +151,7 @@ class RhsComment extends React.Component { EventHelpers.showDeletePostModal(post, 0)} + onClick={() => GlobalActions.showDeletePostModal(post, 0)} >
    diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 023f3dd2d..fc1cd0b41 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component { EventHelpers.showDeletePostModal(post, this.props.commentCount)} + onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)} > { + var l = {}; + l.level = 'ERROR'; + l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; + + $.ajax({ + url: '/api/v1/admin/log_client', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(l) + }); + + if (window.mm_config.EnableDeveloper === 'true') { + window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'}); + window.ErrorStore.emitChange(); + } + }; + + // Ya.... + /*eslint-disable */ + if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { + !function(){var analytics=global.window.analytics=global.window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t; + } + + return ( + + {this.props.children} + + ); + } +} +Root.defaultProps = { +}; + +Root.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 5ab864b7c..3a091bdd1 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -3,8 +3,7 @@ import UserStore from '../stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import Constants from '../utils/constants.jsx'; @@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component { handleClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusEvent(this.props.post.id); + GlobalActions.emitPostFocusEvent(this.props.post.id); if ($(window).width() < 768) { $('.sidebar--right').removeClass('move--left'); @@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component { handleFocusRHSClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); + GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } render() { @@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
    diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx new file mode 100644 index 000000000..c473fe366 --- /dev/null +++ b/web/react/components/should_verify_email.jsx @@ -0,0 +1,111 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; + +export default class ShouldVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.handleResend = this.handleResend.bind(this); + + this.state = { + resendStatus: 'none' + }; + } + handleResend() { + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + this.setState({resendStatus: 'sending'}); + + Client.resendVerification(() => { + this.setState({resendStatus: 'success'}); + }, + () => { + this.setState({resendStatus: 'failure'}); + }, + teamName, + email); + } + render() { + let resendConfirm = ''; + if (this.state.resendStatus === 'success') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + if (this.state.resendStatus === 'failure') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + return ( +
    + +
    +
    +

    + +

    +
    +

    + +

    + + {resendConfirm} +
    +
    +
    +
    + ); + } +} + +ShouldVerifyEmail.defaultProps = { +}; +ShouldVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index c7dba306b..5c682d64b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -129,7 +129,9 @@ export default class Sidebar extends React.Component { directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), - showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER + showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, + currentTeam: TeamStore.getCurrent(), + currentUser: UserStore.getCurrentUser() }; } @@ -179,7 +181,7 @@ export default class Sidebar extends React.Component { } updateTitle() { const channel = ChannelStore.getCurrent(); - if (channel) { + if (channel && this.state.currentTeam) { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { currentSiteName = global.window.mm_config.SiteName; @@ -196,7 +198,7 @@ export default class Sidebar extends React.Component { const unread = this.getTotalUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; - document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName; } } onScroll() { @@ -401,7 +403,6 @@ export default class Sidebar extends React.Component { // set up click handler to switch channels (or create a new channel for non-existant ones) var handleClick = null; var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { handleClick = function clickHandler(e) { @@ -413,7 +414,7 @@ export default class Sidebar extends React.Component { e.preventDefault(); }; - } else if (channel.fake && teamURL) { + } else if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = Utils.getUserIdFromChannelName(channel); @@ -434,7 +435,7 @@ export default class Sidebar extends React.Component { }, () => { this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + window.location.href = '/' + this.state.currentTeam.name; } ); } @@ -497,6 +498,11 @@ export default class Sidebar extends React.Component { ); } render() { + // Check if we have all info needed to render + if (this.state.currentTeam == null || this.state.currentUser == null) { + return (
    ); + } + this.badgesActive = false; // keep track of the first and last unread channels so we can use them to set the unread indicators @@ -586,7 +592,10 @@ export default class Sidebar extends React.Component { ); return ( -
    + ); @@ -114,7 +119,7 @@ UserProfile.defaultProps = { disablePopover: false }; UserProfile.propTypes = { - user: React.PropTypes.object.isRequired, + user: React.PropTypes.object, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx index 2d1c74717..6b00a65c7 100644 --- a/web/react/components/user_settings/manage_languages.jsx +++ b/web/react/components/user_settings/manage_languages.jsx @@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx'; import * as Client from '../../utils/client.jsx'; import * as Utils from '../../utils/utils.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component { submitUser(user) { Client.updateUser(user, () => { - window.location.reload(true); + GlobalActions.newLocalizationSelected(user.locale); }, (err) => { let serverError; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 0acfd4a16..1dd564c8d 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,7 +3,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -28,7 +28,7 @@ class DeveloperTab extends React.Component { } register() { this.props.closeModal(); - EventHelpers.showRegisterAppModal(); + GlobalActions.showRegisterAppModal(); } render() { var appSection; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b0b1c414e..235892819 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; const holders = defineMessages({ usernameReserved: { @@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component { + ) }); } pictureSection = ( @@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsGeneralTab); \ No newline at end of file +export default injectIntl(UserSettingsGeneralTab); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index fa3415988..0c4a3d526 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component { this.updateTab = this.updateTab.bind(this); this.updateSection = this.updateSection.bind(this); + this.onUserChanged = this.onUserChanged.bind(this); this.state = { active_tab: 'general', active_section: '', showConfirmModal: false, - enforceFocus: true + enforceFocus: true, + currentUser: UserStore.getCurrentUser() }; this.requireConfirm = false; } + onUserChanged() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + componentDidMount() { if (this.props.show) { this.handleShow(); } + UserStore.addChangeListener(this.onUserChanged); } componentDidUpdate(prevProps) { if (this.props.show && !prevProps.show) { this.handleShow(); } + UserStore.removeChangeListener(this.onUserChanged); } handleShow() { @@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component { render() { const {formatMessage} = this.props.intl; - var currentUser = UserStore.getCurrentUser(); - var isAdmin = Utils.isAdmin(currentUser.roles); + if (this.state.currentUser == null) { + return (
    ); + } + var isAdmin = Utils.isAdmin(this.state.currentUser.roles); var tabs = []; tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index cba7ffdea..0b6b6c398 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; const holders = defineMessages({ currentPasswordError: { @@ -218,11 +218,24 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - const locale = global.window.mm_locale; const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { - date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }); updateSectionStatus = function updateSection() { @@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
    { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_FOCUSED_POST, - postId, - post_list: data - }); - - AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - } - ); -} - -export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { - Client.getPost( - post.channel_id, - post.id, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: post.channel_id, - numRequested: 0, - post_list: data - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: Utils.getRootId(post), - from_search: SearchStore.getSearchTerm() - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null, - is_mention_search: isMentionSearch - }); - }, - (err) => { - AsyncClient.dispatchError(err, 'getPost'); - } - ); -} - -export function emitLoadMorePostsEvent() { - const id = ChannelStore.getCurrentId(); - loadMorePostsTop(id); -} - -export function emitLoadMorePostsFocusedTopEvent() { - const id = PostStore.getFocusedPostId(); - loadMorePostsTop(id); -} - -export function loadMorePostsTop(id) { - const earliestPostId = PostStore.getEarliestPost(id).id; - if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { - AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); - } -} - -export function emitLoadMorePostsFocusedBottomEvent() { - const id = PostStore.getFocusedPostId(); - const latestPostId = PostStore.getLatestPost(id).id; - AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); -} - -export function emitPostRecievedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST, - post - }); -} - -export function emitUserPostedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.CREATE_POST, - post - }); -} - -export function emitPostDeletedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.POST_DELETED, - post - }); -} - -export function showDeletePostModal(post, commentCount = 0) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_DELETE_POST_MODAL, - value: true, - post, - commentCount - }); -} - -export function showGetPostLinkModal(post) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, - value: true, - post - }); -} - -export function showGetTeamInviteLinkModal() { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, - value: true - }); -} - -export function showInviteMemberModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, - value: true - }); -} - -export function showRegisterAppModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, - value: true - }); -} - -export function emitSuggestionPretextChanged(suggestionId, pretext) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, - id: suggestionId, - pretext - }); -} - -export function emitSelectNextSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_NEXT, - id: suggestionId - }); -} - -export function emitSelectPreviousSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, - id: suggestionId - }); -} - -export function emitCompleteWordSuggestion(suggestionId, term = '') { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, - id: suggestionId, - term - }); -} - -export function emitClearSuggestions(suggestionId) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, - id: suggestionId - }); -} - -export function emitPreferenceChangedEvent(preference) { - AppDispatcher.handleServerAction({ - type: Constants.ActionTypes.RECEIVED_PREFERENCE, - preference - }); -} - -export function emitRemovePost(post) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.REMOVE_POST, - post - }); -} - -export function sendEphemeralPost(message, channelId) { - const timestamp = Utils.getTimestamp(); - const post = { - id: Utils.generateId(), - user_id: '0', - channel_id: channelId || ChannelStore.getCurrentId(), - message, - type: Constants.POST_TYPE_EPHEMERAL, - create_at: timestamp, - update_at: timestamp, - filenames: [], - props: {} - }; - - emitPostRecievedEvent(post); -} diff --git a/web/react/package.json b/web/react/package.json index 07ffa0cdf..509c9967b 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -11,6 +11,8 @@ "marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca", "mm-intl": "mattermost/mm-intl#805442fd474fa40cd586ddeda404dbbe8e60626d", "object-assign": "4.0.1", + "react": "0.14.3", + "react-router": "2.0.0", "twemoji": "1.4.1" }, "devDependencies": { diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx deleted file mode 100644 index 989936d9e..000000000 --- a/web/react/pages/admin_console.jsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ErrorBar from '../components/error_bar.jsx'; -import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; -import AdminController from '../components/admin_console/admin_controller.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - -
    -
    - ); - } -} - -global.window.setup_admin_console_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('admin_controller') - ); -}; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx deleted file mode 100644 index bc78c049c..000000000 --- a/web/react/pages/channel.jsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelView from '../components/channel_view.jsx'; -import ChannelLoader from '../components/channel_loader.jsx'; -import ErrorBar from '../components/error_bar.jsx'; -import * as Client from '../utils/client.jsx'; - -import GetPostLinkModal from '../components/get_post_link_modal.jsx'; -import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; -import EditPostModal from '../components/edit_post_modal.jsx'; -import DeletePostModal from '../components/delete_post_modal.jsx'; -import MoreChannelsModal from '../components/more_channels.jsx'; -import TeamSettingsModal from '../components/team_settings_modal.jsx'; -import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; -import RegisterAppModal from '../components/register_app_modal.jsx'; -import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; -import InviteMemberModal from '../components/invite_member_modal.jsx'; - -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - - - - - - - - - - - -
    -
    - ); - } -} - -global.window.setup_channel_page = function setup(props, team, channel) { - if (props.PostId === '') { - EventHelpers.emitChannelClickEvent(channel); - } else { - EventHelpers.emitPostFocusEvent(props.PostId); - } - - ReactDOM.render( - , - document.getElementById('channel_view') - ); -}; diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx deleted file mode 100644 index abbf72ea3..000000000 --- a/web/react/pages/claim_account.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ClaimAccount from '../components/claim/claim_account.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_claim_account_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('claim') - ); -}; \ No newline at end of file diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx deleted file mode 100644 index 2e47e3e6a..000000000 --- a/web/react/pages/docs.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Docs from '../components/docs.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.mm_user = global.window.mm_user || {}; - -global.window.setup_documentation_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('docs') - ); -}; diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx deleted file mode 100644 index 93394fcde..000000000 --- a/web/react/pages/find_team.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import FindTeam from '../components/find_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_find_team_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('find-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx deleted file mode 100644 index ff81c4994..000000000 --- a/web/react/pages/home.jsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import TeamStore from '../stores/team_store.jsx'; -import Constants from '../utils/constants.jsx'; - -function setupHomePage() { - var last = null; - if (last == null || last.length === 0) { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; - } else { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; - } -} - -global.window.setup_home_page = setupHomePage; diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx deleted file mode 100644 index ec9080945..000000000 --- a/web/react/pages/login.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../utils/client.jsx'; -import Login from '../components/login.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_login_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('login') - ); -}; \ No newline at end of file diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx deleted file mode 100644 index 7caff5034..000000000 --- a/web/react/pages/password_reset.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordReset from '../components/password_reset.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_password_reset_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('reset') - ); -}; diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx new file mode 100644 index 000000000..d0b06e32e --- /dev/null +++ b/web/react/pages/root.jsx @@ -0,0 +1,290 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import Root from '../components/root.jsx'; +import Login from '../components/login.jsx'; +import LoggedIn from '../components/logged_in.jsx'; +import NotLoggedIn from '../components/not_logged_in.jsx'; +import NeedsTeam from '../components/needs_team.jsx'; +import PasswordResetSendLink from '../components/password_reset_send_link.jsx'; +import PasswordResetForm from '../components/password_reset_form.jsx'; +import ChannelView from '../components/channel_view.jsx'; +import Sidebar from '../components/sidebar.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import ErrorStore from '../stores/error_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import SignupTeam from '../components/signup_team.jsx'; +import * as Client from '../utils/client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; +import SignupUserComplete from '../components/signup_user_complete.jsx'; +import ShouldVerifyEmail from '../components/should_verify_email.jsx'; +import DoVerifyEmail from '../components/do_verify_email.jsx'; +import AdminConsole from '../components/admin_console/admin_controller.jsx'; +import ClaimAccount from '../components/claim/claim_account.jsx'; + +import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx'; +import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx'; +import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx'; +import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx'; +import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx'; +import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx'; +import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx'; +import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx'; + +// This is for anything that needs to be done for ALL react components. +// This runs before we start to render anything. +function preRenderSetup(callwhendone) { + const d1 = Client.getClientConfig( + (data, textStatus, xhr) => { + if (!data) { + return; + } + + global.window.mm_config = data; + + var serverVersion = xhr.getResponseHeader('X-Version-ID'); + + if (serverVersion !== BrowserStore.getLastServerVersion()) { + if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') { + BrowserStore.setLastServerVersion(serverVersion); + } else { + BrowserStore.setLastServerVersion(serverVersion); + window.location.reload(true); + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console + } + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientConfig'); + } + ); + + const d2 = Client.getClientLicenceConfig( + (data) => { + if (!data) { + return; + } + + global.window.mm_license = data; + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientLicenceConfig'); + } + ); + + // Set these here so they don't fail in client.jsx track + global.window.analytics = {}; + global.window.analytics.page = () => { + // Do Nothing + }; + global.window.analytics.track = () => { + // Do Nothing + }; + + $.when(d1, d2).done(callwhendone); +} + +function preLoggedIn(nextState, replace, callback) { + const d1 = Client.getAllPreferences( + (data) => { + if (!data) { + return; + } + + PreferenceStore.setPreferences(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'getAllPreferences'); + } + ); + + const d2 = AsyncClient.getChannels(); + + $.when(d1, d2).done(() => callback()); +} + +function onChannelChange(nextState) { + const channelName = nextState.params.channel; + + // Make sure we have all the channels + AsyncClient.getChannels(true); + + // Get our channel's ID + const channel = ChannelStore.getByName(channelName); + + // User clicked channel + GlobalActions.emitChannelClickEvent(channel); +} + +function onRootEnter(nextState, replace, callback) { + if (nextState.location.pathname === '/') { + Client.getMeLoggedIn((data) => { + if (!data || data.logged_in === 'false') { + replace({pathname: '/signup_team'}); + callback(); + } else { + replace({pathname: '/' + data.team_name + '/channels/town-square'}); + callback(); + } + }); + return; + } + + callback(); +} + +function onPermalinkEnter(nextState) { + const postId = nextState.params.postid; + + GlobalActions.emitPostFocusEvent(postId); +} + +function onLoggedOut(nextState) { + const teamName = nextState.params.team; + Client.logout( + () => { + browserHistory.push('/' + teamName + '/login'); + BrowserStore.signalLogout(); + BrowserStore.clear(); + ErrorStore.clearLastError(); + }, + () => { + browserHistory.push('/' + teamName + '/login'); + } + ); +} + +function renderRootComponent() { + ReactDOM.render(( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + document.getElementById('root')); +} + +global.window.setup_root = () => { + // Do the pre-render setup and call renderRootComponent when done + preRenderSetup(renderRootComponent); +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx deleted file mode 100644 index f276c3ff7..000000000 --- a/web/react/pages/signup_team.jsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeam from '../components/signup_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired, - teams: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_page = function setup(props) { - var teams = []; - - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') { - teams.push({name: prop, display_name: props[prop]}); - } - } - } - - ReactDOM.render( - , - document.getElementById('signup-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx deleted file mode 100644 index 8c237f698..000000000 --- a/web/react/pages/signup_team_complete.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamComplete from '../components/signup_team_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx deleted file mode 100644 index 13c8f3fd0..000000000 --- a/web/react/pages/signup_team_confirm.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_confirm_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-confirm') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx deleted file mode 100644 index a14f2140b..000000000 --- a/web/react/pages/signup_user_complete.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupUserComplete from '../components/signup_user_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_user_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-user-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx deleted file mode 100644 index 6b336daa1..000000000 --- a/web/react/pages/verify.jsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import EmailVerify from '../components/email_verify.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setupVerifyPage = function setup(props) { - ReactDOM.render( - , - document.getElementById('verify') - ); -}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 5c911e94b..9f7f6e7ff 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter { } getSelectedTeams() { - return BrowserStore.getItem('seleted_teams'); + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; } saveSelectedTeams(teams) { @@ -156,7 +160,3 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { }); export default AdminStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AdminStore = AdminStore; -} diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx index 0ad989206..ec827f6d7 100644 --- a/web/react/stores/analytics_store.jsx +++ b/web/react/stores/analytics_store.jsx @@ -83,7 +83,3 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { }); export default AnalyticsStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AnalyticsStore = AnalyticsStore; -} diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 3417faaaf..3b35916b3 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -4,8 +4,8 @@ import {generateId} from '../utils/utils.jsx'; function getPrefix() { - if (global.window.mm_user) { - return global.window.mm_user.id + '_'; + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; } return 'unknown_'; @@ -31,7 +31,9 @@ class BrowserStoreClass { this.isSignallingLogout = this.isSignallingLogout.bind(this); this.signalLogin = this.signalLogin.bind(this); this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + checkVersion() { var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index eac24b071..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -350,7 +350,3 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { }); export default ChannelStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ChannelStore = ChannelStore; -} diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx index c1fd0ef74..6d7e0f354 100644 --- a/web/react/stores/file_store.jsx +++ b/web/react/stores/file_store.jsx @@ -57,9 +57,4 @@ class FileStore extends EventEmitter { } } -const instance = new FileStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.FileStore = instance; -} +export default new FileStore(); diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx new file mode 100644 index 000000000..0e3a63724 --- /dev/null +++ b/web/react/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index 1819fffc0..5ea38030b 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -45,7 +45,3 @@ class ModalStoreClass extends EventEmitter { const ModalStore = new ModalStoreClass(); export default ModalStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ModalStore = ModalStore; -} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 5cc3f300d..a6dfcd46f 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -608,7 +608,3 @@ function isPostListNull(pl) { return false; } - -if (window.mm_config.EnableDeveloper === 'true') { - window.PostStore = PostStore; -} diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 96071665c..549f355ef 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -135,7 +135,3 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { }); export default SearchStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SearchStore = SearchStore; -} diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..ad24a04cd 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,7 +10,7 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; @@ -42,10 +42,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,7 +50,7 @@ class SocketStoreClass extends EventEmitter { protocol = 'wss://'; } - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex(); + var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console @@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg, translations) { // Store post const post = JSON.parse(msg.props.post); - EventHelpers.emitPostRecievedEvent(post); + GlobalActions.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { @@ -291,7 +287,7 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - EventHelpers.emitPostDeletedEvent(post); + GlobalActions.emitPostDeletedEvent(post); } function handleNewUserEvent() { @@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) { function handlePreferenceChangedEvent(msg) { const preference = JSON.parse(msg.props.preference); - EventHelpers.emitPreferenceChangedEvent(preference); + GlobalActions.emitPreferenceChangedEvent(preference); } var SocketStore = new SocketStoreClass(); diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index 487bae843..efd2b76ed 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -258,9 +258,4 @@ class SuggestionStore extends EventEmitter { } } -const instance = new SuggestionStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SuggestionStore = instance; -} +export default new SuggestionStore(); diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 493d6bc4d..354a07b72 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; const CHANGE_EVENT = 'change'; @@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter { this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; } emitChange() { @@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return BrowserStore.getItem('user_teams', {}); + return this.teams; } getCurrentId() { - var team = global.window.mm_team; + var team = this.get(this.currentTeamId); if (team) { return team.id; @@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter { } getCurrent() { - if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { - this.saveTeam(global.window.mm_team); + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; } - return global.window.mm_team; + return null; } getCurrentTeamUrl() { @@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - var teams = this.getAll(); - teams[team.id] = team; - BrowserStore.setItem('user_teams', teams); + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; } } @@ -116,17 +127,16 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_TEAM: - TeamStore.saveTeam(action.team); + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - default: } }); export default TeamStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.TeamStore = TeamStore; -} diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 9fcd2440e..c1e5c75dc 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; -const CHANGE_EVENT_TEAMS = 'change_teams'; const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); this.profileCache = null; + this.currentUserId = ''; } emitChange(userId) { @@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_AUDITS, callback); } - emitTeamsChange() { - this.emit(CHANGE_EVENT_TEAMS); - } - - addTeamsChangeListener(callback) { - this.on(CHANGE_EVENT_TEAMS, callback); - } - - removeTeamsChangeListener(callback) { - this.removeListener(CHANGE_EVENT_TEAMS, callback); - } - emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } @@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter { } getCurrentUser() { - if (this.getProfiles()[global.window.mm_user.id] == null) { - this.saveProfile(global.window.mm_user); - } - - return global.window.mm_user; + return this.getProfiles()[this.currentUserId]; } setCurrentUser(user) { - var oldUser = global.window.mm_user; - - if (oldUser.id === user.id) { - global.window.mm_user = user; - this.saveProfile(user); - } else { - throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); - } + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; } getCurrentId() { - var user = global.window.mm_user; + var user = this.getCurrentUser(); if (user) { return user.id; @@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter { saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (currentId in profiles) { - delete profiles[currentId]; + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; } - this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } @@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('audits', {loading: true}); } - setTeams(teams) { - BrowserStore.setItem('teams', teams); - } - - getTeams() { - return BrowserStore.getItem('teams', []); - } - getCurrentMentionKeys() { return this.getMentionKeys(this.getCurrentId()); } @@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.setAudits(action.audits); UserStore.emitAuditsChange(); break; - case ActionTypes.RECEIVED_TEAMS: - UserStore.setTeams(action.teams); - UserStore.emitTeamsChange(); - break; case ActionTypes.RECEIVED_STATUSES: UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); @@ -325,7 +303,3 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { }); export {UserStore as default}; - -if (window.mm_config.EnableDeveloper === 'true') { - window.UserStore = UserStore; -} diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 7d5e1bd0f..b9770a6e9 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as client from './client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -44,15 +45,19 @@ function isCallInProgress(callName) { export function getChannels(checkVersion) { if (isCallInProgress('getChannels')) { - return; + return null; } callTracker.getChannels = utils.getTimestamp(); - client.getChannels( + return client.getChannels( (data, textStatus, xhr) => { callTracker.getChannels = 0; + if (xhr.status === 304 || !data) { + return; + } + if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); @@ -67,10 +72,6 @@ export function getChannels(checkVersion) { } } - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNELS, channels: data.channels, @@ -392,36 +393,6 @@ export function getAllTeams() { ); } -export function findTeams(email) { - if (isCallInProgress('findTeams_' + email)) { - return; - } - - var user = UserStore.getCurrentUser(); - if (user) { - callTracker['findTeams_' + email] = utils.getTimestamp(); - client.findTeams( - user.email, - function findTeamsSuccess(data, textStatus, xhr) { - callTracker['findTeams_' + email] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAMS, - teams: data - }); - }, - function findTeamsFailure(err) { - callTracker['findTeams_' + email] = 0; - dispatchError(err, 'findTeams'); - } - ); - } -} - export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; @@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) { export function getMe() { if (isCallInProgress('getMe')) { - return; + return null; } callTracker.getMe = utils.getTimestamp(); - client.getMe( + return client.getMe( (data, textStatus, xhr) => { callTracker.getMe = 0; @@ -661,6 +632,8 @@ export function getMe() { type: ActionTypes.RECEIVED_ME, me: data }); + + GlobalActions.newLocalizationSelected(data.locale); }, (err) => { callTracker.getMe = 0; @@ -706,11 +679,11 @@ export function getStatuses() { export function getMyTeam() { if (isCallInProgress('getMyTeam')) { - return; + return null; } callTracker.getMyTeam = utils.getTimestamp(); - client.getMyTeam( + return client.getMyTeam( function getMyTeamSuccess(data, textStatus, xhr) { callTracker.getMyTeam = 0; @@ -719,7 +692,7 @@ export function getMyTeam() { } AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM, + type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index ed94f94b8..94f3f0ce0 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl'; @@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
    @@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) { } export function createDefaultIntroMessage(channel) { - const team = TeamStore.getCurrent(); - let inviteModalLink; - if (team.type === Constants.INVITE_TEAM) { - inviteModalLink = ( - - - - - ); - } else { - inviteModalLink = ( - - - - - ); - } + const inviteModalLink = ( + + + + + ); return (
    diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76d42137a..e00f28a14 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1,8 +1,8 @@ // See License.txt for license information. import BrowserStore from '../stores/browser_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import ErrorStore from '../stores/error_store.jsx'; + +import {browserHistory} from 'react-router'; let translations = { connectionError: 'There appears to be a problem with your internet connection.', @@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) { if (xhr.status === 401) { if (window.location.href.indexOf('/channels') === 0) { - window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } else { - var teamURL = window.location.href.split('/channels')[0]; - window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + var teamURL = window.location.pathname.split('/channels')[0]; + browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } } @@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) { track('api', 'api_users_switch_to_email'); } -export function logout() { +export function logout(success, error) { track('api', 'api_users_logout'); - var currentTeamUrl = TeamStore.getCurrentTeamUrl(); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - window.location.href = currentTeamUrl + '/logout'; + $.ajax({ + url: '/api/v1/users/logout', + type: 'POST', + success, + error: function onError(xhr, status, err) { + var e = handleError('logout', xhr, status, err); + error(e); + } + }); } export function loginByEmail(name, email, password, success, error) { @@ -437,7 +441,7 @@ export function getServerAudits(success, error) { } export function getConfig(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/admin/config', dataType: 'json', contentType: 'application/json', @@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) { } else { url += teamId + '/' + name; } - $.ajax({ url, dataType: 'json', @@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) { }); } +export function getClientConfig(success, error) { + return $.ajax({ + url: '/api/v1/admin/client_props', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientConfig', xhr, status, err); + error(e); + } + }); +} + +export function getTeamAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', @@ -529,6 +560,21 @@ export function getAllTeams(success, error) { }); } +export function getMeLoggedIn(success, error) { + return $.ajax({ + cache: false, + url: '/api/v1/users/me_logged_in', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getMeLoggedIn', xhr, status, err); + error(e); + } + }); +} + export function getMe(success, error) { var currentUser = null; $.ajax({ @@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) { }); } -export function findTeamsSendEmail(email, success, error) { - $.ajax({ - url: '/api/v1/teams/email_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamsSendEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_email_teams'); -} - -export function findTeams(email, success, error) { - $.ajax({ - url: '/api/v1/teams/find_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeams', xhr, status, err); - error(e); - } - }); -} - export function createChannel(channel, success, error) { $.ajax({ url: '/api/v1/channels/create', @@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) { } export function getChannels(success, error) { - $.ajax({ + return $.ajax({ cache: false, url: '/api/v1/channels/', dataType: 'json', @@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { url += '/' + memberLimit; } - $.ajax({ + return $.ajax({ url, dataType: 'json', contentType: 'application/json', @@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) } export function getPosts(channelId, since, success, error, complete) { - $.ajax({ + return $.ajax({ url: '/api/v1/channels/' + channelId + '/posts/' + since, dataType: 'json', type: 'GET', @@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) { } export function getMyTeam(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/teams/me', dataType: 'json', type: 'GET', @@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) { } export function getAllPreferences(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/preferences/', dataType: 'json', type: 'GET', @@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) { track('api', 'api_license_upload'); } + +export function getClientLicenceConfig(success, error) { + return $.ajax({ + url: '/api/v1/license/client_config', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientLicenceConfig', xhr, status, err); + error(e); + } + }); +} + +export function getInviteInfo(success, error, id) { + $.ajax({ + url: '/api/v1/teams/get_invite_info', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({invite_id: id}), + success, + error: function onError(xhr, status, err) { + var e = handleError('getInviteInfo', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function verifyEmail(success, error, uid, hid) { + $.ajax({ + url: '/api/v1/users/verify_email', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({uid, hid}), + success, + error: function onError(xhr, status, err) { + var e = handleError('verifyEmail', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function resendVerification(success, error, teamName, email) { + $.ajax({ + url: '/api/v1/users/resend_verification', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({team_name: teamName, email}), + success, + error: function onError(xhr, status, err) { + var e = handleError('resendVerification', xhr, status, err); + if (error) { + error(e); + } + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e..2cff4dbed 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -42,13 +42,15 @@ export default { RECEIVED_MSG: null, - RECEIVED_TEAM: null, + RECEIVED_MY_TEAM: null, RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_LOCALE: null, + SHOW_SEARCH: null, TOGGLE_IMPORT_THEME_MODAL: null, @@ -143,6 +145,7 @@ export default { EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', + SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 6942a8e08..88777164b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; @@ -941,7 +942,7 @@ export function updateAddressBar(channelName) { } export function switchChannel(channel) { - EventHelpers.emitChannelClickEvent(channel); + GlobalActions.emitChannelClickEvent(channel); updateAddressBar(channel.name); @@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) { // Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. export function getFileUrl(filename, isDownload) { - const downloadParam = isDownload ? '&download=1' : ''; - return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam; + const downloadParam = isDownload ? '?download=1' : ''; + return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam; } // Gets the name of a file (including extension) from a given url or file path. @@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) { return ''; } -export function getSessionIndex() { - if (global.window.mm_session_token_index >= 0) { - return 'session_token_index=' + global.window.mm_session_token_index; - } - - return ''; -} - // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 @@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) { export function getRootId(post) { return post.root_id === '' ? post.id : post.root_id; } + +export function localizeMessage(id, defaultMessage) { + const translations = LocalizationStore.getTranslations(); + if (translations) { + const value = translations[id]; + if (value) { + return value; + } + } + + if (defaultMessage) { + return defaultMessage; + } + + return id; +} diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5e7f04724..44681291c 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -24,12 +24,6 @@ padding: 1em 1em 0; display: none; } - > div { - height: 100%; - position: absolute; - padding-bottom: 70px; - width: 100%; - } .badge { background-color: $primary-color; position: absolute; diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md deleted file mode 100644 index 2063ad41c..000000000 --- a/web/static/help/Messaging_en.md +++ /dev/null @@ -1,47 +0,0 @@ -# Messaging - -### Writing Messages - -You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost. - -Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message. - -### Formatting Messages - -Mattermost messages are formatted using a standard called "markdown". Here are examples: - -| Text Entered | How it appears | -|:---------------|:---------------| -|`**bold**`| **bold** | -| `_italic_`|_italic_| -|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)| -|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/). - - -### Mentioning Teammates - -You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention. - -For example, you might write: - -``` -@alice how did your interview go with the new candidate? -``` - -Which sends a special mention notification to **alice** to check your message. - -To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned. - -You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences** - -### Messages Dropdown Menu - -To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message: - -- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread. -- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives. -- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message. -- **Edit:** Lets you edit your own message. diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md deleted file mode 100644 index d3947f36a..000000000 --- a/web/static/help/Messaging_es.md +++ /dev/null @@ -1,37 +0,0 @@ -# Mensajes - -## Escribiendo Mensajes - -Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost. - -Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje. - -## Darle formato a los Mensajes - -Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquí algunos ejemplos: - -| Texto escrito | Como aparece | -|:--------------|:-------------| -|`**negrita**`| **negrita** | -| `_italica_`|_italica_| -|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)| -|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Revisa la lista completa de Emojis [aquí](http://www.emoji-cheat-sheet.com/). - -## Mencionando a compañeros - -Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención. - -Por ejemplo, podrías escribir: - -``` -@alicia como te fue con la entrevista del nuevo candidato? -``` - -Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje. - -Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos. - -Puedes configurar como te gustaría ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación** diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index d2e340641..2a536925c 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -661,8 +661,10 @@ "email_signup.find": "Find my teams", "email_verify.almost": "{siteName}: You are almost done", "email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.", + "email_verify.verifyFailed": "Failed to verify your email.", "email_verify.resend": "Resend Email", "email_verify.sent": " Verification email sent.", + "email_verify.failed": " Failed to send verification email.", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "

    Your email has been verified! Click here to log in.

    ", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -758,6 +760,7 @@ "login.or": "or", "login.signTo": "Sign in to:", "login.verified": " Email Verified", + "login.session_expired": " Your session has expired. Please login again.", "login_email.badTeam": "Bad team name", "login_email.email": "Email", "login_email.emailReq": "An email is required", @@ -822,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click here to log in.", - "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.", + "password_form.enter": "Enter a new password for your {siteName} account.", "password_form.error": "Please enter at least {chars} characters.", "password_form.pwd": "Password", "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", + "password_send.description": "To reset your password, enter the email address you used to sign up.", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", - "password_send.link": "

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ", + "password_send.link": "

    A password reset link has been sent to {email}

    ", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "▲ collapse text", @@ -1303,5 +1306,11 @@ "view_image.loading": "Loading ", "view_image_popover.download": "Download", "view_image_popover.file": "File {count} of {total}", - "view_image_popover.publicLink": "Get Public Link" + "view_image_popover.publicLink": "Get Public Link", + "web.footer.about": "About", + "web.footer.help": "Help", + "web.footer.privacy": "Privacy", + "web.footer.terms": "Terms", + "web.header.back": "Back", + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" } diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index c6b16a293..f42dc879a 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -822,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha aquí para iniciar sesión.", - "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.", + "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.", "password_form.error": "Por favor ingresa al menos {chars} caracteres.", "password_form.pwd": "Contraseña", "password_form.title": "Restablecer Contraseña", "password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.", + "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.", "password_send.email": "Correo electrónico", "password_send.error": "Por favor ingresa una dirección correo electrónico válida.", - "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email} para tu equipo {teamDisplayName} en {hostname}.

    ", + "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email}

    ", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "▲ colapsar texto", @@ -1303,5 +1303,11 @@ "view_image.loading": "Cargando ", "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", - "view_image_popover.publicLink": "Obtener Enlace Público" + "view_image_popover.publicLink": "Obtener Enlace Público", + "web.footer.about": "Acerca", + "web.footer.help": "Ayuda", + "web.footer.privacy": "Privacidad", + "web.footer.terms": "Términos", + "web.header.back": "Atrás", + "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" } diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index b9b8f4c07..d276e339a 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -820,16 +820,15 @@ "navbar_dropdown.teamSettings": "Configurações da Equipe", "password_form.change": "Alterar minha senha", "password_form.click": "Clique aqui para logar.", - "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.", + "password_form.enter": "Entre uma nova senha para sua conta {siteName}.", "password_form.error": "Por favor, insira pelo menos {chars} caracteres.", "password_form.pwd": "Senha", "password_form.title": "Resetar Senha", "password_form.update": "Sua senha foi atualizada com sucesso.", "password_send.checkInbox": "Por favor verifique sua caixa de entrada.", - "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.", + "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.", "password_send.email": "E-mail", "password_send.error": "Por favor entre um endereço de e-mail válido.", - "password_send.link": "

    Um link para resetar a sua senha na equipe {teamDisplayName} em {hostname} foi enviado para {email}.

    ", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "▲ recolher texto", diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493e..000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} - - -{{template "head" . }} - - - -
    - - - - -{{end}} diff --git a/web/templates/authorize.html b/web/templates/authorize.html deleted file mode 100644 index 0fa36b0ab..000000000 --- a/web/templates/authorize.html +++ /dev/null @@ -1,12 +0,0 @@ -{{define "authorize"}} - -{{template "head" . }} - -
    -
    - - - -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a022..000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} - - -{{template "head" . }} - -
    - - - -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1b..000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/docs.html b/web/templates/docs.html deleted file mode 100644 index dc18e5cb6..000000000 --- a/web/templates/docs.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "docs"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/find_team.html b/web/templates/find_team.html deleted file mode 100644 index b7e1d7eca..000000000 --- a/web/templates/find_team.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "find_team"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/footer.html b/web/templates/footer.html deleted file mode 100644 index 5b11328fb..000000000 --- a/web/templates/footer.html +++ /dev/null @@ -1,39 +0,0 @@ -{{define "footer"}} - - -{{end}} diff --git a/web/templates/head.html b/web/templates/head.html deleted file mode 100644 index 61b1aa12b..000000000 --- a/web/templates/head.html +++ /dev/null @@ -1,191 +0,0 @@ -{{define "head"}} - - - - - - {{ .Props.Title }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{end}} diff --git a/web/templates/home.html b/web/templates/home.html deleted file mode 100644 index 08876d41d..000000000 --- a/web/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "home"}} - - -{{template "head" . }} - -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - - -{{end}} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index 88540a906..000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "login"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html deleted file mode 100644 index e68f8b693..000000000 --- a/web/templates/password_reset.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "password_reset"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html deleted file mode 100644 index afba58066..000000000 --- a/web/templates/signup_team.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team"}} - - -{{template "head" . }} - -
    -
    -
    -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html deleted file mode 100644 index 3873d8978..000000000 --- a/web/templates/signup_team_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html deleted file mode 100644 index 31f1ba95b..000000000 --- a/web/templates/signup_team_confirm.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_team_confirm"}} - - -{{template "head" . }} - -
    -
    - - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html deleted file mode 100644 index 937a89dd2..000000000 --- a/web/templates/signup_user_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_user_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/verify.html b/web/templates/verify.html deleted file mode 100644 index 2e5496d7a..000000000 --- a/web/templates/verify.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "verify"}} - - - {{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/web.go b/web/web.go index 09450b976..2a44ece00 100644 --- a/web/web.go +++ b/web/web.go @@ -4,67 +4,16 @@ package web import ( - "fmt" + "net/http" + "strings" + l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" - "gopkg.in/fsnotify.v1" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" ) -var Templates *template.Template - -type HtmlTemplatePage api.Page - -func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage { - - if len(title) > 0 { - title = utils.Cfg.TeamSettings.SiteName + " - " + title - } - - props := make(map[string]string) - props["Title"] = title - return &HtmlTemplatePage{ - TemplateName: templateName, - Props: props, - ClientCfg: utils.ClientCfg, - ClientLicense: utils.ClientLicense, - Locale: locale, - } -} - -func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { - if me.Team != nil { - me.Team.Sanitize() - } - - if me.User != nil { - me.User.Sanitize(map[string]bool{}) - me.Locale = me.User.Locale - } - - me.Props["Locale"] = me.Locale - me.SessionTokenIndex = c.SessionTokenIndex - - me.ClientCfg["HeaderBack"] = c.T("web.header.back") - me.ClientCfg["FooterHelp"] = c.T("web.footer.help") - me.ClientCfg["FooterTerms"] = c.T("web.footer.terms") - me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy") - me.ClientCfg["FooterAbout"] = c.T("web.footer.about") - - if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { - c.SetUnknownError(me.TemplateName, err.Error()) - } -} - func InitWeb() { l4g.Debug(utils.T("web.init.debug")) @@ -74,81 +23,7 @@ func InitWeb() { l4g.Debug("Using static directory at %v", staticDir) mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) - mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") - mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") - mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") - - mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") - mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") - mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") - mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") - mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") - - mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET") - - mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") - - mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET") - - // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team specific should go below this line - // ---------------------------------------------------------------------------------------------- - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") - mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - - watchAndParseTemplates() -} - -func watchAndParseTemplates() { - - templatesDir := utils.FindDir("web/templates") - l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir) - var err error - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(utils.T("web.create_dir.error"), err) - } - - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - l4g.Info(utils.T("web.reparse_templates.info"), event.Name) - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - } - case err := <-watcher.Errors: - l4g.Error(utils.T("web.dir_fail.error"), err) - } - } - }() - - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(utils.T("web.watcher_fail.error"), err) - } + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" @@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if len(c.Session.UserId) == 0 { - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Props["Info"] = c.T("web.root.singup_info") - - if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - for _, team := range teams { - page.Props[team.Name] = team.DisplayName - } - - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { - http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) - return - } - } - - page.Render(c, w) - } else { - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale) - page.Team = team - page.User = user - page.Render(c, w) - } -} - -func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !CheckBrowserCompatability(c, r) { - return - } - - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Render(c, w) -} - -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - params := mux.Vars(r) - teamName := params["team"] - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - // We still might be able to switch to this team because we've logged in before - _, session := api.FindMultiSessionForTeamId(r, team.Id) - if session != nil { - w.Header().Set(model.HEADER_TOKEN, session.Token) - lastViewChannelName := "town-square" - if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil { - if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil { - lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name - } - } - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect) - return - } - - page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale) - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - if team.AllowOpenInvite { - page.Props["InviteId"] = team.InviteId - } - - page.Render(c, w) -} - -func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale) - page.Props["Email"] = email - page.Render(c, w) -} - -func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - data := r.FormValue("d") - hash := r.FormValue("h") - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "") - return - } - - page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - - id := r.FormValue("id") - data := r.FormValue("d") - hash := r.FormValue("h") - var props map[string]string - - if len(id) > 0 { - props = make(map[string]string) - - if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { - c.Err = result.Err - return - } else { - team := result.Data.(*model.Team) - if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { - c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id) - return - } - - props["email"] = "" - props["display_name"] = team.DisplayName - props["name"] = team.Name - props["id"] = team.Id - data = model.MapToJson(props) - hash = "" - } - } else { - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "") - return - } - - props = model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "") - return - } - } - - page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["TeamDisplayName"] = props["display_name"] - page.Props["TeamName"] = props["name"] - page.Props["TeamId"] = props["id"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { - api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) -} - -func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - teamName := params["team"] - postId := params["postid"] - - if len(postId) != 26 { - c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId) - return - } - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var post *model.Post - if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil { - c.Err = result.Err - return - } else { - postlist := result.Data.(*model.PostList) - post = postlist.Posts[postlist.Order[0]] - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - if result.Data.(int64) == 0 { - if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, post.Id) -} - -func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - name := params["channelname"] - teamName := params["team"] - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - channelId := result.Data.(string) - if len(channelId) == 0 { - if channel = autoJoinChannelName(c, w, r, name); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, "") -} - -func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel { - if strings.Index(channelName, "__") > 0 { - // It's a direct message channel that doesn't exist yet so let's create it - ids := strings.Split(channelName, "__") - otherUserId := "" - if ids[0] == c.Session.UserId { - otherUserId = ids[1] - } else { - otherUserId = ids[0] - } - - if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { - api.Handle404(w, r) - return nil - } else { - return sc - } - } else { - // We will attempt to auto-join open channels - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName)) - } - - return nil -} - -func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel { - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId)) -} - -func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel { - if cr := <-channel; cr.Err != nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } else { - channel := cr.Data.(*model.Channel) - if channel.Type == model.CHANNEL_OPEN { - api.JoinChannel(c, channel.Id, "") - if c.Err != nil { - return nil - } - } else { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } - return channel - } -} - -func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team { - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - // We are logged into a different team. Lets see if we have another - // session in the cookie that will give us access. - if c.Session.TeamId != team.Id { - index, session := api.FindMultiSessionForTeamId(r, team.Id) - if session == nil { - // redirect to login - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) - } else { - c.Session = *session - c.SessionTokenIndex = index - } - } - - return team -} - -func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId) - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - c.RemoveSessionCookie(w, r) - l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId) - return - } else { - user = ur.Data.(*model.User) - } - - var preferences model.Preferences - if result := <-prefChan; result.Err != nil { - l4g.Error("Error in getting preferences for id=%v", c.Session.UserId) - } else { - preferences = result.Data.(model.Preferences) - } - - page := NewHtmlTemplatePage("channel", "", c.Locale) - page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["ChannelName"] = channel.Name - page.Props["ChannelId"] = channel.Id - page.Props["PostId"] = postid - page.Team = team - page.User = user - page.Channel = channel - page.Preferences = &preferences - page.Render(c, w) -} - -func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { - resend := r.URL.Query().Get("resend") - resendSuccess := r.URL.Query().Get("resend_success") - name := r.URL.Query().Get("teamname") - email := r.URL.Query().Get("email") - hashedId := r.URL.Query().Get("hid") - userId := r.URL.Query().Get("uid") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if resend == "true" { - if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user := result.Data.(*model.User) - - if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } else { - api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } - - newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) - http.Redirect(w, r, newAddress, http.StatusFound) - return - } - } - - if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) { - if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) - return - } - } - - page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale) - page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) - page.Props["UserEmail"] = email - page.Props["ResendSuccess"] = resendSuccess - page.Render(c, w) -} - -func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { - page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale) - page.Render(c, w) -} - -func docs(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - doc := params["doc"] - - var user *model.User - if len(c.Session.UserId) != 0 { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - if userChan := <-userChan; userChan.Err == nil { - user = userChan.Data.(*model.User) - } - } - - page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale) - page.Props["Site"] = doc - page.User = user - page.Render(c, w) -} - -func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { - isResetLink := true - hash := r.URL.Query().Get("h") - data := r.URL.Query().Get("d") - params := mux.Vars(r) - teamName := params["team"] - - if len(hash) == 0 || len(data) == 0 { - isResetLink = false - } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "") - return - } - } - - teamDisplayName := "Developer/Beta" - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - - if team != nil { - teamDisplayName = team.DisplayName - } - - page := NewHtmlTemplatePage("password_reset", "", c.Locale) - page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = teamDisplayName - page.Props["TeamName"] = teamName - page.Props["Hash"] = hash - page.Props["Data"] = data - page.Props["TeamName"] = teamName - page.Props["IsReset"] = strconv.FormatBool(isResetLink) - page.Render(c, w) -} - -func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - - if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - hash := r.URL.Query().Get("h") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if api.IsVerifyHashRequired(nil, team, hash) { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") - return - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") - return - } - - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return - } - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) - - if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { - c.Err = err - return - } else { - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - api.CreateOAuthUser(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_LOGIN: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - } - } -} - -func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - loginHint := r.URL.Query().Get("login_hint") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !c.HasSystemAdminPermissions("adminConsole") { - return - } - - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - params := mux.Vars(r) - activeTab := params["tab"] - teamId := params["team"] - - page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale) - page.User = user - page.Team = team - page.Props["ActiveTab"] = activeTab - page.Props["TeamId"] = teamId - page.Render(c, w) -} - -func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if !CheckBrowserCompatability(c, r) { - return - } - - responseType := r.URL.Query().Get("response_type") - clientId := r.URL.Query().Get("client_id") - redirect := r.URL.Query().Get("redirect_uri") - scope := r.URL.Query().Get("scope") - state := r.URL.Query().Get("state") - - if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = result.Err - return - } else { - app = result.Data.(*model.OAuthApp) - } - - var team *model.Team - if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) + page := utils.NewHTMLTemplate("root", c.Locale) + page.Props["Title"] = c.T("web.root.home_title") + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) } - - page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale) - page.Props["TeamName"] = team.Name - page.Props["AppName"] = app.Name - page.Props["ResponseType"] = responseType - page.Props["ClientId"] = clientId - page.Props["RedirectUri"] = redirect - page.Props["Scope"] = scope - page.Props["State"] = state - page.Render(c, w) -} - -func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - - r.ParseForm() - - grantType := r.FormValue("grant_type") - if grantType != model.ACCESS_TOKEN_GRANT_TYPE { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - return - } - - redirectUri := r.FormValue("redirect_uri") - - achan := api.Srv.Store.OAuth().GetApp(clientId) - tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) - - authData := api.GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := api.Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") - return - } - - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { - c.LogAudit("fail - auth code is invalid") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } else { - app = result.Data.(*model.OAuthApp) - } - - if !model.ComparePassword(app.ClientSecret, secret) { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } - - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } - - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - c.LogAudit("fail - auth code has been used previously") - accessData := result.Data.(*model.AccessData) - - // Revoke access token, related auth code, and session from DB as well as from cache - if err := api.RevokeAccessToken(accessData.Token); err != nil { - l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) - } - - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") - return - } - - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } - - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} - - if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - return - } else { - session = result.Data.(*model.Session) - api.AddSessionToCache(session) - } - - accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} - - if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - - c.LogAuditWithUserId(user.Id, "success") - - w.Write([]byte(accessRsp.ToJson())) -} - -func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - params := mux.Vars(r) - id := params["id"] - - hchan := api.Srv.Store.Webhook().GetIncoming(id) - - r.ParseForm() - - var parsedRequest *model.IncomingWebhookRequest - contentType := r.Header.Get("Content-Type") - if strings.Split(contentType, "; ")[0] == "application/json" { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) - } else { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) - } - - if parsedRequest == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") - return - } - - text := parsedRequest.Text - if len(text) == 0 && parsedRequest.Attachments == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "") - return - } - - channelName := parsedRequest.ChannelName - webhookType := parsedRequest.Type - - //attachments is in here for slack compatibility - if parsedRequest.Attachments != nil { - if len(parsedRequest.Props) == 0 { - parsedRequest.Props = make(model.StringInterface) - } - parsedRequest.Props["attachments"] = parsedRequest.Attachments - webhookType = model.POST_SLACK_ATTACHMENT - } - - var hook *model.IncomingWebhook - if result := <-hchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message) - return - } else { - hook = result.Data.(*model.IncomingWebhook) - } - - var channel *model.Channel - var cchan store.StoreChannel - - if len(channelName) != 0 { - if channelName[0] == '@' { - if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) - return - } else { - channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) - } - } else if channelName[0] == '#' { - channelName = channelName[1:] - } - - cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) - } else { - cchan = api.Srv.Store.Channel().Get(hook.ChannelId) - } - - overrideUsername := parsedRequest.Username - overrideIconUrl := parsedRequest.IconURL - - if result := <-cchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) - return - } else { - channel = result.Data.(*model.Channel) - } - - pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) - - // create a mock session - c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} - - if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") - return - } - - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { - c.Err = err - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("ok")) -} - -func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - - params := mux.Vars(r) - teamName := params["team"] - email := r.URL.Query().Get("email") - newType := r.URL.Query().Get("new_type") - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - authType := "" - if len(email) != 0 { - if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { - l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - user := uResult.Data.(*model.User) - authType = user.AuthService - - // if user is not logged in to their SSO account, ask them to log in - if len(authType) != 0 && user.Id != c.Session.UserId { - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL - stateProps["email"] = email - - if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } - } - } - } - - page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale) - page.Props["Email"] = email - page.Props["CurrentType"] = authType - page.Props["NewType"] = newType - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - page.Render(c, w) } -- cgit v1.2.3-1-g7c22