From 2e5617c29be69637acd384e85f795a0b343bec8d Mon Sep 17 00:00:00 2001 From: Corey Hulen Date: Thu, 21 Apr 2016 22:37:01 -0700 Subject: PLT-2057 User as a first class object (#2648) * Adding TeamMember to system * Fixing all unit tests on the backend * Fixing merge conflicts * Fixing merge conflict * Adding javascript unit tests * Adding TeamMember to system * Fixing all unit tests on the backend * Fixing merge conflicts * Fixing merge conflict * Adding javascript unit tests * Adding client side unit test * Cleaning up the clint side tests * Fixing msg * Adding more client side unit tests * Adding more using tests * Adding last bit of client side unit tests and adding make cmd * Fixing bad merge * Fixing libraries * Updating to new client side API * Fixing borken unit test * Fixing unit tests * ugg...trying to beat gofmt * ugg...trying to beat gofmt * Cleaning up remainder of the server side routes * Adding inital load api * Increased coverage of webhook unit tests (#2660) * Adding loading ... to root html * Fixing bad merge * Removing explicit content type so superagent will guess corectly (#2685) * Fixing merge and unit tests * Adding create team UI * Fixing signup flows * Adding LDAP unit tests and enterprise unit test helper (#2702) * Add the ability to reset MFA from the commandline (#2706) * Fixing compliance unit tests * Fixing client side tests * Adding open server to system console * Moving websocket connection * Fixing unit test * Fixing unit tests * Fixing unit tests * Adding nickname and more LDAP unit tests (#2717) * Adding join open teams * Cleaning up all TODOs in the code * Fixing web sockets * Removing unused webockets file * PLT-2533 Add the ability to reset a user's MFA from the system console (#2715) * Add the ability to reset a user's MFA from the system console * Add client side unit test for adminResetMfa * Reorganizing authentication to fix LDAP error message (#2723) * Fixing failing unit test * Initial upgrade db code * Adding upgrade script * Fixing upgrade script after running on core * Update OAuth and Claim routes to work with user model changes (#2739) * Fixing perminant deletion. Adding ability to delete all user and the entire database (#2740) * Fixing team invite ldap login call (#2741) * Fixing bluebar and some img stuff * Fix all the different file upload web utils (#2743) * Fixing invalid session redirect (#2744) * Redirect on bad channel name (#2746) * Fixing a bunch of issue and removing dead code * Patch to fix error message on leave channel (#2747) * Setting EnableOpenServer to false by default * Fixing config * Fixing upgrade * Fixing reported bugs * Bug fixes for PLT-2057 * PLT-2563 Redo password recovery to use a database table (#2745) * Redo password recovery to use a database table * Update reset password audits * Split out admin and user reset password APIs to be separate * Delete password recovery when user is permanently deleted * Consolidate password resetting into a single function * Removed private channels as an option for outgoing webhooks (#2752) * PLT-2577/PLT-2552 Fixes for backstage (#2753) * Added URL to incoming webhook list * Fixed client functions for adding/removing integrations * Disallowed slash commands without trigger words * Fixed clientside handling of errors on AddCommand page * Minor auth cleanup (#2758) * Changed EditPostModal to just close if you save without making any changes (#2759) * Renamed client -> Client in async_client.jsx and fixed eslint warnings (#2756) * Fixed url in channel info modal (#2755) * Fixing reported issues * Moving to version 3 of the apis * Fixing command unit tests (#2760) * Adding team admins * Fixing DM issue * Fixing eslint error * Properly set EditPostModal's originalText state in all cases (#2762) * Update client config check to assume features is defined if server is licensed (#2772) * Fixing url link * Fixing issue with websocket crashing when sending messages to different teams --- .gitignore | 2 + Godeps/Godeps.json | 5 + .../src/github.com/gorilla/handlers/.travis.yml | 17 + .../src/github.com/gorilla/handlers/LICENSE | 22 + .../src/github.com/gorilla/handlers/README.md | 53 + .../src/github.com/gorilla/handlers/canonical.go | 74 + .../src/github.com/gorilla/handlers/compress.go | 145 ++ .../src/github.com/gorilla/handlers/cors.go | 317 ++++ .../src/github.com/gorilla/handlers/doc.go | 9 + .../src/github.com/gorilla/handlers/handlers.go | 403 +++++ .../github.com/gorilla/handlers/proxy_headers.go | 113 ++ .../src/github.com/gorilla/handlers/recovery.go | 86 + Makefile | 64 +- api/admin.go | 83 +- api/admin_test.go | 324 ++-- api/api.go | 78 +- api/api_test.go | 47 - api/apitestlib.go | 223 +++ api/authentication.go | 99 ++ api/auto_channels.go | 12 +- api/auto_environment.go | 12 +- api/auto_posts.go | 2 +- api/auto_teams.go | 4 +- api/auto_users.go | 31 +- api/channel.go | 101 +- api/channel_benchmark_test.go | 115 +- api/channel_test.go | 368 ++-- api/command.go | 44 +- api/command_echo_test.go | 19 +- api/command_join.go | 2 +- api/command_join_test.go | 25 +- api/command_loadtest.go | 35 +- api/command_loadtest_test.go | 124 +- api/command_logout_test.go | 17 +- api/command_me_test.go | 23 +- api/command_msg.go | 8 +- api/command_msg_test.go | 40 +- api/command_shrug_test.go | 23 +- api/command_test.go | 119 +- api/context.go | 157 +- api/context_test.go | 51 +- api/export.go | 2 +- api/file.go | 34 +- api/file_benchmark_test.go | 13 +- api/file_test.go | 124 +- api/import.go | 6 +- api/license.go | 10 +- api/license_test.go | 3 +- api/oauth.go | 256 ++- api/oauth_test.go | 26 +- api/post.go | 110 +- api/post_benchmark_test.go | 30 +- api/post_test.go | 385 ++--- api/preference.go | 13 +- api/preference_test.go | 75 +- api/server.go | 4 +- api/sharding.go | 79 - api/slackimport.go | 3 +- api/team.go | 461 ++--- api/team_test.go | 245 ++- api/user.go | 1401 +++++++--------- api/user_test.go | 571 ++++--- api/web_conn.go | 78 +- api/web_hub.go | 128 +- api/web_socket.go | 7 +- api/web_socket_test.go | 44 +- api/web_team_hub.go | 123 -- api/webhook.go | 50 +- api/webhook_test.go | 733 +++++--- config/config.json | 5 +- einterfaces/ldap.go | 4 +- einterfaces/mfa.go | 2 +- i18n/en.json | 50 +- i18n/es.json | 20 - i18n/fr.json | 16 - i18n/pt.json | 20 - manualtesting/manual_testing.go | 10 +- mattermost.go | 430 ++++- model/client.go | 308 +++- model/command.go | 2 +- model/command_test.go | 81 +- model/config.go | 18 +- model/gitlab.go | 8 + model/gitlab/gitlab.go | 10 +- model/initial_load.go | 40 + model/initial_load_test.go | 20 + model/password_recovery.go | 37 + model/session.go | 32 +- model/team.go | 25 +- model/team_member.go | 78 + model/team_member_test.go | 43 + model/user.go | 20 +- model/user_test.go | 9 +- model/utils.go | 11 +- model/version.go | 1 + store/sql_audit_store.go | 11 - store/sql_channel_store.go | 42 +- store/sql_channel_store_test.go | 69 +- store/sql_command_store_test.go | 7 + store/sql_compliance_store_test.go | 4 +- store/sql_post_store.go | 14 +- store/sql_recovery_store.go | 124 ++ store/sql_recovery_store_test.go | 54 + store/sql_session_store.go | 45 +- store/sql_session_store_test.go | 24 +- store/sql_store.go | 128 +- store/sql_team_store.go | 189 ++- store/sql_team_store_test.go | 172 +- store/sql_user_store.go | 217 ++- store/sql_user_store_test.go | 272 ++- store/store.go | 34 +- utils/config.go | 13 +- utils/license.go | 4 +- utils/textgeneration.go | 20 +- web/web.go | 6 + web/web_test.go | 10 +- webapp/.eslintrc.json | 7 + webapp/Makefile | 2 +- webapp/action_creators/global_actions.jsx | 113 +- webapp/action_creators/websocket_actions.jsx | 19 +- webapp/client/client.jsx | 1463 ++++++++++++++++ webapp/components/activity_log_modal.jsx | 2 +- .../admin_console/admin_navbar_dropdown.jsx | 20 +- .../admin_console/admin_sidebar_header.jsx | 3 +- .../admin_console/compliance_reports.jsx | 4 +- .../admin_console/compliance_settings.jsx | 2 +- webapp/components/admin_console/email_settings.jsx | 2 +- .../components/admin_console/gitlab_settings.jsx | 2 +- webapp/components/admin_console/image_settings.jsx | 2 +- webapp/components/admin_console/ldap_settings.jsx | 32 +- .../admin_console/legal_and_support_settings.jsx | 2 +- .../components/admin_console/license_settings.jsx | 7 +- webapp/components/admin_console/log_settings.jsx | 2 +- .../components/admin_console/privacy_settings.jsx | 2 +- webapp/components/admin_console/rate_settings.jsx | 2 +- .../admin_console/reset_password_modal.jsx | 13 +- .../components/admin_console/service_settings.jsx | 2 +- webapp/components/admin_console/sql_settings.jsx | 2 +- webapp/components/admin_console/team_settings.jsx | 105 +- webapp/components/admin_console/team_users.jsx | 2 +- webapp/components/admin_console/user_item.jsx | 133 +- webapp/components/authorize.jsx | 2 +- webapp/components/backstage/add_command.jsx | 10 +- .../components/backstage/add_incoming_webhook.jsx | 5 +- .../components/backstage/add_outgoing_webhook.jsx | 7 +- webapp/components/backstage/backstage_navbar.jsx | 2 +- webapp/components/backstage/backstage_sidebar.jsx | 3 +- webapp/components/backstage/installed_commands.jsx | 3 +- .../backstage/installed_incoming_webhook.jsx | 11 + .../backstage/installed_incoming_webhooks.jsx | 3 +- .../backstage/installed_outgoing_webhooks.jsx | 3 +- webapp/components/backstage/integrations.jsx | 7 +- webapp/components/channel_header.jsx | 2 +- webapp/components/channel_info_modal.jsx | 30 +- webapp/components/channel_invite_button.jsx | 8 +- webapp/components/channel_members_modal.jsx | 7 +- webapp/components/channel_notifications_modal.jsx | 6 +- webapp/components/channel_select.jsx | 2 +- webapp/components/claim/claim.jsx | 32 +- .../components/claim/components/email_to_ldap.jsx | 47 +- .../components/claim/components/email_to_oauth.jsx | 23 +- .../components/claim/components/ldap_to_email.jsx | 33 +- .../components/claim/components/oauth_to_email.jsx | 18 +- webapp/components/create_comment.jsx | 3 +- webapp/components/create_post.jsx | 4 +- .../create_team/components/display_name.jsx | 136 ++ .../components/create_team/components/team_url.jsx | 229 +++ webapp/components/create_team/create_team.jsx | 72 + webapp/components/delete_channel_modal.jsx | 2 +- webapp/components/delete_post_modal.jsx | 2 +- webapp/components/do_verify_email.jsx | 8 +- webapp/components/edit_channel_header_modal.jsx | 2 +- webapp/components/edit_channel_purpose_modal.jsx | 11 +- webapp/components/edit_post_modal.jsx | 49 +- webapp/components/error_bar.jsx | 4 +- webapp/components/file_attachment.jsx | 4 +- webapp/components/file_upload.jsx | 46 +- webapp/components/filtered_user_list.jsx | 3 + webapp/components/header_footer_template.jsx | 74 + webapp/components/invite_member_modal.jsx | 2 +- webapp/components/logged_in.jsx | 147 +- webapp/components/login/login.jsx | 198 +-- webapp/components/member_list_team.jsx | 16 +- webapp/components/more_channels.jsx | 2 +- webapp/components/navbar.jsx | 2 +- webapp/components/navbar_dropdown.jsx | 59 +- webapp/components/needs_team.jsx | 148 +- webapp/components/new_channel_flow.jsx | 8 +- webapp/components/not_logged_in.jsx | 74 - webapp/components/notify_counts.jsx | 11 +- webapp/components/password_reset_form.jsx | 14 +- webapp/components/password_reset_send_link.jsx | 8 +- webapp/components/popover_list_members.jsx | 3 +- webapp/components/post.jsx | 4 +- webapp/components/register_app_modal.jsx | 2 +- webapp/components/rename_channel_modal.jsx | 2 +- webapp/components/rhs_comment.jsx | 6 +- webapp/components/rhs_header_post.jsx | 12 +- webapp/components/root.jsx | 50 +- webapp/components/search_bar.jsx | 2 +- webapp/components/select_team/select_team.jsx | 257 +++ webapp/components/should_verify_email.jsx | 20 +- webapp/components/sidebar_header.jsx | 3 +- webapp/components/sidebar_right_menu.jsx | 7 +- webapp/components/signup_team.jsx | 235 --- .../components/signup_team_complete.jsx | 1 + .../components/team_signup_display_name_page.jsx | 141 -- .../components/team_signup_email_item.jsx | 89 - .../components/team_signup_finished.jsx | 17 - .../components/team_signup_password_page.jsx | 225 --- .../components/team_signup_send_invites_page.jsx | 215 --- .../components/team_signup_url_page.jsx | 211 --- .../components/team_signup_username_page.jsx | 169 -- .../components/team_signup_welcome_page.jsx | 243 --- webapp/components/signup_team_confirm.jsx | 49 - webapp/components/signup_user_complete.jsx | 199 ++- .../components/suggestion/at_mention_provider.jsx | 3 +- .../components/suggestion/search_user_provider.jsx | 3 +- webapp/components/team_export_tab.jsx | 2 +- webapp/components/team_general_tab.jsx | 160 +- webapp/components/team_members_dropdown.jsx | 46 +- webapp/components/team_settings.jsx | 1 + webapp/components/team_signup_choose_auth.jsx | 132 -- webapp/components/team_signup_with_email.jsx | 116 -- webapp/components/team_signup_with_ldap.jsx | 231 --- webapp/components/team_signup_with_sso.jsx | 174 -- webapp/components/toggle_modal_button.jsx | 7 +- webapp/components/user_list.jsx | 10 + webapp/components/user_list_row.jsx | 11 +- webapp/components/user_profile.jsx | 3 +- .../user_settings/import_theme_modal.jsx | 2 +- .../components/user_settings/manage_languages.jsx | 2 +- .../user_settings/user_settings_general.jsx | 8 +- .../user_settings/user_settings_notifications.jsx | 2 +- .../user_settings/user_settings_security.jsx | 37 +- .../user_settings/user_settings_theme.jsx | 2 +- webapp/components/view_image.jsx | 2 +- webapp/i18n/en.json | 104 +- webapp/i18n/es.json | 64 +- webapp/i18n/fr.json | 65 +- webapp/i18n/ja.json | 55 - webapp/i18n/pt.json | 64 +- webapp/package.json | 17 +- webapp/root.html | 13 +- webapp/root.jsx | 463 +++--- webapp/sass/routes/_backstage.scss | 1 + webapp/sass/routes/_signup.scss | 11 + webapp/stores/browser_store.jsx | 12 - webapp/stores/error_store.jsx | 11 + webapp/stores/preference_store.jsx | 2 +- webapp/stores/team_store.jsx | 47 + webapp/stores/user_store.jsx | 33 +- webapp/tests/client_channel.test.jsx | 334 ++++ webapp/tests/client_command.test.jsx | 123 ++ webapp/tests/client_general.test.jsx | 333 ++++ webapp/tests/client_hooks.test.jsx | 139 ++ webapp/tests/client_oauth.test.jsx | 60 + webapp/tests/client_post.test.jsx | 207 +++ webapp/tests/client_preferences.test.jsx | 72 + webapp/tests/client_team.test.jsx | 235 +++ webapp/tests/client_user.test.jsx | 550 ++++++ webapp/tests/spinner_button.test.jsx | 24 + webapp/tests/test_helper.jsx | 183 ++ webapp/utils/async_client.jsx | 324 ++-- webapp/utils/channel_intro_messages.jsx | 3 +- webapp/utils/client.jsx | 1759 -------------------- webapp/utils/constants.jsx | 5 + webapp/utils/utils.jsx | 20 +- webapp/utils/web_client.jsx | 67 + webapp/webpack.config-test.js | 131 ++ webapp/webpack.config.js | 3 +- 271 files changed, 13497 insertions(+), 9969 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/README.md create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/compress.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/cors.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/doc.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/handlers.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/handlers/recovery.go delete mode 100644 api/api_test.go create mode 100644 api/apitestlib.go create mode 100644 api/authentication.go delete mode 100644 api/sharding.go delete mode 100644 api/web_team_hub.go create mode 100644 model/gitlab.go create mode 100644 model/initial_load.go create mode 100644 model/initial_load_test.go create mode 100644 model/password_recovery.go create mode 100644 model/team_member.go create mode 100644 model/team_member_test.go create mode 100644 store/sql_recovery_store.go create mode 100644 store/sql_recovery_store_test.go create mode 100644 webapp/client/client.jsx create mode 100644 webapp/components/create_team/components/display_name.jsx create mode 100644 webapp/components/create_team/components/team_url.jsx create mode 100644 webapp/components/create_team/create_team.jsx create mode 100644 webapp/components/header_footer_template.jsx delete mode 100644 webapp/components/not_logged_in.jsx create mode 100644 webapp/components/select_team/select_team.jsx delete mode 100644 webapp/components/signup_team.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_display_name_page.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_email_item.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_finished.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_password_page.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_send_invites_page.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_url_page.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_username_page.jsx delete mode 100644 webapp/components/signup_team_complete/components/team_signup_welcome_page.jsx delete mode 100644 webapp/components/signup_team_confirm.jsx delete mode 100644 webapp/components/team_signup_choose_auth.jsx delete mode 100644 webapp/components/team_signup_with_email.jsx delete mode 100644 webapp/components/team_signup_with_ldap.jsx delete mode 100644 webapp/components/team_signup_with_sso.jsx create mode 100644 webapp/tests/client_channel.test.jsx create mode 100644 webapp/tests/client_command.test.jsx create mode 100644 webapp/tests/client_general.test.jsx create mode 100644 webapp/tests/client_hooks.test.jsx create mode 100644 webapp/tests/client_oauth.test.jsx create mode 100644 webapp/tests/client_post.test.jsx create mode 100644 webapp/tests/client_preferences.test.jsx create mode 100644 webapp/tests/client_team.test.jsx create mode 100644 webapp/tests/client_user.test.jsx create mode 100644 webapp/tests/spinner_button.test.jsx create mode 100644 webapp/tests/test_helper.jsx delete mode 100644 webapp/utils/client.jsx create mode 100644 webapp/utils/web_client.jsx create mode 100644 webapp/webpack.config-test.js diff --git a/.gitignore b/.gitignore index 422c6d5f5..882c5a783 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ _testmain.go *mattermost.log *npm-debug.log* +.tmp + # Vim temporary files [._]*.s[a-w][a-z] [._]s[a-w][a-z] diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index aff57a2af..06798d010 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -9,6 +9,11 @@ "ImportPath": "github.com/NYTimes/gziphandler", "Rev": "a88790d49798560db24af70fb6a10a66e2549a72" }, + { + "ImportPath": "github.com/gorilla/handlers", + "Comment": "v1.1-4-gd0f2612", + "Rev": "d0f261246491e3a8613039e90764460448dc05f5" + }, { "ImportPath": "github.com/alecthomas/log4go", "Rev": "8e9057c3b25c409a34c0b9737cdc82cbcafeabce" diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/.travis.yml b/Godeps/_workspace/src/github.com/gorilla/handlers/.travis.yml new file mode 100644 index 000000000..ad1d76acc --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/.travis.yml @@ -0,0 +1,17 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.4 + - go: 1.5 + - go: 1.6 + +install: + - go get golang.org/x/tools/cmd/vet + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go tool vet . + - go test -v -race ./... diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE b/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE new file mode 100644 index 000000000..66ea3c8ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/README.md b/Godeps/_workspace/src/github.com/gorilla/handlers/README.md new file mode 100644 index 000000000..a782c4152 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/README.md @@ -0,0 +1,53 @@ +gorilla/handlers +================ +[![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) [![Build Status](https://travis-ci.org/gorilla/handlers.svg?branch=master)](https://travis-ci.org/gorilla/handlers) + +Package handlers is a collection of handlers (aka "HTTP middleware") for use +with Go's `net/http` package (or any framework supporting `http.Handler`), including: + +* [**LoggingHandler**](https://godoc.org/github.com/gorilla/handlers#LoggingHandler) for logging HTTP requests in the Apache [Common Log + Format](http://httpd.apache.org/docs/2.2/logs.html#common). +* [**CombinedLoggingHandler**](https://godoc.org/github.com/gorilla/handlers#CombinedLoggingHandler) for logging HTTP requests in the Apache [Combined Log + Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by + both Apache and nginx. +* [**CompressHandler**](https://godoc.org/github.com/gorilla/handlers#CompressHandler) for gzipping responses. +* [**ContentTypeHandler**](https://godoc.org/github.com/gorilla/handlers#ContentTypeHandler) for validating requests against a list of accepted + content types. +* [**MethodHandler**](https://godoc.org/github.com/gorilla/handlers#MethodHandler) for matching HTTP methods against handlers in a + `map[string]http.Handler` +* [**ProxyHeaders**](https://godoc.org/github.com/gorilla/handlers#ProxyHeaders) for populating `r.RemoteAddr` and `r.URL.Scheme` based on the + `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded` + headers when running a Go server behind a HTTP reverse proxy. +* [**CanonicalHost**](https://godoc.org/github.com/gorilla/handlers#CanonicalHost) for re-directing to the preferred host when handling multiple + domains (i.e. multiple CNAME aliases). +* [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics. + +Other handlers are documented [on the Gorilla +website](http://www.gorillatoolkit.org/pkg/handlers). + +## Example + +A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`: + +```go +import ( + "net/http" + "github.com/gorilla/handlers" +) + +func main() { + r := http.NewServeMux() + + // Only log requests to our admin dashboard to stdout + r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard))) + r.HandleFunc("/", ShowIndex) + + // Wrap our server with our gzip handler to gzip compress all responses. + http.ListenAndServe(":8000", handlers.CompressHandler(r)) +} +``` + +## License + +BSD licensed. See the included LICENSE file for details. + diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go b/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go new file mode 100644 index 000000000..8437fefc1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "net/http" + "net/url" + "strings" +) + +type canonical struct { + h http.Handler + domain string + code int +} + +// CanonicalHost is HTTP middleware that re-directs requests to the canonical +// domain. It accepts a domain and a status code (e.g. 301 or 302) and +// re-directs clients to this domain. The existing request path is maintained. +// +// Note: If the provided domain is considered invalid by url.Parse or otherwise +// returns an empty scheme or host, clients are not re-directed. +// +// Example: +// +// r := mux.NewRouter() +// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302) +// r.HandleFunc("/route", YourHandler) +// +// log.Fatal(http.ListenAndServe(":7000", canonical(r))) +// +func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler { + fn := func(h http.Handler) http.Handler { + return canonical{h, domain, code} + } + + return fn +} + +func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dest, err := url.Parse(c.domain) + if err != nil { + // Call the next handler if the provided domain fails to parse. + c.h.ServeHTTP(w, r) + return + } + + if dest.Scheme == "" || dest.Host == "" { + // Call the next handler if the scheme or host are empty. + // Note that url.Parse won't fail on in this case. + c.h.ServeHTTP(w, r) + return + } + + if !strings.EqualFold(cleanHost(r.Host), dest.Host) { + // Re-build the destination URL + dest := dest.Scheme + "://" + dest.Host + r.URL.Path + if r.URL.RawQuery != "" { + dest += "?" + r.URL.RawQuery + } + http.Redirect(w, r, dest, c.code) + return + } + + c.h.ServeHTTP(w, r) +} + +// cleanHost cleans invalid Host headers by stripping anything after '/' or ' '. +// This is backported from Go 1.5 (in response to issue #11206) and attempts to +// mitigate malformed Host headers that do not match the format in RFC7230. +func cleanHost(in string) string { + if i := strings.IndexAny(in, " /"); i != -1 { + return in[:i] + } + return in +} diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/compress.go b/Godeps/_workspace/src/github.com/gorilla/handlers/compress.go new file mode 100644 index 000000000..5e140c503 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/compress.go @@ -0,0 +1,145 @@ +// Copyright 2013 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package handlers + +import ( + "compress/flate" + "compress/gzip" + "io" + "net/http" + "strings" +) + +type compressResponseWriter struct { + io.Writer + http.ResponseWriter + http.Hijacker + http.Flusher + http.CloseNotifier +} + +func (w *compressResponseWriter) WriteHeader(c int) { + w.ResponseWriter.Header().Del("Content-Length") + w.ResponseWriter.WriteHeader(c) +} + +func (w *compressResponseWriter) Header() http.Header { + return w.ResponseWriter.Header() +} + +func (w *compressResponseWriter) Write(b []byte) (int, error) { + h := w.ResponseWriter.Header() + if h.Get("Content-Type") == "" { + h.Set("Content-Type", http.DetectContentType(b)) + } + h.Del("Content-Length") + + return w.Writer.Write(b) +} + +type flusher interface { + Flush() error +} + +func (w *compressResponseWriter) Flush() { + // Flush compressed data if compressor supports it. + if f, ok := w.Writer.(flusher); ok { + f.Flush() + } + // Flush HTTP response. + if w.Flusher != nil { + w.Flusher.Flush() + } +} + +// CompressHandler gzip compresses HTTP responses for clients that support it +// via the 'Accept-Encoding' header. +func CompressHandler(h http.Handler) http.Handler { + return CompressHandlerLevel(h, gzip.DefaultCompression) +} + +// CompressHandlerLevel gzip compresses HTTP responses with specified compression level +// for clients that support it via the 'Accept-Encoding' header. +// +// The compression level should be gzip.DefaultCompression, gzip.NoCompression, +// or any integer value between gzip.BestSpeed and gzip.BestCompression inclusive. +// gzip.DefaultCompression is used in case of invalid compression level. +func CompressHandlerLevel(h http.Handler, level int) http.Handler { + if level < gzip.DefaultCompression || level > gzip.BestCompression { + level = gzip.DefaultCompression + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + L: + for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { + switch strings.TrimSpace(enc) { + case "gzip": + w.Header().Set("Content-Encoding", "gzip") + w.Header().Add("Vary", "Accept-Encoding") + + gw, _ := gzip.NewWriterLevel(w, level) + defer gw.Close() + + h, hok := w.(http.Hijacker) + if !hok { /* w is not Hijacker... oh well... */ + h = nil + } + + f, fok := w.(http.Flusher) + if !fok { + f = nil + } + + cn, cnok := w.(http.CloseNotifier) + if !cnok { + cn = nil + } + + w = &compressResponseWriter{ + Writer: gw, + ResponseWriter: w, + Hijacker: h, + Flusher: f, + CloseNotifier: cn, + } + + break L + case "deflate": + w.Header().Set("Content-Encoding", "deflate") + w.Header().Add("Vary", "Accept-Encoding") + + fw, _ := flate.NewWriter(w, level) + defer fw.Close() + + h, hok := w.(http.Hijacker) + if !hok { /* w is not Hijacker... oh well... */ + h = nil + } + + f, fok := w.(http.Flusher) + if !fok { + f = nil + } + + cn, cnok := w.(http.CloseNotifier) + if !cnok { + cn = nil + } + + w = &compressResponseWriter{ + Writer: fw, + ResponseWriter: w, + Hijacker: h, + Flusher: f, + CloseNotifier: cn, + } + + break L + } + } + + h.ServeHTTP(w, r) + }) +} diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/cors.go b/Godeps/_workspace/src/github.com/gorilla/handlers/cors.go new file mode 100644 index 000000000..1f92d1ad4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/cors.go @@ -0,0 +1,317 @@ +package handlers + +import ( + "net/http" + "strconv" + "strings" +) + +// CORSOption represents a functional option for configuring the CORS middleware. +type CORSOption func(*cors) error + +type cors struct { + h http.Handler + allowedHeaders []string + allowedMethods []string + allowedOrigins []string + allowedOriginValidator OriginValidator + exposedHeaders []string + maxAge int + ignoreOptions bool + allowCredentials bool +} + +// OriginValidator takes an origin string and returns whether or not that origin is allowed. +type OriginValidator func(string) bool + +var ( + defaultCorsMethods = []string{"GET", "HEAD", "POST"} + defaultCorsHeaders = []string{"Accept", "Accept-Language", "Content-Language", "Origin"} + // (WebKit/Safari v9 sends the Origin header by default in AJAX requests) +) + +const ( + corsOptionMethod string = "OPTIONS" + corsAllowOriginHeader string = "Access-Control-Allow-Origin" + corsExposeHeadersHeader string = "Access-Control-Expose-Headers" + corsMaxAgeHeader string = "Access-Control-Max-Age" + corsAllowMethodsHeader string = "Access-Control-Allow-Methods" + corsAllowHeadersHeader string = "Access-Control-Allow-Headers" + corsAllowCredentialsHeader string = "Access-Control-Allow-Credentials" + corsRequestMethodHeader string = "Access-Control-Request-Method" + corsRequestHeadersHeader string = "Access-Control-Request-Headers" + corsOriginHeader string = "Origin" + corsVaryHeader string = "Vary" + corsOriginMatchAll string = "*" +) + +func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get(corsOriginHeader) + if !ch.isOriginAllowed(origin) { + ch.h.ServeHTTP(w, r) + return + } + + if r.Method == corsOptionMethod { + if ch.ignoreOptions { + ch.h.ServeHTTP(w, r) + return + } + + if _, ok := r.Header[corsRequestMethodHeader]; !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + method := r.Header.Get(corsRequestMethodHeader) + if !ch.isMatch(method, ch.allowedMethods) { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + requestHeaders := strings.Split(r.Header.Get(corsRequestHeadersHeader), ",") + allowedHeaders := []string{} + for _, v := range requestHeaders { + canonicalHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) + if canonicalHeader == "" || ch.isMatch(canonicalHeader, defaultCorsHeaders) { + continue + } + + if !ch.isMatch(canonicalHeader, ch.allowedHeaders) { + w.WriteHeader(http.StatusForbidden) + return + } + + allowedHeaders = append(allowedHeaders, canonicalHeader) + } + + if len(allowedHeaders) > 0 { + w.Header().Set(corsAllowHeadersHeader, strings.Join(allowedHeaders, ",")) + } + + if ch.maxAge > 0 { + w.Header().Set(corsMaxAgeHeader, strconv.Itoa(ch.maxAge)) + } + + if !ch.isMatch(method, defaultCorsMethods) { + w.Header().Set(corsAllowMethodsHeader, method) + } + } else { + if len(ch.exposedHeaders) > 0 { + w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ",")) + } + } + + if ch.allowCredentials { + w.Header().Set(corsAllowCredentialsHeader, "true") + } + + if len(ch.allowedOrigins) > 1 { + w.Header().Set(corsVaryHeader, corsOriginHeader) + } + + w.Header().Set(corsAllowOriginHeader, origin) + + if r.Method == corsOptionMethod { + return + } + ch.h.ServeHTTP(w, r) +} + +// CORS provides Cross-Origin Resource Sharing middleware. +// Example: +// +// import ( +// "net/http" +// +// "github.com/gorilla/handlers" +// "github.com/gorilla/mux" +// ) +// +// func main() { +// r := mux.NewRouter() +// r.HandleFunc("/users", UserEndpoint) +// r.HandleFunc("/projects", ProjectEndpoint) +// +// // Apply the CORS middleware to our top-level router, with the defaults. +// http.ListenAndServe(":8000", handlers.CORS()(r)) +// } +// +func CORS(opts ...CORSOption) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + ch := parseCORSOptions(opts...) + ch.h = h + return ch + } +} + +func parseCORSOptions(opts ...CORSOption) *cors { + ch := &cors{ + allowedMethods: defaultCorsMethods, + allowedHeaders: defaultCorsHeaders, + allowedOrigins: []string{corsOriginMatchAll}, + } + + for _, option := range opts { + option(ch) + } + + return ch +} + +// +// Functional options for configuring CORS. +// + +// AllowedHeaders adds the provided headers to the list of allowed headers in a +// CORS request. +// This is an append operation so the headers Accept, Accept-Language, +// and Content-Language are always allowed. +// Content-Type must be explicitly declared if accepting Content-Types other than +// application/x-www-form-urlencoded, multipart/form-data, or text/plain. +func AllowedHeaders(headers []string) CORSOption { + return func(ch *cors) error { + for _, v := range headers { + normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) + if normalizedHeader == "" { + continue + } + + if !ch.isMatch(normalizedHeader, ch.allowedHeaders) { + ch.allowedHeaders = append(ch.allowedHeaders, normalizedHeader) + } + } + + return nil + } +} + +// AllowedMethods can be used to explicitly allow methods in the +// Access-Control-Allow-Methods header. +// This is a replacement operation so you must also +// pass GET, HEAD, and POST if you wish to support those methods. +func AllowedMethods(methods []string) CORSOption { + return func(ch *cors) error { + ch.allowedMethods = []string{} + for _, v := range methods { + normalizedMethod := strings.ToUpper(strings.TrimSpace(v)) + if normalizedMethod == "" { + continue + } + + if !ch.isMatch(normalizedMethod, ch.allowedMethods) { + ch.allowedMethods = append(ch.allowedMethods, normalizedMethod) + } + } + + return nil + } +} + +// AllowedOrigins sets the allowed origins for CORS requests, as used in the +// 'Allow-Access-Control-Origin' HTTP header. +// Note: Passing in a []string{"*"} will allow any domain. +func AllowedOrigins(origins []string) CORSOption { + return func(ch *cors) error { + for _, v := range origins { + if v == corsOriginMatchAll { + ch.allowedOrigins = []string{corsOriginMatchAll} + return nil + } + } + + ch.allowedOrigins = origins + return nil + } +} + +// AllowedOriginValidator sets a function for evaluating allowed origins in CORS requests, represented by the +// 'Allow-Access-Control-Origin' HTTP header. +func AllowedOriginValidator(fn OriginValidator) CORSOption { + return func(ch *cors) error { + ch.allowedOriginValidator = fn + return nil + } +} + +// ExposeHeaders can be used to specify headers that are available +// and will not be stripped out by the user-agent. +func ExposedHeaders(headers []string) CORSOption { + return func(ch *cors) error { + ch.exposedHeaders = []string{} + for _, v := range headers { + normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) + if normalizedHeader == "" { + continue + } + + if !ch.isMatch(normalizedHeader, ch.exposedHeaders) { + ch.exposedHeaders = append(ch.exposedHeaders, normalizedHeader) + } + } + + return nil + } +} + +// MaxAge determines the maximum age (in seconds) between preflight requests. A +// maximum of 10 minutes is allowed. An age above this value will default to 10 +// minutes. +func MaxAge(age int) CORSOption { + return func(ch *cors) error { + // Maximum of 10 minutes. + if age > 600 { + age = 600 + } + + ch.maxAge = age + return nil + } +} + +// IgnoreOptions causes the CORS middleware to ignore OPTIONS requests, instead +// passing them through to the next handler. This is useful when your application +// or framework has a pre-existing mechanism for responding to OPTIONS requests. +func IgnoreOptions() CORSOption { + return func(ch *cors) error { + ch.ignoreOptions = true + return nil + } +} + +// AllowCredentials can be used to specify that the user agent may pass +// authentication details along with the request. +func AllowCredentials() CORSOption { + return func(ch *cors) error { + ch.allowCredentials = true + return nil + } +} + +func (ch *cors) isOriginAllowed(origin string) bool { + if origin == "" { + return false + } + + if ch.allowedOriginValidator != nil { + return ch.allowedOriginValidator(origin) + } + + for _, allowedOrigin := range ch.allowedOrigins { + if allowedOrigin == origin || allowedOrigin == corsOriginMatchAll { + return true + } + } + + return false +} + +func (ch *cors) isMatch(needle string, haystack []string) bool { + for _, v := range haystack { + if v == needle { + return true + } + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go b/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go new file mode 100644 index 000000000..944e5a8ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go @@ -0,0 +1,9 @@ +/* +Package handlers is a collection of handlers (aka "HTTP middleware") for use +with Go's net/http package (or any framework supporting http.Handler). + +The package includes handlers for logging in standardised formats, compressing +HTTP responses, validating content types and other useful tools for manipulating +requests and responses. +*/ +package handlers diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/handlers.go b/Godeps/_workspace/src/github.com/gorilla/handlers/handlers.go new file mode 100644 index 000000000..9544d2f0a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/handlers.go @@ -0,0 +1,403 @@ +// Copyright 2013 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package handlers + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +// MethodHandler is an http.Handler that dispatches to a handler whose key in the +// MethodHandler's map matches the name of the HTTP request's method, eg: GET +// +// If the request's method is OPTIONS and OPTIONS is not a key in the map then +// the handler responds with a status of 200 and sets the Allow header to a +// comma-separated list of available methods. +// +// If the request's method doesn't match any of its keys the handler responds +// with a status of HTTP 405 "Method Not Allowed" and sets the Allow header to a +// comma-separated list of available methods. +type MethodHandler map[string]http.Handler + +func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if handler, ok := h[req.Method]; ok { + handler.ServeHTTP(w, req) + } else { + allow := []string{} + for k := range h { + allow = append(allow, k) + } + sort.Strings(allow) + w.Header().Set("Allow", strings.Join(allow, ", ")) + if req.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + } +} + +// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its +// friends +type loggingHandler struct { + writer io.Writer + handler http.Handler +} + +// combinedLoggingHandler is the http.Handler implementation for LoggingHandlerTo +// and its friends +type combinedLoggingHandler struct { + writer io.Writer + handler http.Handler +} + +func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + t := time.Now() + logger := makeLogger(w) + url := *req.URL + h.handler.ServeHTTP(logger, req) + writeLog(h.writer, req, url, t, logger.Status(), logger.Size()) +} + +func (h combinedLoggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + t := time.Now() + logger := makeLogger(w) + url := *req.URL + h.handler.ServeHTTP(logger, req) + writeCombinedLog(h.writer, req, url, t, logger.Status(), logger.Size()) +} + +func makeLogger(w http.ResponseWriter) loggingResponseWriter { + var logger loggingResponseWriter = &responseLogger{w: w} + if _, ok := w.(http.Hijacker); ok { + logger = &hijackLogger{responseLogger{w: w}} + } + h, ok1 := logger.(http.Hijacker) + c, ok2 := w.(http.CloseNotifier) + if ok1 && ok2 { + return hijackCloseNotifier{logger, h, c} + } + if ok2 { + return &closeNotifyWriter{logger, c} + } + return logger +} + +type loggingResponseWriter interface { + http.ResponseWriter + http.Flusher + Status() int + Size() int +} + +// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP +// status code and body size +type responseLogger struct { + w http.ResponseWriter + status int + size int +} + +func (l *responseLogger) Header() http.Header { + return l.w.Header() +} + +func (l *responseLogger) Write(b []byte) (int, error) { + if l.status == 0 { + // The status will be StatusOK if WriteHeader has not been called yet + l.status = http.StatusOK + } + size, err := l.w.Write(b) + l.size += size + return size, err +} + +func (l *responseLogger) WriteHeader(s int) { + l.w.WriteHeader(s) + l.status = s +} + +func (l *responseLogger) Status() int { + return l.status +} + +func (l *responseLogger) Size() int { + return l.size +} + +func (l *responseLogger) Flush() { + f, ok := l.w.(http.Flusher) + if ok { + f.Flush() + } +} + +type hijackLogger struct { + responseLogger +} + +func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { + h := l.responseLogger.w.(http.Hijacker) + conn, rw, err := h.Hijack() + if err == nil && l.responseLogger.status == 0 { + // The status will be StatusSwitchingProtocols if there was no error and + // WriteHeader has not been called yet + l.responseLogger.status = http.StatusSwitchingProtocols + } + return conn, rw, err +} + +type closeNotifyWriter struct { + loggingResponseWriter + http.CloseNotifier +} + +type hijackCloseNotifier struct { + loggingResponseWriter + http.Hijacker + http.CloseNotifier +} + +const lowerhex = "0123456789abcdef" + +func appendQuoted(buf []byte, s string) []byte { + var runeTmp [utf8.UTFMax]byte + for width := 0; len(s) > 0; s = s[width:] { + r := rune(s[0]) + width = 1 + if r >= utf8.RuneSelf { + r, width = utf8.DecodeRuneInString(s) + } + if width == 1 && r == utf8.RuneError { + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[s[0]>>4]) + buf = append(buf, lowerhex[s[0]&0xF]) + continue + } + if r == rune('"') || r == '\\' { // always backslashed + buf = append(buf, '\\') + buf = append(buf, byte(r)) + continue + } + if strconv.IsPrint(r) { + n := utf8.EncodeRune(runeTmp[:], r) + buf = append(buf, runeTmp[:n]...) + continue + } + switch r { + case '\a': + buf = append(buf, `\a`...) + case '\b': + buf = append(buf, `\b`...) + case '\f': + buf = append(buf, `\f`...) + case '\n': + buf = append(buf, `\n`...) + case '\r': + buf = append(buf, `\r`...) + case '\t': + buf = append(buf, `\t`...) + case '\v': + buf = append(buf, `\v`...) + default: + switch { + case r < ' ': + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[s[0]>>4]) + buf = append(buf, lowerhex[s[0]&0xF]) + case r > utf8.MaxRune: + r = 0xFFFD + fallthrough + case r < 0x10000: + buf = append(buf, `\u`...) + for s := 12; s >= 0; s -= 4 { + buf = append(buf, lowerhex[r>>uint(s)&0xF]) + } + default: + buf = append(buf, `\U`...) + for s := 28; s >= 0; s -= 4 { + buf = append(buf, lowerhex[r>>uint(s)&0xF]) + } + } + } + } + return buf + +} + +// buildCommonLogLine builds a log entry for req in Apache Common Log Format. +// ts is the timestamp with which the entry should be logged. +// status and size are used to provide the response HTTP status and size. +func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte { + username := "-" + if url.User != nil { + if name := url.User.Username(); name != "" { + username = name + } + } + + host, _, err := net.SplitHostPort(req.RemoteAddr) + + if err != nil { + host = req.RemoteAddr + } + + uri := req.RequestURI + + // Requests using the CONNECT method over HTTP/2.0 must use + // the authority field (aka r.Host) to identify the target. + // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT + if req.ProtoMajor == 2 && req.Method == "CONNECT" { + uri = req.Host + } + if uri == "" { + uri = url.RequestURI() + } + + buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) + buf = append(buf, host...) + buf = append(buf, " - "...) + buf = append(buf, username...) + buf = append(buf, " ["...) + buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...) + buf = append(buf, `] "`...) + buf = append(buf, req.Method...) + buf = append(buf, " "...) + buf = appendQuoted(buf, uri) + buf = append(buf, " "...) + buf = append(buf, req.Proto...) + buf = append(buf, `" `...) + buf = append(buf, strconv.Itoa(status)...) + buf = append(buf, " "...) + buf = append(buf, strconv.Itoa(size)...) + return buf +} + +// writeLog writes a log entry for req to w in Apache Common Log Format. +// ts is the timestamp with which the entry should be logged. +// status and size are used to provide the response HTTP status and size. +func writeLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) { + buf := buildCommonLogLine(req, url, ts, status, size) + buf = append(buf, '\n') + w.Write(buf) +} + +// writeCombinedLog writes a log entry for req to w in Apache Combined Log Format. +// ts is the timestamp with which the entry should be logged. +// status and size are used to provide the response HTTP status and size. +func writeCombinedLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) { + buf := buildCommonLogLine(req, url, ts, status, size) + buf = append(buf, ` "`...) + buf = appendQuoted(buf, req.Referer()) + buf = append(buf, `" "`...) + buf = appendQuoted(buf, req.UserAgent()) + buf = append(buf, '"', '\n') + w.Write(buf) +} + +// CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in +// Apache Combined Log Format. +// +// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. +// +// LoggingHandler always sets the ident field of the log to - +func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { + return combinedLoggingHandler{out, h} +} + +// LoggingHandler return a http.Handler that wraps h and logs requests to out in +// Apache Common Log Format (CLF). +// +// See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format. +// +// LoggingHandler always sets the ident field of the log to - +// +// Example: +// +// r := mux.NewRouter() +// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("This is a catch-all route")) +// }) +// loggedRouter := handlers.LoggingHandler(os.Stdout, r) +// http.ListenAndServe(":1123", loggedRouter) +// +func LoggingHandler(out io.Writer, h http.Handler) http.Handler { + return loggingHandler{out, h} +} + +// isContentType validates the Content-Type header matches the supplied +// contentType. That is, its type and subtype match. +func isContentType(h http.Header, contentType string) bool { + ct := h.Get("Content-Type") + if i := strings.IndexRune(ct, ';'); i != -1 { + ct = ct[0:i] + } + return ct == contentType +} + +// ContentTypeHandler wraps and returns a http.Handler, validating the request +// content type is compatible with the contentTypes list. It writes a HTTP 415 +// error if that fails. +// +// Only PUT, POST, and PATCH requests are considered. +func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") { + h.ServeHTTP(w, r) + return + } + + for _, ct := range contentTypes { + if isContentType(r.Header, ct) { + h.ServeHTTP(w, r) + return + } + } + http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType) + }) +} + +const ( + // HTTPMethodOverrideHeader is a commonly used + // http header to override a request method. + HTTPMethodOverrideHeader = "X-HTTP-Method-Override" + // HTTPMethodOverrideFormKey is a commonly used + // HTML form key to override a request method. + HTTPMethodOverrideFormKey = "_method" +) + +// HTTPMethodOverrideHandler wraps and returns a http.Handler which checks for +// the X-HTTP-Method-Override header or the _method form key, and overrides (if +// valid) request.Method with its value. +// +// This is especially useful for HTTP clients that don't support many http verbs. +// It isn't secure to override e.g a GET to a POST, so only POST requests are +// considered. Likewise, the override method can only be a "write" method: PUT, +// PATCH or DELETE. +// +// Form method takes precedence over header method. +func HTTPMethodOverrideHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + om := r.FormValue(HTTPMethodOverrideFormKey) + if om == "" { + om = r.Header.Get(HTTPMethodOverrideHeader) + } + if om == "PUT" || om == "PATCH" || om == "DELETE" { + r.Method = om + } + } + h.ServeHTTP(w, r) + }) +} diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go b/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go new file mode 100644 index 000000000..268de9c6a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "net/http" + "regexp" + "strings" +) + +var ( + // De-facto standard header keys. + xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") + xRealIP = http.CanonicalHeaderKey("X-Real-IP") + xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Scheme") +) + +var ( + // RFC7239 defines a new "Forwarded: " header designed to replace the + // existing use of X-Forwarded-* headers. + // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 + forwarded = http.CanonicalHeaderKey("Forwarded") + // Allows for a sub-match of the first value after 'for=' to the next + // comma, semi-colon or space. The match is case-insensitive. + forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`) + // Allows for a sub-match for the first instance of scheme (http|https) + // prefixed by 'proto='. The match is case-insensitive. + protoRegex = regexp.MustCompile(`(?i)(?:proto=)(https|http)`) +) + +// ProxyHeaders inspects common reverse proxy headers and sets the corresponding +// fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP +// for the remote (client) IP address, X-Forwarded-Proto for the scheme +// (http|https) and the RFC7239 Forwarded header, which may include both client +// IPs and schemes. +// +// NOTE: This middleware should only be used when behind a reverse +// proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are +// configured not to) strip these headers from client requests, or where these +// headers are accepted "as is" from a remote client (e.g. when Go is not behind +// a proxy), can manifest as a vulnerability if your application uses these +// headers for validating the 'trustworthiness' of a request. +func ProxyHeaders(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // Set the remote IP with the value passed from the proxy. + if fwd := getIP(r); fwd != "" { + r.RemoteAddr = fwd + } + + // Set the scheme (proto) with the value passed from the proxy. + if scheme := getScheme(r); scheme != "" { + r.URL.Scheme = scheme + } + + // Call the next handler in the chain. + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} + +// getIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239 +// Forwarded headers (in that order). +func getIP(r *http.Request) string { + var addr string + + if fwd := r.Header.Get(xForwardedFor); fwd != "" { + // Only grab the first (client) address. Note that '192.168.0.1, + // 10.1.1.1' is a valid key for X-Forwarded-For where addresses after + // the first may represent forwarding proxies earlier in the chain. + s := strings.Index(fwd, ", ") + if s == -1 { + s = len(fwd) + } + addr = fwd[:s] + } else if fwd := r.Header.Get(xRealIP); fwd != "" { + // X-Real-IP should only contain one IP address (the client making the + // request). + addr = fwd + } else if fwd := r.Header.Get(forwarded); fwd != "" { + // match should contain at least two elements if the protocol was + // specified in the Forwarded header. The first element will always be + // the 'for=' capture, which we ignore. In the case of multiple IP + // addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only + // extract the first, which should be the client IP. + if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 { + // IPv6 addresses in Forwarded headers are quoted-strings. We strip + // these quotes. + addr = strings.Trim(match[1], `"`) + } + } + + return addr +} + +// getScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239 +// Forwarded headers (in that order). +func getScheme(r *http.Request) string { + var scheme string + + // Retrieve the scheme from X-Forwarded-Proto. + if proto := r.Header.Get(xForwardedProto); proto != "" { + scheme = strings.ToLower(proto) + } else if proto := r.Header.Get(forwarded); proto != "" { + // match should contain at least two elements if the protocol was + // specified in the Forwarded header. The first element will always be + // the 'proto=' capture, which we ignore. In the case of multiple proto + // parameters (invalid) we only extract the first. + if match := protoRegex.FindStringSubmatch(proto); len(match) > 1 { + scheme = strings.ToLower(match[1]) + } + } + + return scheme +} diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/recovery.go b/Godeps/_workspace/src/github.com/gorilla/handlers/recovery.go new file mode 100644 index 000000000..65b7de58a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/handlers/recovery.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "log" + "net/http" + "runtime/debug" +) + +type recoveryHandler struct { + handler http.Handler + logger *log.Logger + printStack bool +} + +// RecoveryOption provides a functional approach to define +// configuration for a handler; such as setting the logging +// whether or not to print strack traces on panic. +type RecoveryOption func(http.Handler) + +func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler { + for _, option := range opts { + option(h) + } + + return h +} + +// RecoveryHandler is HTTP middleware that recovers from a panic, +// logs the panic, writes http.StatusInternalServerError, and +// continues to the next handler. +// +// Example: +// +// r := mux.NewRouter() +// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// panic("Unexpected error!") +// }) +// +// http.ListenAndServe(":1123", handlers.RecoveryHandler()(r)) +func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + r := &recoveryHandler{handler: h} + return parseRecoveryOptions(r, opts...) + } +} + +// RecoveryLogger is a functional option to override +// the default logger +func RecoveryLogger(logger *log.Logger) RecoveryOption { + return func(h http.Handler) { + r := h.(*recoveryHandler) + r.logger = logger + } +} + +// PrintRecoveryStack is a functional option to enable +// or disable printing stack traces on panic. +func PrintRecoveryStack(print bool) RecoveryOption { + return func(h http.Handler) { + r := h.(*recoveryHandler) + r.printStack = print + } +} + +func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + defer func() { + if err := recover(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + h.log(err) + } + }() + + h.handler.ServeHTTP(w, req) +} + +func (h recoveryHandler) log(message interface{}) { + if h.logger != nil { + h.logger.Println(message) + } else { + log.Println(message) + } + + if h.printStack { + debug.PrintStack() + } +} diff --git a/Makefile b/Makefile index a20086652..0d22d392d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build package run stop run-client run-server stop-client stop-server restart-server restart-client start-docker clean-dist clean nuke check-style check-unit-tests test dist setup-mac prepare-enteprise build-linux build-osx build-windows +.PHONY: build package run stop run-client run-server stop-client stop-server restart-server restart-client start-docker clean-dist clean nuke check-style check-unit-tests test dist setup-mac prepare-enteprise run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows # Build Flags BUILD_NUMBER ?= $(BUILD_NUMBER:) @@ -63,10 +63,30 @@ start-docker: echo starting mattermost-postgres; \ docker run --name mattermost-postgres -p 5432:5432 -e POSTGRES_USER=mmuser -e POSTGRES_PASSWORD=mostest \ -d postgres:9.4 > /dev/null; \ - sleep 10; \ elif [ $(shell docker ps | grep -ci mattermost-postgres) -eq 0 ]; then \ echo restarting mattermost-postgres; \ docker start mattermost-postgres > /dev/null; \ + fi + + @echo Ldap test user test.one + @if [ $(shell docker ps -a | grep -ci mattermost-openldap) -eq 0 ]; then \ + echo starting mattermost-openldap; \ + docker run --name mattermost-openldap -p 389:389 -p 636:636 \ + -e LDAP_TLS_VERIFY_CLIENT="never" \ + -e LDAP_ORGANISATION="Mattermost Test" \ + -e LDAP_DOMAIN="mm.test.com" \ + -e LDAP_ADMIN_PASSWORD="mostest" \ + -d osixia/openldap:1.1.2 > /dev/null;\ + sleep 10; \ + docker exec -ti mattermost-openldap bash -c 'echo -e "dn: ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: organizationalunit" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';\ + docker exec -ti mattermost-openldap bash -c 'echo -e "dn: uid=test.one,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: iNetOrgPerson\nsn: User\ncn: Test1\nmail: success+testone@simulator.amazonses.com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';\ + docker exec -ti mattermost-openldap bash -c 'ldappasswd -s Password1 -D "cn=admin,dc=mm,dc=test,dc=com" -x "uid=test.one,ou=testusers,dc=mm,dc=test,dc=com" -w mostest';\ + docker exec -ti mattermost-openldap bash -c 'echo -e "dn: uid=test.two,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: iNetOrgPerson\nsn: User\ncn: Test2\nmail: success+testtwo@simulator.amazonses.com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';\ + docker exec -ti mattermost-openldap bash -c 'ldappasswd -s Password1 -D "cn=admin,dc=mm,dc=test,dc=com" -x "uid=test.two,ou=testusers,dc=mm,dc=test,dc=com" -w mostest';\ + docker exec -ti mattermost-openldap bash -c 'echo -e "dn: cn=tgroup,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: groupOfUniqueNames\nuniqueMember: uid=test.one,ou=testusers,dc=mm,dc=test,dc=com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';\ + elif [ $(shell docker ps | grep -ci mattermost-openldap) -eq 0 ]; then \ + echo restarting mattermost-openldap; \ + docker start mattermost-openldap > /dev/null; \ sleep 10; \ fi @@ -82,22 +102,33 @@ stop-docker: echo stopping mattermost-postgres; \ docker stop mattermost-postgres > /dev/null; \ fi + + @if [ $(shell docker ps -a | grep -ci mattermost-openldap) -eq 1 ]; then \ + echo stopping mattermost-openldap; \ + docker stop mattermost-openldap > /dev/null; \ + fi clean-docker: @echo Removing docker containers @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 1 ]; then \ - echo stopping mattermost-mysql; \ + echo removing mattermost-mysql; \ docker stop mattermost-mysql > /dev/null; \ docker rm -v mattermost-mysql > /dev/null; \ fi @if [ $(shell docker ps -a | grep -ci mattermost-postgres) -eq 1 ]; then \ - echo stopping mattermost-postgres; \ + echo removing mattermost-postgres; \ docker stop mattermost-postgres > /dev/null; \ docker rm -v mattermost-postgres > /dev/null; \ fi + @if [ $(shell docker ps -a | grep -ci mattermost-openldap) -eq 1 ]; then \ + echo removing mattermost-openldap; \ + docker stop mattermost-openldap > /dev/null; \ + docker rm -v mattermost-openldap > /dev/null; \ + fi + check-style: @echo Running GOFMT $(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ einterfaces/ mattermost.go 2>&1)) @@ -110,11 +141,32 @@ check-style: fi test: start-docker - $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 + @echo Running tests + + $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=240s ./api || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 +ifeq ($(BUILD_ENTERPRISE_READY),true) + @echo Running Enterprise tests + $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s $(BUILD_ENTERPRISE_DIR)/ldap || exit 1 + $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s $(BUILD_ENTERPRISE_DIR)/compliance || exit 1 +endif + +setup-run-client-tests: + sed -i'.bak' 's|"EnableOpenServer": false,|"EnableOpenServer": true,|g' config/config.json + +cleanup-run-client-tests: + sed -i'.bak' 's|"EnableOpenServer": true,|"EnableOpenServer": false,|g' config/config.json + +run-client-tests: + cd $(BUILD_WEBAPP_DIR) && $(MAKE) test + sleep 10 + @echo Running client side unit tests + cd $(BUILD_WEBAPP_DIR) && npm test + +test-client: setup-run-client-tests run-server run-client-tests stop-server cleanup-run-client-tests .prebuild: @echo Preparation for running go code @@ -124,7 +176,7 @@ test: start-docker prepare-enterprise: ifeq ($(BUILD_ENTERPRISE_READY),true) - @echo Enterprise build selected, perparing + @echo Enterprise build selected, preparing cp $(BUILD_ENTERPRISE_DIR)/imports.go . endif diff --git a/api/admin.go b/api/admin.go index 3ed2bee7a..930170619 100644 --- a/api/admin.go +++ b/api/admin.go @@ -19,24 +19,25 @@ import ( "github.com/mssola/user_agent" ) -func InitAdmin(r *mux.Router) { +func InitAdmin() { l4g.Debug(utils.T("api.admin.init.debug")) - sr := r.PathPrefix("/admin").Subrouter() - sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET") - sr.Handle("/audits", ApiUserRequired(getAllAudits)).Methods("GET") - sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET") - sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST") - sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST") - sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") - sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") - sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") - sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") - sr.Handle("/save_compliance_report", ApiUserRequired(saveComplianceReport)).Methods("POST") - sr.Handle("/compliance_reports", ApiUserRequired(getComplianceReports)).Methods("GET") - sr.Handle("/download_compliance_report/{id:[A-Za-z0-9]+}", ApiUserRequired(downloadComplianceReport)).Methods("GET") - sr.Handle("/upload_brand_image", ApiAdminSystemRequired(uploadBrandImage)).Methods("POST") - sr.Handle("/get_brand_image", ApiAppHandlerTrustRequester(getBrandImage)).Methods("GET") + BaseRoutes.Admin.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET") + BaseRoutes.Admin.Handle("/audits", ApiUserRequired(getAllAudits)).Methods("GET") + BaseRoutes.Admin.Handle("/config", ApiUserRequired(getConfig)).Methods("GET") + BaseRoutes.Admin.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST") + BaseRoutes.Admin.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST") + BaseRoutes.Admin.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") + BaseRoutes.Admin.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") + BaseRoutes.Admin.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") + BaseRoutes.Admin.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") + BaseRoutes.Admin.Handle("/save_compliance_report", ApiUserRequired(saveComplianceReport)).Methods("POST") + BaseRoutes.Admin.Handle("/compliance_reports", ApiUserRequired(getComplianceReports)).Methods("GET") + BaseRoutes.Admin.Handle("/download_compliance_report/{id:[A-Za-z0-9]+}", ApiUserRequired(downloadComplianceReport)).Methods("GET") + BaseRoutes.Admin.Handle("/upload_brand_image", ApiAdminSystemRequired(uploadBrandImage)).Methods("POST") + BaseRoutes.Admin.Handle("/get_brand_image", ApiAppHandlerTrustRequester(getBrandImage)).Methods("GET") + BaseRoutes.Admin.Handle("/reset_mfa", ApiAdminSystemRequired(adminResetMfa)).Methods("POST") + BaseRoutes.Admin.Handle("/reset_password", ApiAdminSystemRequired(adminResetPassword)).Methods("POST") } func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { @@ -374,7 +375,7 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId) oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId) commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId) - sessionChan := Srv.Store.Session().AnalyticsSessionCount(teamId) + sessionChan := Srv.Store.Session().AnalyticsSessionCount() if r := <-fileChan; r.Err != nil { c.Err = r.Err @@ -498,3 +499,51 @@ func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(img) } } + +func adminResetMfa(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("adminResetMfa", "user_id") + return + } + + if err := DeactivateMfa(userId); err != nil { + c.Err = err + return + } + + c.LogAudit("") + + rdata := map[string]string{} + rdata["status"] = "ok" + w.Write([]byte(model.MapToJson(rdata))) +} + +func adminResetPassword(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("adminResetPassword", "user_id") + return + } + + newPassword := props["new_password"] + if len(newPassword) < model.MIN_PASSWORD_LENGTH { + c.SetInvalidParam("adminResetPassword", "new_password") + return + } + + if err := ResetPassword(c, userId, newPassword); err != nil { + c.Err = err + return + } + + c.LogAudit("") + + rdata := map[string]string{} + rdata["status"] = "ok" + w.Write([]byte(model.MapToJson(rdata))) +} diff --git a/api/admin_test.go b/api/admin_test.go index 67bc1d38b..2edc151bd 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -7,33 +7,18 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "strings" "testing" ) func TestGetLogs(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin().InitBasic() - 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.GetLogs(); err == nil { + if _, err := th.BasicClient.GetLogs(); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if logs, err := Client.GetLogs(); err != nil { + if logs, err := th.SystemAdminClient.GetLogs(); err != nil { t.Fatal(err) } else if len(logs.Data.([]string)) <= 0 { t.Fatal() @@ -41,29 +26,13 @@ func TestGetLogs(t *testing.T) { } func TestGetAllAudits(t *testing.T) { - Setup() + th := Setup().InitBasic().InitSystemAdmin() - 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.GetAllAudits(); err == nil { + if _, err := th.BasicClient.GetAllAudits(); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if audits, err := Client.GetAllAudits(); err != nil { + if audits, err := th.SystemAdminClient.GetAllAudits(); err != nil { t.Fatal(err) } else if len(audits.Data.(model.Audits)) <= 0 { t.Fatal() @@ -71,9 +40,9 @@ func TestGetAllAudits(t *testing.T) { } func TestGetClientProperties(t *testing.T) { - Setup() + th := Setup().InitBasic() - if result, err := Client.GetClientProperties(); err != nil { + if result, err := th.BasicClient.GetClientProperties(); err != nil { t.Fatal(err) } else { props := result.Data.(map[string]string) @@ -85,29 +54,13 @@ func TestGetClientProperties(t *testing.T) { } func TestGetConfig(t *testing.T) { - Setup() + th := Setup().InitBasic().InitSystemAdmin() - 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.GetConfig(); err == nil { + if _, err := th.BasicClient.GetConfig(); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if result, err := Client.GetConfig(); err != nil { + if result, err := th.SystemAdminClient.GetConfig(); err != nil { t.Fatal(err) } else { cfg := result.Data.(*model.Config) @@ -119,29 +72,15 @@ func TestGetConfig(t *testing.T) { } func TestSaveConfig(t *testing.T) { - Setup() + th := Setup().InitBasic().InitSystemAdmin() - 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.SaveConfig(utils.Cfg); err == nil { + if _, err := th.BasicClient.SaveConfig(utils.Cfg); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") + *utils.Cfg.TeamSettings.EnableOpenServer = false - if result, err := Client.SaveConfig(utils.Cfg); err != nil { + if result, err := th.SystemAdminClient.SaveConfig(utils.Cfg); err != nil { t.Fatal(err) } else { cfg := result.Data.(*model.Config) @@ -150,66 +89,31 @@ func TestSaveConfig(t *testing.T) { t.Fatal() } } + + *utils.Cfg.TeamSettings.EnableOpenServer = true } func TestEmailTest(t *testing.T) { - Setup() + th := Setup().InitBasic().InitSystemAdmin() - 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.TestEmail(utils.Cfg); err == nil { + if _, err := th.BasicClient.TestEmail(utils.Cfg); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.TestEmail(utils.Cfg); err != nil { + if _, err := th.SystemAdminClient.TestEmail(utils.Cfg); err != nil { t.Fatal(err) } } func TestGetTeamAnalyticsStandard(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") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} - post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + th := Setup().InitBasic().InitSystemAdmin() + th.CreatePrivateChannel(th.BasicClient, th.BasicTeam) - if _, err := Client.GetTeamAnalytics(team.Id, "standard"); err == nil { + if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "standard"); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if result, err := Client.GetTeamAnalytics(team.Id, "standard"); err != nil { + if result, err := th.SystemAdminClient.GetTeamAnalytics(th.BasicTeam.Id, "standard"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -219,7 +123,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) { t.Fatal() } - if rows[0].Value != 2 { + if rows[0].Value != 3 { t.Log(rows.ToJson()) t.Fatal() } @@ -249,7 +153,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) { t.Fatal() } - if rows[3].Value != 1 { + if rows[3].Value != 2 { t.Log(rows.ToJson()) t.Fatal() } @@ -265,7 +169,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) { } } - if result, err := Client.GetSystemAnalytics("standard"); err != nil { + if result, err := th.SystemAdminClient.GetSystemAnalytics("standard"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -275,7 +179,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) { t.Fatal() } - if rows[0].Value < 2 { + if rows[0].Value < 3 { t.Log(rows.ToJson()) t.Fatal() } @@ -323,39 +227,17 @@ func TestGetTeamAnalyticsStandard(t *testing.T) { } func TestGetPostCount(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") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} - post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + th := Setup().InitBasic().InitSystemAdmin() // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", - map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) + map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) - if _, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err == nil { + if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "post_counts_day"); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if result, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err != nil { + if result, err := th.SystemAdminClient.GetTeamAnalytics(th.BasicTeam.Id, "post_counts_day"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -368,39 +250,17 @@ func TestGetPostCount(t *testing.T) { } func TestUserCountsWithPostsByDay(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") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} - post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + th := Setup().InitBasic().InitSystemAdmin() // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", - map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) + map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) - if _, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err == nil { + if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "user_counts_with_posts_day"); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if result, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err != nil { + if result, err := th.SystemAdminClient.GetTeamAnalytics(th.BasicTeam.Id, "user_counts_with_posts_day"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -413,38 +273,15 @@ func TestUserCountsWithPostsByDay(t *testing.T) { } func TestGetTeamAnalyticsExtra(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") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} - post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + th := Setup().InitBasic().InitSystemAdmin() - post2 := &model.Post{ChannelId: channel1.Id, Message: "#test a" + model.NewId() + "a"} - post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + th.CreatePost(th.BasicClient, th.BasicChannel) - if _, err := Client.GetTeamAnalytics("", "extra_counts"); err == nil { + if _, err := th.BasicClient.GetTeamAnalytics("", "extra_counts"); err == nil { t.Fatal("Shouldn't have permissions") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if result, err := Client.GetTeamAnalytics(team.Id, "extra_counts"); err != nil { + if result, err := th.SystemAdminClient.GetTeamAnalytics(th.BasicTeam.Id, "extra_counts"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -464,7 +301,7 @@ func TestGetTeamAnalyticsExtra(t *testing.T) { t.Fatal() } - if rows[1].Value != 1 { + if rows[1].Value != 0 { t.Log(rows.ToJson()) t.Fatal() } @@ -510,7 +347,7 @@ func TestGetTeamAnalyticsExtra(t *testing.T) { } } - if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil { + if result, err := th.SystemAdminClient.GetSystemAnalytics("extra_counts"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -525,11 +362,6 @@ func TestGetTeamAnalyticsExtra(t *testing.T) { t.Fatal() } - if rows[1].Value < 1 { - t.Log(rows.ToJson()) - t.Fatal() - } - if rows[2].Name != "incoming_webhook_count" { t.Log(rows.ToJson()) t.Fatal() @@ -551,3 +383,73 @@ func TestGetTeamAnalyticsExtra(t *testing.T) { } } } + +func TestAdminResetMfa(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + + if _, err := th.BasicClient.AdminResetMfa("12345678901234567890123456"); err == nil { + t.Fatal("should have failed - not an admin") + } + + if _, err := th.SystemAdminClient.AdminResetMfa(""); err == nil { + t.Fatal("should have failed - empty user id") + } + + if _, err := th.SystemAdminClient.AdminResetMfa("12345678901234567890123456"); err == nil { + t.Fatal("should have failed - bad user id") + } + + if _, err := th.SystemAdminClient.AdminResetMfa(th.BasicUser.Id); err == nil { + t.Fatal("should have failed - not licensed or configured") + } + + // need to add more test cases when enterprise bits can be loaded into tests +} + +func TestAdminResetPassword(t *testing.T) { + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + if _, err := Client.AdminResetPassword("", "newpwd"); err == nil { + t.Fatal("Should have errored - empty user id") + } + + if _, err := Client.AdminResetPassword("123", "newpwd"); err == nil { + t.Fatal("Should have errored - bad user id") + } + + if _, err := Client.AdminResetPassword("12345678901234567890123456", "newpwd"); err == nil { + t.Fatal("Should have errored - bad user id") + } + + if _, err := Client.AdminResetPassword("12345678901234567890123456", "newp"); err == nil { + t.Fatal("Should have errored - password too short") + } + + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + if _, err := Client.AdminResetPassword(user2.Id, "newpwd"); err == nil { + t.Fatal("should have errored - SSO user can't reset password") + } + + if _, err := Client.AdminResetPassword(user.Id, "newpwd"); err != nil { + t.Fatal(err) + } + + Client.Logout() + Client.Must(Client.LoginById(user.Id, "newpwd")) + Client.SetTeamId(team.Id) + + if _, err := Client.AdminResetPassword(user.Id, "newpwd"); err == nil { + t.Fatal("Should have errored - not sytem admin") + } +} diff --git a/api/api.go b/api/api.go index 476047877..e9a95b125 100644 --- a/api/api.go +++ b/api/api.go @@ -6,6 +6,7 @@ package api import ( "net/http" + "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -13,20 +14,71 @@ import ( _ "github.com/nicksnyder/go-i18n/i18n" ) +type Routes struct { + Root *mux.Router // '' + ApiRoot *mux.Router // 'api/v3' + + Users *mux.Router // 'api/v3/users' + NeedUser *mux.Router // 'api/v3/users/{user_id:[A-Za-z0-9]+}' + + Teams *mux.Router // 'api/v3/teams' + NeedTeam *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}' + + Channels *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels' + NeedChannel *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}' + + Posts *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}/posts' + NeedPost *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}' + + Commands *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/commands' + Hooks *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/hooks' + + Files *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/files' + + OAuth *mux.Router // 'api/v3/oauth' + + Admin *mux.Router // 'api/v3/admin' + + Preferences *mux.Router // 'api/v3/preferences' + + License *mux.Router // 'api/v3/license' +} + +var BaseRoutes *Routes + func InitApi() { - r := Srv.Router.PathPrefix("/api/v1").Subrouter() - InitUser(r) - InitTeam(r) - InitChannel(r) - InitPost(r) - InitWebSocket(r) - InitFile(r) - InitCommand(r) - InitAdmin(r) - InitOAuth(r) - InitWebhook(r) - InitPreference(r) - InitLicense(r) + BaseRoutes = &Routes{} + BaseRoutes.Root = Srv.Router + BaseRoutes.ApiRoot = Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter() + BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter() + BaseRoutes.NeedUser = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter() + BaseRoutes.NeedTeam = BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.Channels = BaseRoutes.NeedTeam.PathPrefix("/channels").Subrouter() + BaseRoutes.NeedChannel = BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.Posts = BaseRoutes.NeedChannel.PathPrefix("/posts").Subrouter() + BaseRoutes.NeedPost = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.Commands = BaseRoutes.NeedTeam.PathPrefix("/commands").Subrouter() + BaseRoutes.Files = BaseRoutes.NeedTeam.PathPrefix("/files").Subrouter() + BaseRoutes.Hooks = BaseRoutes.NeedTeam.PathPrefix("/hooks").Subrouter() + BaseRoutes.OAuth = BaseRoutes.ApiRoot.PathPrefix("/oauth").Subrouter() + BaseRoutes.Admin = BaseRoutes.ApiRoot.PathPrefix("/admin").Subrouter() + BaseRoutes.Preferences = BaseRoutes.ApiRoot.PathPrefix("/preferences").Subrouter() + BaseRoutes.License = BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter() + + InitUser() + InitTeam() + InitChannel() + InitPost() + InitWebSocket() + InitFile() + InitCommand() + InitAdmin() + InitOAuth() + InitWebhook() + InitPreference() + InitLicense() + // 404 on any api route before web.go has a chance to serve it Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api/api_test.go b/api/api_test.go deleted file mode 100644 index 94691ab4b..000000000 --- a/api/api_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" - "github.com/mattermost/platform/utils" -) - -var Client *model.Client - -func Setup() { - if Srv == nil { - utils.LoadConfig("config.json") - utils.InitTranslations() - utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 - NewServer() - StartServer() - InitApi() - Client = model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) - - Srv.Store.MarkSystemRanUnitTests() - } -} - -func SetupBenchmark() (*model.Team, *model.User, *model.Channel) { - Setup() - - team := &model.Team{DisplayName: "Benchmark Team", Name: "z-z-" + model.NewId() + "a", Email: "benchmark@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: "Mr. Benchmarker", 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") - channel := &model.Channel{DisplayName: "Benchmark Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - - return team, user, channel -} - -func TearDown() { - if Srv != nil { - StopServer() - } -} diff --git a/api/apitestlib.go b/api/apitestlib.go new file mode 100644 index 000000000..d82dc30be --- /dev/null +++ b/api/apitestlib.go @@ -0,0 +1,223 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" +) + +type TestHelper struct { + BasicClient *model.Client + BasicTeam *model.Team + BasicUser *model.User + BasicUser2 *model.User + BasicChannel *model.Channel + BasicPost *model.Post + + SystemAdminClient *model.Client + SystemAdminTeam *model.Team + SystemAdminUser *model.User + SystemAdminChannel *model.Channel +} + +func SetupEnterprise(platformDir string) *TestHelper { + if Srv == nil { + utils.LoadConfig(platformDir + "/config/config.json") + utils.InitTranslationsWithDir(platformDir + "/i18n") + utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 + utils.DisableDebugLogForTest() + utils.License.Features.SetDefaults() + NewServer() + StartServer() + utils.InitHTMLWithDir(platformDir + "/templates") + InitApi() + utils.EnableDebugLogForTest() + Srv.Store.MarkSystemRanUnitTests() + + *utils.Cfg.TeamSettings.EnableOpenServer = true + } + + return &TestHelper{} +} + +func Setup() *TestHelper { + if Srv == nil { + utils.LoadConfig("config.json") + utils.InitTranslations() + utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 + utils.DisableDebugLogForTest() + NewServer() + StartServer() + InitApi() + utils.EnableDebugLogForTest() + Srv.Store.MarkSystemRanUnitTests() + + *utils.Cfg.TeamSettings.EnableOpenServer = true + } + + return &TestHelper{} +} + +func (me *TestHelper) InitBasic() *TestHelper { + me.BasicClient = me.CreateClient() + me.BasicTeam = me.CreateTeam(me.BasicClient) + me.BasicUser = me.CreateUser(me.BasicClient) + LinkUserToTeam(me.BasicUser, me.BasicTeam) + me.BasicUser2 = me.CreateUser(me.BasicClient) + LinkUserToTeam(me.BasicUser2, me.BasicTeam) + me.BasicClient.SetTeamId(me.BasicTeam.Id) + me.LoginBasic() + me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam) + me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel) + + return me +} + +func (me *TestHelper) InitSystemAdmin() *TestHelper { + me.SystemAdminClient = me.CreateClient() + me.SystemAdminTeam = me.CreateTeam(me.SystemAdminClient) + me.SystemAdminUser = me.CreateUser(me.SystemAdminClient) + LinkUserToTeam(me.SystemAdminUser, me.SystemAdminTeam) + me.SystemAdminClient.SetTeamId(me.SystemAdminTeam.Id) + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, me.SystemAdminUser, model.ROLE_SYSTEM_ADMIN) + me.SystemAdminUser.Password = "Password1" + me.LoginSystemAdmin() + me.SystemAdminChannel = me.CreateChannel(me.SystemAdminClient, me.SystemAdminTeam) + + return me +} + +func (me *TestHelper) CreateClient() *model.Client { + return model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) +} + +func (me *TestHelper) CreateTeam(client *model.Client) *model.Team { + id := model.NewId() + team := &model.Team{ + DisplayName: "dn_" + id, + Name: "name" + id, + Email: "success+" + id + "@simulator.amazonses.com", + Type: model.TEAM_OPEN, + } + + utils.DisableDebugLogForTest() + r := client.Must(client.CreateTeam(team)).Data.(*model.Team) + utils.EnableDebugLogForTest() + return r +} + +func (me *TestHelper) CreateUser(client *model.Client) *model.User { + id := model.NewId() + + user := &model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + id, + Nickname: "nn_" + id, + Password: "Password1", + } + + utils.DisableDebugLogForTest() + ruser := client.Must(client.CreateUser(user, "")).Data.(*model.User) + ruser.Password = "Password1" + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + utils.EnableDebugLogForTest() + return ruser +} + +func LinkUserToTeam(user *model.User, team *model.Team) { + utils.DisableDebugLogForTest() + + err := JoinUserToTeam(team, user) + if err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + + utils.EnableDebugLogForTest() +} + +func UpdateUserToTeamAdmin(user *model.User, team *model.Team) { + utils.DisableDebugLogForTest() + + tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.ROLE_TEAM_ADMIN} + if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { + l4g.Error(tmr.Err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(tmr.Err) + } +} + +func (me *TestHelper) CreateChannel(client *model.Client, team *model.Team) *model.Channel { + return me.createChannel(client, team, model.CHANNEL_OPEN) +} + +func (me *TestHelper) CreatePrivateChannel(client *model.Client, team *model.Team) *model.Channel { + return me.createChannel(client, team, model.CHANNEL_PRIVATE) +} + +func (me *TestHelper) createChannel(client *model.Client, team *model.Team, channelType string) *model.Channel { + id := model.NewId() + + channel := &model.Channel{ + DisplayName: "dn_" + id, + Name: "name_" + id, + Type: channelType, + TeamId: team.Id, + } + + utils.DisableDebugLogForTest() + r := client.Must(client.CreateChannel(channel)).Data.(*model.Channel) + utils.EnableDebugLogForTest() + return r +} + +func (me *TestHelper) CreatePost(client *model.Client, channel *model.Channel) *model.Post { + id := model.NewId() + + post := &model.Post{ + ChannelId: channel.Id, + Message: "message_" + id, + } + + utils.DisableDebugLogForTest() + r := client.Must(client.CreatePost(post)).Data.(*model.Post) + utils.EnableDebugLogForTest() + return r +} + +func (me *TestHelper) LoginBasic() { + utils.DisableDebugLogForTest() + me.BasicClient.Must(me.BasicClient.LoginByEmail(me.BasicTeam.Name, me.BasicUser.Email, me.BasicUser.Password)) + utils.EnableDebugLogForTest() +} + +func (me *TestHelper) LoginBasic2() { + utils.DisableDebugLogForTest() + me.BasicClient.Must(me.BasicClient.LoginByEmail(me.BasicTeam.Name, me.BasicUser2.Email, me.BasicUser2.Password)) + utils.EnableDebugLogForTest() +} + +func (me *TestHelper) LoginSystemAdmin() { + utils.DisableDebugLogForTest() + me.SystemAdminClient.Must(me.SystemAdminClient.LoginByEmail(me.SystemAdminTeam.Name, me.SystemAdminUser.Email, me.SystemAdminUser.Password)) + utils.EnableDebugLogForTest() +} + +func TearDown() { + if Srv != nil { + StopServer() + } +} diff --git a/api/authentication.go b/api/authentication.go new file mode 100644 index 000000000..bab83a720 --- /dev/null +++ b/api/authentication.go @@ -0,0 +1,99 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { + if err := checkUserPassword(user, password); err != nil { + return err + } + + if err := checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { + return err + } + + return nil +} + +func checkUserPassword(user *model.User, password string) *model.AppError { + if !model.ComparePassword(user.Password, password) { + if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil { + return result.Err + } + + return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id) + } else { + if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { + return result.Err + } + + return nil + } +} + +func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError { + if err := checkUserMfa(user, mfaToken); err != nil { + return err + } + + if err := checkEmailVerified(user); err != nil { + return err + } + + if err := checkUserNotDisabled(user); err != nil { + return err + } + + if err := checkUserLoginAttempts(user); err != nil { + return err + } + + return nil +} + +func checkUserMfa(user *model.User, token string) *model.AppError { + if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + return nil + } + + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") + } + + if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { + return err + } else if !ok { + return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") + } + + return nil +} + +func checkUserLoginAttempts(user *model.User) *model.AppError { + if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { + return model.NewLocAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id) + } + + return nil +} + +func checkEmailVerified(user *model.User) *model.AppError { + if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { + return model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id) + } + return nil +} + +func checkUserNotDisabled(user *model.User) *model.AppError { + if user.DeleteAt > 0 { + return model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id) + } + return nil +} diff --git a/api/auto_channels.go b/api/auto_channels.go index ab1fe6ed3..1d0f0e7d9 100644 --- a/api/auto_channels.go +++ b/api/auto_channels.go @@ -10,7 +10,7 @@ import ( type AutoChannelCreator struct { client *model.Client - teamID string + team *model.Team Fuzzy bool DisplayNameLen utils.Range DisplayNameCharset string @@ -19,10 +19,10 @@ type AutoChannelCreator struct { ChannelType string } -func NewAutoChannelCreator(client *model.Client, teamID string) *AutoChannelCreator { +func NewAutoChannelCreator(client *model.Client, team *model.Team) *AutoChannelCreator { return &AutoChannelCreator{ client: client, - teamID: teamID, + team: team, Fuzzy: false, DisplayNameLen: CHANNEL_DISPLAY_NAME_LEN, DisplayNameCharset: utils.ALPHANUMERIC, @@ -42,13 +42,17 @@ func (cfg *AutoChannelCreator) createRandomChannel() (*model.Channel, bool) { name := utils.RandomName(cfg.NameLen, cfg.NameCharset) channel := &model.Channel{ - TeamId: cfg.teamID, + TeamId: cfg.team.Id, DisplayName: displayName, Name: name, Type: cfg.ChannelType} + println(cfg.client.GetTeamRoute()) result, err := cfg.client.CreateChannel(channel) if err != nil { + err.Translate(utils.T) + println(err.Error()) + println(err.DetailedError) return nil, false } return result.Data.(*model.Channel), true diff --git a/api/auto_environment.go b/api/auto_environment.go index 68186ec6c..270b43936 100644 --- a/api/auto_environment.go +++ b/api/auto_environment.go @@ -28,14 +28,15 @@ func CreateTestEnvironmentWithTeams(client *model.Client, rangeTeams utils.Range environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))} for i, team := range teams { - userCreator := NewAutoUserCreator(client, team.Id) + userCreator := NewAutoUserCreator(client, team) userCreator.Fuzzy = fuzzy randomUser, err := userCreator.createRandomUser() if err != true { return TestEnvironment{}, false } client.LoginById(randomUser.Id, USER_PASSWORD) - teamEnvironment, err := CreateTestEnvironmentInTeam(client, team.Id, rangeChannels, rangeUsers, rangePosts, fuzzy) + client.SetTeamId(team.Id) + teamEnvironment, err := CreateTestEnvironmentInTeam(client, team, rangeChannels, rangeUsers, rangePosts, fuzzy) if err != true { return TestEnvironment{}, false } @@ -45,7 +46,7 @@ func CreateTestEnvironmentWithTeams(client *model.Client, rangeTeams utils.Range return environment, true } -func CreateTestEnvironmentInTeam(client *model.Client, teamID string, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, bool) { +func CreateTestEnvironmentInTeam(client *model.Client, team *model.Team, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, bool) { rand.Seed(time.Now().UTC().UnixNano()) // We need to create at least one user @@ -53,7 +54,7 @@ func CreateTestEnvironmentInTeam(client *model.Client, teamID string, rangeChann rangeUsers.Begin = 1 } - userCreator := NewAutoUserCreator(client, teamID) + userCreator := NewAutoUserCreator(client, team) userCreator.Fuzzy = fuzzy users, err := userCreator.CreateTestUsers(rangeUsers) if err != true { @@ -64,7 +65,7 @@ func CreateTestEnvironmentInTeam(client *model.Client, teamID string, rangeChann usernames[i] = user.Username } - channelCreator := NewAutoChannelCreator(client, teamID) + channelCreator := NewAutoChannelCreator(client, team) channelCreator.Fuzzy = fuzzy channels, err := channelCreator.CreateTestChannels(rangeChannels) @@ -79,6 +80,7 @@ func CreateTestEnvironmentInTeam(client *model.Client, teamID string, rangeChann if err != true { return TeamEnvironment{}, false } + numPosts := utils.RandIntFromRange(rangePosts) numImages := utils.RandIntFromRange(rangePosts) / 4 for j := 0; j < numPosts; j++ { diff --git a/api/auto_posts.go b/api/auto_posts.go index b64217c55..2e26e513b 100644 --- a/api/auto_posts.go +++ b/api/auto_posts.go @@ -74,7 +74,7 @@ func (cfg *AutoPostCreator) UploadTestFile() ([]string, bool) { return nil, false } - resp, appErr := cfg.client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + resp, appErr := cfg.client.UploadPostAttachment(body.Bytes(), writer.FormDataContentType()) if appErr != nil { return nil, false } diff --git a/api/auto_teams.go b/api/auto_teams.go index 082415d32..b2e1ace85 100644 --- a/api/auto_teams.go +++ b/api/auto_teams.go @@ -42,11 +42,11 @@ func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, bool) { var teamDisplayName string var teamName string if cfg.Fuzzy { - teamEmail = utils.FuzzEmail() + teamEmail = "success+" + model.NewId() + "simulator.amazonses.com" teamDisplayName = utils.FuzzName() teamName = utils.FuzzName() } else { - teamEmail = utils.RandomEmail(cfg.EmailLength, cfg.EmailCharset) + teamEmail = "success+" + model.NewId() + "simulator.amazonses.com" teamDisplayName = utils.RandomName(cfg.NameLength, cfg.NameCharset) teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId() } diff --git a/api/auto_users.go b/api/auto_users.go index d1e3d494e..a23b76246 100644 --- a/api/auto_users.go +++ b/api/auto_users.go @@ -7,11 +7,13 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" ) type AutoUserCreator struct { client *model.Client - teamID string + team *model.Team EmailLength utils.Range EmailCharset string NameLength utils.Range @@ -19,10 +21,10 @@ type AutoUserCreator struct { Fuzzy bool } -func NewAutoUserCreator(client *model.Client, teamID string) *AutoUserCreator { +func NewAutoUserCreator(client *model.Client, team *model.Team) *AutoUserCreator { return &AutoUserCreator{ client: client, - teamID: teamID, + team: team, EmailLength: USER_EMAIL_LEN, EmailCharset: utils.LOWERCASE, NameLength: USER_NAME_LEN, @@ -33,7 +35,7 @@ func NewAutoUserCreator(client *model.Client, teamID string) *AutoUserCreator { // Basic test team and user so you always know one func CreateBasicUser(client *model.Client) *model.AppError { - result, _ := client.FindTeamByName(BTEST_TEAM_NAME, true) + result, _ := client.FindTeamByName(BTEST_TEAM_NAME) if result.Data.(bool) == false { newteam := &model.Team{DisplayName: BTEST_TEAM_DISPLAY_NAME, Name: BTEST_TEAM_NAME, Email: BTEST_TEAM_EMAIL, Type: BTEST_TEAM_TYPE} result, err := client.CreateTeam(newteam) @@ -41,12 +43,14 @@ func CreateBasicUser(client *model.Client) *model.AppError { return err } basicteam := result.Data.(*model.Team) - newuser := &model.User{TeamId: basicteam.Id, Email: BTEST_USER_EMAIL, Nickname: BTEST_USER_NAME, Password: BTEST_USER_PASSWORD} + newuser := &model.User{Email: BTEST_USER_EMAIL, Nickname: BTEST_USER_NAME, Password: BTEST_USER_PASSWORD} result, err = client.CreateUser(newuser, "") if err != nil { return err } - store.Must(Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id)) + ruser := result.Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id})) } return nil } @@ -55,25 +59,30 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { var userEmail string var userName string if cfg.Fuzzy { - userEmail = utils.RandString(FUZZ_USER_EMAIL_PREFIX_LEN, utils.LOWERCASE) + "-" + utils.FuzzEmail() + userEmail = "success+" + model.NewId() + "simulator.amazonses.com" userName = utils.FuzzName() } else { - userEmail = utils.RandomEmail(cfg.EmailLength, cfg.EmailCharset) + userEmail = "success+" + model.NewId() + "simulator.amazonses.com" userName = utils.RandomName(cfg.NameLength, cfg.NameCharset) } user := &model.User{ - TeamId: cfg.teamID, Email: userEmail, Nickname: userName, Password: USER_PASSWORD} - result, err := cfg.client.CreateUser(user, "") + result, err := cfg.client.CreateUserWithInvite(user, "", "", cfg.team.InviteId) if err != nil { + err.Translate(utils.T) + l4g.Error(err.Error()) return nil, false } + + ruser := result.Data.(*model.User) + // We need to cheat to verify the user's email - store.Must(Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id)) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + return result.Data.(*model.User), true } diff --git a/api/channel.go b/api/channel.go index e97e08fc0..871477824 100644 --- a/api/channel.go +++ b/api/channel.go @@ -18,29 +18,28 @@ const ( defaultExtraMemberLimit = 100 ) -func InitChannel(r *mux.Router) { +func InitChannel() { l4g.Debug(utils.T("api.channel.init.debug")) - sr := r.PathPrefix("/channels").Subrouter() - sr.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") - sr.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") - sr.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET") - sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") - sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") - sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") - sr.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST") - sr.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST") - sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(join)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leave)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/delete", ApiUserRequired(deleteChannel)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/add", ApiUserRequired(addMember)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/remove", ApiUserRequired(removeMember)).Methods("POST") - sr.Handle("/{id:[A-Za-z0-9]+}/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST") - + BaseRoutes.Channels.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") + BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") + BaseRoutes.Channels.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET") + BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") + BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") + BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") + BaseRoutes.Channels.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST") + BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST") + BaseRoutes.Channels.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST") + + BaseRoutes.NeedChannel.Handle("/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/join", ApiUserRequired(join)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/leave", ApiUserRequired(leave)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/add", ApiUserRequired(addMember)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/remove", ApiUserRequired(removeMember)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST") } func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { @@ -52,6 +51,10 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } + if len(channel.TeamId) == 0 { + channel.TeamId = c.TeamId + } + if !c.HasPermissionsToTeam(channel.TeamId, "createChannel") { return } @@ -107,10 +110,6 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.HasPermissionsToTeam(c.Session.TeamId, "createDirectChannel") { - return - } - if sc, err := CreateDirectChannel(c, userId); err != nil { c.Err = err return @@ -131,7 +130,7 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model channel.DisplayName = "" channel.Name = model.GetDMNameFromIds(otherUserId, c.Session.UserId) - channel.TeamId = c.Session.TeamId + channel.TeamId = c.TeamId channel.Header = "" channel.Type = model.CHANNEL_DIRECT @@ -214,7 +213,7 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { if oldChannel.Name == model.DEFAULT_CHANNEL { if (len(channel.Name) > 0 && channel.Name != oldChannel.Name) || (len(channel.Type) > 0 && channel.Type != oldChannel.Type) { c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.tried.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "") - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest return } } @@ -367,7 +366,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { // user is already in the team - if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Channel().GetChannels(c.TeamId, c.Session.UserId); result.Err != nil { if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" { // lets make sure the user is valid if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { @@ -392,7 +391,7 @@ func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { // user is already in the team - if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.ChannelList).Etag(), w, r) { @@ -408,7 +407,7 @@ func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) { // user is already in the team - if result := <-Srv.Store.Channel().GetChannelCounts(c.Session.TeamId, c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Channel().GetChannelCounts(c.TeamId, c.Session.UserId); result.Err != nil { c.Err = model.NewLocAppError("getChannelCounts", "api.channel.get_channel_counts.app_error", nil, result.Err.Message) return } else if HandleEtag(result.Data.(*model.ChannelCounts).Etag(), w, r) { @@ -423,7 +422,7 @@ func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) { func join(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - channelId := params["id"] + channelId := params["channel_id"] JoinChannel(c, channelId, "") @@ -498,7 +497,7 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM } go func() { - UpdateChannelAccessCache(channel.TeamId, user.Id, channel.Id) + InvalidateCacheForUser(user.Id) message := model.NewMessage(channel.TeamId, channel.Id, user.Id, model.ACTION_USER_ADDED) PublishAndForget(message) @@ -507,12 +506,12 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM return newMember, nil } -func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError { +func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *model.AppError { // We don't call JoinChannel here since c.Session is not populated on user creation var err *model.AppError = nil - if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil { + if result := <-Srv.Store.Channel().GetByName(teamId, "town-square"); result.Err != nil { err = result.Err } else { cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, @@ -523,7 +522,7 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError { } } - if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil { + if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil { err = result.Err } else { cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, @@ -540,7 +539,7 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError { func leave(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] sc := Srv.Store.Channel().Get(id) uc := Srv.Store.User().Get(c.Session.UserId) @@ -561,13 +560,13 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { if channel.Type == model.CHANNEL_DIRECT { c.Err = model.NewLocAppError("leave", "api.channel.leave.direct.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest return } if channel.Name == model.DEFAULT_CHANNEL { c.Err = model.NewLocAppError("leave", "api.channel.leave.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "") - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest return } @@ -589,7 +588,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] sc := Srv.Store.Channel().Get(id) scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) @@ -637,7 +636,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { if channel.Name == model.DEFAULT_CHANNEL { c.Err = model.NewLocAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "") - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest return } @@ -682,7 +681,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] Srv.Store.Channel().UpdateLastViewedAt(id, c.Session.UserId) @@ -695,7 +694,7 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Srv.Store.Preference().Save(&model.Preferences{preference}) - message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_CHANNEL_VIEWED) + message := model.NewMessage(c.TeamId, id, c.Session.UserId, model.ACTION_CHANNEL_VIEWED) message.Add("channel_id", id) PublishAndForget(message) @@ -707,9 +706,9 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] - //pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + //pchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, id, c.Session.UserId) cchan := Srv.Store.Channel().Get(id) cmchan := Srv.Store.Channel().GetMember(id, c.Session.UserId) @@ -737,7 +736,7 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] var memberLimit int if memberLimitString, ok := params["member_limit"]; !ok { @@ -781,7 +780,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { extraMembers := ecmresult.Data.([]model.ExtraMember) memberCount := ccmresult.Data.(int64) - if !c.HasPermissionsToTeam(channel.TeamId, "getChannelExtraInfo") { + if len(channel.TeamId) > 0 && !c.HasPermissionsToTeam(channel.TeamId, "getChannelExtraInfo") { return } @@ -803,7 +802,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { func addMember(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] data := model.MapFromJson(r.Body) userId := data["user_id"] @@ -813,7 +812,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, id, c.Session.UserId) sc := Srv.Store.Channel().Get(id) ouc := Srv.Store.User().Get(c.Session.UserId) nuc := Srv.Store.User().Get(userId) @@ -857,7 +856,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { func removeMember(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - channelId := params["id"] + channelId := params["channel_id"] data := model.MapFromJson(r.Body) userIdToRemove := data["user_id"] @@ -914,7 +913,7 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel return cmresult.Err } - UpdateChannelAccessCacheAndForget(channel.TeamId, userIdToRemove, channel.Id) + InvalidateCacheForUser(userIdToRemove) message := model.NewMessage(channel.TeamId, channel.Id, userIdToRemove, model.ACTION_USER_REMOVED) message.Add("remover_id", removerUserId) @@ -938,7 +937,7 @@ func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) if !c.HasPermissionsToUser(userId, "updateNotifyLevel") { return diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go index 09c734cc2..3e7c2882c 100644 --- a/api/channel_benchmark_test.go +++ b/api/channel_benchmark_test.go @@ -12,61 +12,47 @@ import ( const ( NUM_CHANNELS = 140 + NUM_USERS = 40 ) func BenchmarkCreateChannel(b *testing.B) { - var ( - NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} - ) - team, _, _ := SetupBenchmark() + th := Setup().InitBasic() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) - // Benchmark Start b.ResetTimer() for i := 0; i < b.N; i++ { - channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + channelCreator.CreateTestChannels(utils.Range{NUM_CHANNELS, NUM_CHANNELS}) } } func BenchmarkCreateDirectChannel(b *testing.B) { - var ( - NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} - ) - team, _, _ := SetupBenchmark() + th := Setup().InitBasic() - userCreator := NewAutoUserCreator(Client, team.Id) - users, err := userCreator.CreateTestUsers(NUM_CHANNELS_RANGE) + userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam) + users, err := userCreator.CreateTestUsers(utils.Range{NUM_USERS, NUM_USERS}) if err == false { b.Fatal("Could not create users") } - data := make([]map[string]string, len(users)) - - for i := range data { - newmap := map[string]string{ - "user_id": users[i].Id, - } - data[i] = newmap - } - // Benchmark Start b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < NUM_CHANNELS; j++ { - Client.CreateDirectChannel(data[j]) + for j := 0; j < NUM_USERS; j++ { + th.BasicClient.CreateDirectChannel(users[j].Id) } } } func BenchmarkUpdateChannel(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} CHANNEL_HEADER_LEN = 50 ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -80,7 +66,7 @@ func BenchmarkUpdateChannel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range channels { - if _, err := Client.UpdateChannel(channels[j]); err != nil { + if _, err := th.BasicClient.UpdateChannel(channels[j]); err != nil { b.Fatal(err) } } @@ -88,12 +74,13 @@ func BenchmarkUpdateChannel(b *testing.B) { } func BenchmarkGetChannels(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) _, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -102,17 +89,18 @@ func BenchmarkGetChannels(b *testing.B) { // Benchmark Start b.ResetTimer() for i := 0; i < b.N; i++ { - Client.Must(Client.GetChannels("")) + th.BasicClient.Must(th.BasicClient.GetChannels("")) } } func BenchmarkGetMoreChannels(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) _, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -121,44 +109,47 @@ func BenchmarkGetMoreChannels(b *testing.B) { // Benchmark Start b.ResetTimer() for i := 0; i < b.N; i++ { - Client.Must(Client.GetMoreChannels("")) + th.BasicClient.Must(th.BasicClient.GetMoreChannels("")) } } func BenchmarkJoinChannel(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") } // Secondary test user to join channels created by primary test user - user := &model.User{TeamId: team.Id, Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "That Guy", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "That Guy", Password: "pwd"} + user = th.BasicClient.Must(th.BasicClient.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, th.BasicTeam) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + th.BasicClient.LoginByEmail(th.BasicTeam.Name, user.Email, "pwd") // Benchmark Start b.ResetTimer() for i := 0; i < b.N; i++ { for j := range channels { - Client.Must(Client.JoinChannel(channels[j].Id)) + th.BasicClient.Must(th.BasicClient.JoinChannel(channels[j].Id)) } } } func BenchmarkDeleteChannel(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -168,18 +159,19 @@ func BenchmarkDeleteChannel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range channels { - Client.Must(Client.DeleteChannel(channels[j].Id)) + th.BasicClient.Must(th.BasicClient.DeleteChannel(channels[j].Id)) } } } func BenchmarkGetChannelExtraInfo(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, _, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -189,22 +181,23 @@ func BenchmarkGetChannelExtraInfo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range channels { - Client.Must(Client.GetChannelExtraInfo(channels[j].Id, -1, "")) + th.BasicClient.Must(th.BasicClient.GetChannelExtraInfo(channels[j].Id, -1, "")) } } } func BenchmarkAddChannelMember(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_USERS = 100 NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS} ) - team, _, _ := SetupBenchmark() - channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id} + channel = th.BasicClient.Must(th.BasicClient.CreateChannel(channel)).Data.(*model.Channel) - userCreator := NewAutoUserCreator(Client, team.Id) + userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam) users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE) if valid == false { b.Fatal("Unable to create test users") @@ -214,7 +207,7 @@ func BenchmarkAddChannelMember(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range users { - if _, err := Client.AddChannelMember(channel.Id, users[j].Id); err != nil { + if _, err := th.BasicClient.AddChannelMember(channel.Id, users[j].Id); err != nil { b.Fatal(err) } } @@ -223,23 +216,24 @@ func BenchmarkAddChannelMember(b *testing.B) { // Is this benchmark failing? Raise your file ulimit! 2048 worked for me. func BenchmarkRemoveChannelMember(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_USERS = 140 NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS} ) - team, _, _ := SetupBenchmark() - channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id} + channel = th.BasicClient.Must(th.BasicClient.CreateChannel(channel)).Data.(*model.Channel) - userCreator := NewAutoUserCreator(Client, team.Id) + userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam) users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE) if valid == false { b.Fatal("Unable to create test users") } for i := range users { - if _, err := Client.AddChannelMember(channel.Id, users[i].Id); err != nil { + if _, err := th.BasicClient.AddChannelMember(channel.Id, users[i].Id); err != nil { b.Fatal(err) } } @@ -248,7 +242,7 @@ func BenchmarkRemoveChannelMember(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range users { - if _, err := Client.RemoveChannelMember(channel.Id, users[j].Id); err != nil { + if _, err := th.BasicClient.RemoveChannelMember(channel.Id, users[j].Id); err != nil { b.Fatal(err) } } @@ -256,12 +250,13 @@ func BenchmarkRemoveChannelMember(b *testing.B) { } func BenchmarkUpdateNotifyProps(b *testing.B) { + th := Setup().InitBasic() + var ( NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} ) - team, user, _ := SetupBenchmark() - channelCreator := NewAutoChannelCreator(Client, team.Id) + channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam) channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) if valid == false { b.Fatal("Unable to create test channels") @@ -272,7 +267,7 @@ func BenchmarkUpdateNotifyProps(b *testing.B) { for i := range data { newmap := map[string]string{ "channel_id": channels[i].Id, - "user_id": user.Id, + "user_id": th.BasicUser.Id, "desktop": model.CHANNEL_NOTIFY_MENTION, "mark_unread": model.CHANNEL_MARK_UNREAD_MENTION, } @@ -283,7 +278,7 @@ func BenchmarkUpdateNotifyProps(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := range channels { - Client.Must(Client.UpdateNotifyProps(data[j])) + th.BasicClient.Must(th.BasicClient.UpdateNotifyProps(data[j])) } } } diff --git a/api/channel_test.go b/api/channel_test.go index c3015f924..23dd77698 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -14,19 +14,13 @@ import ( ) func TestCreateChannel(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) - - team2 := &model.Team{DisplayName: "Name Team 2", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team2 = Client.Must(Client.CreateTeam(team2)).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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + Client.Must(Client.Logout()) + team2 := th.CreateTeam(th.BasicClient) + th.LoginBasic() + th.BasicClient.SetTeamId(team.Id) channel := model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} rchannel, err := Client.CreateChannel(&channel) @@ -63,7 +57,7 @@ func TestCreateChannel(t *testing.T) { } } - if _, err := Client.DoApiPost("/channels/create", "garbage"); err == nil { + if _, err := Client.DoApiPost(Client.GetTeamRoute()+"/channels/create", "garbage"); err == nil { t.Fatal("should have been an error") } @@ -94,25 +88,12 @@ func TestCreateChannel(t *testing.T) { } func TestCreateDirectChannel(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)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - data := make(map[string]string) - data["user_id"] = user2.Id + th := Setup().InitBasic() + Client := th.BasicClient + user := th.BasicUser + user2 := th.BasicUser2 - rchannel, err := Client.CreateDirectChannel(data) + rchannel, err := Client.CreateDirectChannel(th.BasicUser2.Id) if err != nil { t.Fatal(err) } @@ -132,47 +113,31 @@ func TestCreateDirectChannel(t *testing.T) { t.Fatal("channel type was not direct") } - if _, err := Client.CreateDirectChannel(data); err == nil { + if _, err := Client.CreateDirectChannel(th.BasicUser2.Id); err == nil { t.Fatal("channel already exists and should have failed") } - data["user_id"] = "junk" - if _, err := Client.CreateDirectChannel(data); err == nil { + if _, err := Client.CreateDirectChannel("junk"); err == nil { t.Fatal("should have failed with bad user id") } - data["user_id"] = "12345678901234567890123456" - if _, err := Client.CreateDirectChannel(data); err == nil { + if _, err := Client.CreateDirectChannel("12345678901234567890123456"); err == nil { t.Fatal("should have failed with non-existent user") } } func TestUpdateChannel(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) - - userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, Nickname: "Corey Hulen", Password: "pwd"} - userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userTeamAdmin.Id)) - - userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userChannelAdmin.Id)) - - userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userStd.Id)) - userStd.Roles = "" - - Client.LoginByEmail(team.Name, userChannelAdmin.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + user2 := th.CreateUser(th.BasicClient) channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - Client.AddChannelMember(channel1.Id, userTeamAdmin.Id) + Client.AddChannelMember(channel1.Id, user.Id) header := "a" + model.NewId() + "a" purpose := "a" + model.NewId() + "a" @@ -191,25 +156,6 @@ func TestUpdateChannel(t *testing.T) { t.Fatal("Channel admin failed to skip displayName") } - Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") - - header = "b" + model.NewId() + "b" - purpose = "b" + model.NewId() + "b" - upChannel1 = &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose} - upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) - - if upChannel1.Header != header { - t.Fatal("Team admin failed to update header") - } - - if upChannel1.Purpose != purpose { - t.Fatal("Team admin failed to update purpose") - } - - if upChannel1.DisplayName != channel1.DisplayName { - t.Fatal("Team admin failed to skip displayName") - } - rget := Client.Must(Client.GetChannels("")) data := rget.Data.(*model.ChannelList) for _, c := range data.Channels { @@ -223,7 +169,7 @@ func TestUpdateChannel(t *testing.T) { } } - Client.LoginByEmail(team.Name, userStd.Email, "pwd") + Client.LoginByEmail(team.Name, user2.Email, user2.Password) if _, err := Client.UpdateChannel(upChannel1); err == nil { t.Fatal("Standard User should have failed to update") @@ -231,16 +177,9 @@ func TestUpdateChannel(t *testing.T) { } func TestUpdateChannelHeader(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -276,11 +215,7 @@ func TestUpdateChannelHeader(t *testing.T) { t.Fatal("should have errored on bad channel header") } - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() data["channel_id"] = channel1.Id data["channel_header"] = "new header" @@ -290,16 +225,9 @@ func TestUpdateChannelHeader(t *testing.T) { } func TestUpdateChannelPurpose(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -335,11 +263,7 @@ func TestUpdateChannelPurpose(t *testing.T) { t.Fatal("should have errored on bad channel purpose") } - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() data["channel_id"] = channel1.Id data["channel_purpose"] = "new purpose" @@ -349,16 +273,9 @@ func TestUpdateChannelPurpose(t *testing.T) { } func TestGetChannel(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -412,16 +329,9 @@ func TestGetChannel(t *testing.T) { } func TestGetMoreChannel(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -429,11 +339,7 @@ func TestGetMoreChannel(t *testing.T) { channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() rget := Client.Must(Client.GetMoreChannels("")) data := rget.Data.(*model.ChannelList) @@ -456,16 +362,9 @@ func TestGetMoreChannel(t *testing.T) { } func TestGetChannelCounts(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -478,11 +377,11 @@ func TestGetChannelCounts(t *testing.T) { } else { counts := result.Data.(*model.ChannelCounts) - if len(counts.Counts) != 4 { + if len(counts.Counts) != 5 { t.Fatal("wrong number of channel counts") } - if len(counts.UpdateTimes) != 4 { + if len(counts.UpdateTimes) != 5 { t.Fatal("wrong number of channel update times") } @@ -497,16 +396,9 @@ func TestGetChannelCounts(t *testing.T) { } func TestJoinChannel(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -514,11 +406,7 @@ func TestJoinChannel(t *testing.T) { channel3 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() Client.Must(Client.JoinChannel(channel1.Id)) @@ -526,13 +414,10 @@ func TestJoinChannel(t *testing.T) { t.Fatal("shouldn't be able to join secret group") } - data := make(map[string]string) - data["user_id"] = user1.Id - rchannel := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) - - user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + rchannel := Client.Must(Client.CreateDirectChannel(th.BasicUser.Id)).Data.(*model.Channel) + user3 := th.CreateUser(th.BasicClient) + LinkUserToTeam(user3, team) Client.LoginByEmail(team.Name, user3.Email, "pwd") if _, err := Client.JoinChannel(rchannel.Id); err == nil { @@ -541,16 +426,9 @@ func TestJoinChannel(t *testing.T) { } func TestLeaveChannel(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -558,20 +436,14 @@ func TestLeaveChannel(t *testing.T) { channel3 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() Client.Must(Client.JoinChannel(channel1.Id)) // No error if you leave a channel you cannot see Client.Must(Client.LeaveChannel(channel3.Id)) - data := make(map[string]string) - data["user_id"] = user1.Id - rchannel := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) + rchannel := Client.Must(Client.CreateDirectChannel(th.BasicUser.Id)).Data.(*model.Channel) if _, err := Client.LeaveChannel(rchannel.Id); err == nil { t.Fatal("should have errored, cannot leave direct channel") @@ -590,20 +462,12 @@ func TestLeaveChannel(t *testing.T) { } func TestDeleteChannel(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) - - userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, Nickname: "Corey Hulen", Password: "pwd"} - userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userTeamAdmin.Id)) - - userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userChannelAdmin.Id)) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + userTeamAdmin := th.BasicUser - Client.LoginByEmail(team.Name, userChannelAdmin.Email, "pwd") + th.LoginBasic2() channelMadeByCA := &model.Channel{DisplayName: "C Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channelMadeByCA = Client.Must(Client.CreateChannel(channelMadeByCA)).Data.(*model.Channel) @@ -631,11 +495,9 @@ func TestDeleteChannel(t *testing.T) { t.Fatal("should have failed to post to deleted channel") } - userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userStd.Id)) - - Client.LoginByEmail(team.Name, userStd.Email, "pwd") + userStd := th.CreateUser(th.BasicClient) + LinkUserToTeam(userStd, team) + Client.LoginByEmail(team.Name, userStd.Email, userStd.Password) if _, err := Client.JoinChannel(channel1.Id); err == nil { t.Fatal("should have failed to join deleted channel") @@ -660,16 +522,9 @@ func TestDeleteChannel(t *testing.T) { } func TestGetChannelExtraInfo(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -701,8 +556,10 @@ func TestGetChannelExtraInfo(t *testing.T) { Client2 := model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) - user2 := &model.User{TeamId: team.Id, Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Tester 2", Password: "pwd"} + user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Tester 2", Password: "pwd"} user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) + Client2.SetTeamId(team.Id) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) Client2.LoginByEmail(team.Name, user2.Email, "pwd") @@ -784,24 +641,14 @@ func TestGetChannelExtraInfo(t *testing.T) { } func TestAddChannelMember(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user2 := th.BasicUser2 channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - if _, err := Client.AddChannelMember(channel1.Id, user2.Id); err != nil { t.Fatal(err) } @@ -825,13 +672,13 @@ func TestAddChannelMember(t *testing.T) { channel2 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() if _, err := Client.AddChannelMember(channel2.Id, user2.Id); err == nil { t.Fatal("Should have errored, user not in channel") } - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th.LoginBasic() Client.Must(Client.DeleteChannel(channel2.Id)) @@ -842,34 +689,24 @@ func TestAddChannelMember(t *testing.T) { } func TestRemoveChannelMember(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) - - userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, Nickname: "Corey Hulen", Password: "pwd"} - userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userTeamAdmin.Id)) - - userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userChannelAdmin.Id)) - - Client.LoginByEmail(team.Name, userChannelAdmin.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user2 := th.BasicUser2 + UpdateUserToTeamAdmin(user2, team) channelMadeByCA := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channelMadeByCA = Client.Must(Client.CreateChannel(channelMadeByCA)).Data.(*model.Channel) - Client.Must(Client.AddChannelMember(channelMadeByCA.Id, userTeamAdmin.Id)) + Client.Must(Client.AddChannelMember(channelMadeByCA.Id, user2.Id)) - Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") + th.LoginBasic2() channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userStd.Id)) + userStd := th.CreateUser(th.BasicClient) + LinkUserToTeam(userStd, team) Client.Must(Client.AddChannelMember(channel1.Id, userStd.Id)) @@ -894,13 +731,13 @@ func TestRemoveChannelMember(t *testing.T) { channel2 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - Client.LoginByEmail(team.Name, userStd.Email, "pwd") + Client.LoginByEmail(team.Name, userStd.Email, userStd.Password) if _, err := Client.RemoveChannelMember(channel2.Id, userStd.Id); err == nil { t.Fatal("Should have errored, user not channel admin") } - Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") + th.LoginBasic2() Client.Must(Client.AddChannelMember(channel2.Id, userStd.Id)) Client.Must(Client.DeleteChannel(channel2.Id)) @@ -912,16 +749,11 @@ func TestRemoveChannelMember(t *testing.T) { } func TestUpdateNotifyProps(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + user2 := th.BasicUser2 channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -1019,10 +851,7 @@ func TestUpdateNotifyProps(t *testing.T) { t.Fatal("Should have errored - bad mark unread level") } - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() data["channel_id"] = channel1.Id data["user_id"] = user2.Id @@ -1034,16 +863,9 @@ func TestUpdateNotifyProps(t *testing.T) { } func TestFuzzyChannel(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") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam // Strings that should pass as acceptable channel names var fuzzyStringsPass = []string{ diff --git a/api/command.go b/api/command.go index 99fd05d7a..72249a48c 100644 --- a/api/command.go +++ b/api/command.go @@ -12,7 +12,7 @@ import ( "strings" l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -38,23 +38,21 @@ func GetCommandProvider(name string) CommandProvider { return nil } -func InitCommand(r *mux.Router) { +func InitCommand() { l4g.Debug(utils.T("api.command.init.debug")) - sr := r.PathPrefix("/commands").Subrouter() - - sr.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST") - sr.Handle("/list", ApiUserRequired(listCommands)).Methods("GET") + BaseRoutes.Commands.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST") + BaseRoutes.Commands.Handle("/list", ApiUserRequired(listCommands)).Methods("GET") - sr.Handle("/create", ApiUserRequired(createCommand)).Methods("POST") - sr.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET") - sr.Handle("/regen_token", ApiUserRequired(regenCommandToken)).Methods("POST") - sr.Handle("/delete", ApiUserRequired(deleteCommand)).Methods("POST") + BaseRoutes.Commands.Handle("/create", ApiUserRequired(createCommand)).Methods("POST") + BaseRoutes.Commands.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET") + BaseRoutes.Commands.Handle("/regen_token", ApiUserRequired(regenCommandToken)).Methods("POST") + BaseRoutes.Commands.Handle("/delete", ApiUserRequired(deleteCommand)).Methods("POST") - sr.Handle("/test", ApiAppHandler(testCommand)).Methods("POST") - sr.Handle("/test", ApiAppHandler(testCommand)).Methods("GET") - sr.Handle("/test_e", ApiAppHandler(testEphemeralCommand)).Methods("POST") - sr.Handle("/test_e", ApiAppHandler(testEphemeralCommand)).Methods("GET") + BaseRoutes.Teams.Handle("/command_test", ApiAppHandler(testCommand)).Methods("POST") + BaseRoutes.Teams.Handle("/command_test", ApiAppHandler(testCommand)).Methods("GET") + BaseRoutes.Teams.Handle("/command_test_e", ApiAppHandler(testEphemeralCommand)).Methods("POST") + BaseRoutes.Teams.Handle("/command_test_e", ApiAppHandler(testEphemeralCommand)).Methods("GET") } func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { @@ -70,7 +68,7 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { } if *utils.Cfg.ServiceSettings.EnableCommands { - if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -99,7 +97,7 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { } if len(channelId) > 0 { - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) if !c.HasPermissionsToChannel(cchan, "checkCommand") { return @@ -124,10 +122,10 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { } chanChan := Srv.Store.Channel().Get(channelId) - teamChan := Srv.Store.Team().Get(c.Session.TeamId) + teamChan := Srv.Store.Team().Get(c.TeamId) userChan := Srv.Store.User().Get(c.Session.UserId) - if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -254,7 +252,7 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe post.Message = response.Text post.CreateAt = model.GetMillis() SendEphemeralPost( - c.Session.TeamId, + c.TeamId, c.Session.UserId, post, ) @@ -288,7 +286,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { } cmd.CreatorId = c.Session.UserId - cmd.TeamId = c.Session.TeamId + cmd.TeamId = c.TeamId if result := <-Srv.Store.Command().Save(cmd); result.Err != nil { c.Err = result.Err @@ -315,7 +313,7 @@ func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -356,7 +354,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { } else { cmd = result.Data.(*model.Command) - if c.Session.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !c.IsTeamAdmin()) { + if c.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !c.IsTeamAdmin()) { c.LogAudit("fail - inappropriate permissions") c.Err = model.NewLocAppError("regenToken", "api.command.regen.app_error", nil, "user_id="+c.Session.UserId) return @@ -402,7 +400,7 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = result.Err return } else { - if c.Session.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !c.IsTeamAdmin()) { + if c.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !c.IsTeamAdmin()) { c.LogAudit("fail - inappropriate permissions") c.Err = model.NewLocAppError("deleteCommand", "api.command.delete.app_error", nil, "user_id="+c.Session.UserId) return diff --git a/api/command_echo_test.go b/api/command_echo_test.go index 3bfaa0279..26fba007c 100644 --- a/api/command_echo_test.go +++ b/api/command_echo_test.go @@ -8,23 +8,12 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestEchoCommand(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel echoTestString := "/echo test" @@ -36,7 +25,7 @@ func TestEchoCommand(t *testing.T) { time.Sleep(100 * time.Millisecond) p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) - if len(p1.Order) != 1 { + if len(p1.Order) != 2 { t.Fatal("Echo command failed to send") } } diff --git a/api/command_join.go b/api/command_join.go index ba3b0041e..f59925c06 100644 --- a/api/command_join.go +++ b/api/command_join.go @@ -33,7 +33,7 @@ func (me *JoinProvider) GetCommand(c *Context) *model.Command { } func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { - if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId); result.Err != nil { return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { channels := result.Data.(*model.ChannelList) diff --git a/api/command_join_test.go b/api/command_join_test.go index 7260915a6..2b4a5bfe3 100644 --- a/api/command_join_test.go +++ b/api/command_join_test.go @@ -8,20 +8,13 @@ import ( "testing" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestJoinCommands(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user2 := th.BasicUser2 channel0 := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel0 = Client.Must(Client.CreateChannel(channel0)).Data.(*model.Channel) @@ -34,13 +27,7 @@ func TestJoinCommands(t *testing.T) { channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) Client.Must(Client.LeaveChannel(channel2.Id)) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - data := make(map[string]string) - data["user_id"] = user2.Id - channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) + channel3 := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel) rs5 := Client.Must(Client.Command(channel0.Id, "/join "+channel2.Name, false)).Data.(*model.CommandResponse) if !strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name) { @@ -54,7 +41,7 @@ func TestJoinCommands(t *testing.T) { c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) - if len(c1.Channels) != 5 { // 4 because of town-square, off-topic and direct + if len(c1.Channels) != 6 { // 4 because of town-square, off-topic and direct t.Fatal("didn't join channel") } diff --git a/api/command_loadtest.go b/api/command_loadtest.go index 63598c06e..2738f4b51 100644 --- a/api/command_loadtest.go +++ b/api/command_loadtest.go @@ -182,10 +182,19 @@ func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message s } } } else { + + var team *model.Team + if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + client.MockSession(c.Session.Token) + client.SetTeamId(c.TeamId) CreateTestEnvironmentInTeam( client, - c.Session.TeamId, + team, utils.Range{numChannels, numChannels}, utils.Range{numUsers, numUsers}, utils.Range{numPosts, numPosts}, @@ -209,8 +218,16 @@ func (me *LoadTestProvider) UsersCommand(c *Context, channelId string, message s usersr = utils.Range{2, 5} } + var team *model.Team + if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + client := model.NewClient(c.GetSiteURL()) - userCreator := NewAutoUserCreator(client, c.Session.TeamId) + client.SetTeamId(team.Id) + userCreator := NewAutoUserCreator(client, team) userCreator.Fuzzy = doFuzz userCreator.CreateTestUsers(usersr) @@ -230,9 +247,18 @@ func (me *LoadTestProvider) ChannelsCommand(c *Context, channelId string, messag if err == false { channelsr = utils.Range{2, 5} } + + var team *model.Team + if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + client := model.NewClient(c.GetSiteURL()) + client.SetTeamId(team.Id) client.MockSession(c.Session.Token) - channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) + channelCreator := NewAutoChannelCreator(client, team) channelCreator.Fuzzy = doFuzz channelCreator.CreateTestChannels(channelsr) @@ -262,7 +288,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s } var usernames []string - if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err == nil { + if result := <-Srv.Store.User().GetProfiles(c.TeamId); result.Err == nil { profileUsers := result.Data.(map[string]*model.User) usernames = make([]string, len(profileUsers)) i := 0 @@ -273,6 +299,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s } client := model.NewClient(c.GetSiteURL()) + client.SetTeamId(c.TeamId) client.MockSession(c.Session.Token) testPoster := NewAutoPostCreator(client, channelId) testPoster.Fuzzy = doFuzz diff --git a/api/command_loadtest_test.go b/api/command_loadtest_test.go index 4988050a1..8c138842e 100644 --- a/api/command_loadtest_test.go +++ b/api/command_loadtest_test.go @@ -9,12 +9,14 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) func TestLoadTestHelpCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -23,18 +25,6 @@ func TestLoadTestHelpCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - rs := Client.Must(Client.Command(channel.Id, "/loadtest help", false)).Data.(*model.CommandResponse) if !strings.Contains(rs.Text, "Mattermost load testing commands to help") { t.Fatal(rs.Text) @@ -44,7 +34,10 @@ func TestLoadTestHelpCommands(t *testing.T) { } func TestLoadTestSetupCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -53,18 +46,6 @@ func TestLoadTestSetupCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - rs := Client.Must(Client.Command(channel.Id, "/loadtest setup fuzz 1 1 1", false)).Data.(*model.CommandResponse) if rs.Text != "Creating enviroment..." { t.Fatal(rs.Text) @@ -74,7 +55,10 @@ func TestLoadTestSetupCommands(t *testing.T) { } func TestLoadTestUsersCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -83,18 +67,6 @@ func TestLoadTestUsersCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - rs := Client.Must(Client.Command(channel.Id, "/loadtest users fuzz 1 2", false)).Data.(*model.CommandResponse) if rs.Text != "Adding users..." { t.Fatal(rs.Text) @@ -104,7 +76,10 @@ func TestLoadTestUsersCommands(t *testing.T) { } func TestLoadTestChannelsCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -113,18 +88,6 @@ func TestLoadTestChannelsCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - rs := Client.Must(Client.Command(channel.Id, "/loadtest channels fuzz 1 2", false)).Data.(*model.CommandResponse) if rs.Text != "Adding channels..." { t.Fatal(rs.Text) @@ -134,7 +97,10 @@ func TestLoadTestChannelsCommands(t *testing.T) { } func TestLoadTestPostsCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -143,18 +109,6 @@ func TestLoadTestPostsCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - rs := Client.Must(Client.Command(channel.Id, "/loadtest posts fuzz 2 3 2", false)).Data.(*model.CommandResponse) if rs.Text != "Adding posts..." { t.Fatal(rs.Text) @@ -164,7 +118,10 @@ func TestLoadTestPostsCommands(t *testing.T) { } func TestLoadTestUrlCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -173,18 +130,6 @@ func TestLoadTestUrlCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - command := "/loadtest url " if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Command must contain a url" { t.Fatal("/loadtest url with no url should've failed") @@ -223,7 +168,10 @@ func TestLoadTestUrlCommands(t *testing.T) { } func TestLoadTestJsonCommands(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json enableTesting := utils.Cfg.ServiceSettings.EnableTesting defer func() { @@ -232,18 +180,6 @@ func TestLoadTestJsonCommands(t *testing.T) { utils.Cfg.ServiceSettings.EnableTesting = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) - command := "/loadtest json " if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Command must contain a url" { t.Fatal("/loadtest url with no url should've failed") @@ -255,9 +191,9 @@ func TestLoadTestJsonCommands(t *testing.T) { t.Fatal("/loadtest url with invalid url should've failed") } - command = "/loadtest url https://secure.beldienst.nl/test.json" // Chicken-egg so will replace with mattermost/platform URL soon + command = "/loadtest json test-slack-attachments" if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." { - t.Fatal("/loadtest url for README.md should've executed") + t.Fatal("/loadtest json should've executed") } time.Sleep(2 * time.Second) diff --git a/api/command_logout_test.go b/api/command_logout_test.go index eee7520a8..eec959115 100644 --- a/api/command_logout_test.go +++ b/api/command_logout_test.go @@ -8,25 +8,12 @@ import ( "testing" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestLogoutTestCommand(t *testing.T) { - Setup() + th := Setup().InitBasic() - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - rs1 := Client.Must(Client.Command(channel1.Id, "/logout", false)).Data.(*model.CommandResponse) + rs1 := th.BasicClient.Must(th.BasicClient.Command(th.BasicChannel.Id, "/logout", false)).Data.(*model.CommandResponse) if !strings.HasSuffix(rs1.GotoLocation, "logout") { t.Fatal("failed to logout") } diff --git a/api/command_me_test.go b/api/command_me_test.go index d55a15b2c..f466f5764 100644 --- a/api/command_me_test.go +++ b/api/command_me_test.go @@ -8,35 +8,24 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestMeCommand(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testString := "/me hello" - r1 := Client.Must(Client.Command(channel1.Id, testString, false)).Data.(*model.CommandResponse) + r1 := Client.Must(Client.Command(channel.Id, testString, false)).Data.(*model.CommandResponse) if r1 == nil { t.Fatal("Command failed to execute") } time.Sleep(100 * time.Millisecond) - p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) - if len(p1.Order) != 1 { + p1 := Client.Must(Client.GetPosts(channel.Id, 0, 2, "")).Data.(*model.PostList) + if len(p1.Order) != 2 { t.Fatal("Command failed to send") } else { if p1.Posts[p1.Order[0]].Message != `*hello*` { diff --git a/api/command_msg.go b/api/command_msg.go index 273a45be9..517999695 100644 --- a/api/command_msg.go +++ b/api/command_msg.go @@ -46,7 +46,7 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) * targetUser = strings.SplitN(message, " ", 2)[0] targetUser = strings.TrimPrefix(targetUser, "@") - if profileList := <-Srv.Store.User().GetProfiles(c.Session.TeamId); profileList.Err != nil { + if profileList := <-Srv.Store.User().GetProfiles(c.TeamId); profileList.Err != nil { c.Err = profileList.Err return &model.CommandResponse{Text: c.T("api.command_msg.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { @@ -62,7 +62,7 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) * //Find the channel based on this user channelName := model.GetDMNameFromIds(c.Session.UserId, userProfile.Id) - if channel := <-Srv.Store.Channel().GetByName(c.Session.TeamId, channelName); channel.Err != nil { + if channel := <-Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil { if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" { if directChannel, err := CreateDirectChannel(c, userProfile.Id); err != nil { c.Err = err @@ -78,7 +78,7 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) * targetChannelId = channel.Data.(*model.Channel).Id } - makeDirectChannelVisible(c.Session.TeamId, targetChannelId) + makeDirectChannelVisible(c.TeamId, targetChannelId) if len(parsedMessage) > 0 { post := &model.Post{} post.Message = parsedMessage @@ -87,9 +87,11 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) * return &model.CommandResponse{Text: c.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } + return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + channelName, Text: c.T("api.command_msg.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } } + return &model.CommandResponse{Text: c.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } diff --git a/api/command_msg_test.go b/api/command_msg_test.go index 222a401fd..db8c3216c 100644 --- a/api/command_msg_test.go +++ b/api/command_msg_test.go @@ -4,39 +4,29 @@ package api import ( + "github.com/mattermost/platform/model" "strings" "testing" - - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestMsgCommands(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "user1", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test2@simulator.amazonses.com", Nickname: "Corey Hulen 2", Username: "user2", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test3@simulator.amazonses.com", Nickname: "Corey Hulen 3", Username: "user3", Password: "pwd"} - user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - - rs1 := Client.Must(Client.Command("", "/msg user2", false)).Data.(*model.CommandResponse) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user1 := th.BasicUser + user2 := th.BasicUser2 + user3 := th.CreateUser(th.BasicClient) + LinkUserToTeam(user3, team) + + Client.Must(Client.CreateDirectChannel(user2.Id)) + Client.Must(Client.CreateDirectChannel(user3.Id)) + + rs1 := Client.Must(Client.Command("", "/msg "+user2.Username, false)).Data.(*model.CommandResponse) if !strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) && !strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id) { t.Fatal("failed to create direct channel") } - rs2 := Client.Must(Client.Command("", "/msg user3 foobar", false)).Data.(*model.CommandResponse) + rs2 := Client.Must(Client.Command("", "/msg "+user3.Username+" foobar", false)).Data.(*model.CommandResponse) if !strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user3.Id) && !strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user3.Id+"__"+user1.Id) { t.Fatal("failed to create second direct channel") } @@ -44,7 +34,7 @@ func TestMsgCommands(t *testing.T) { t.Fatalf("post did not get sent to direct message") } - rs3 := Client.Must(Client.Command("", "/msg user2", false)).Data.(*model.CommandResponse) + rs3 := Client.Must(Client.Command("", "/msg "+user2.Username, false)).Data.(*model.CommandResponse) if !strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) && !strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id) { t.Fatal("failed to go back to existing direct channel") } diff --git a/api/command_shrug_test.go b/api/command_shrug_test.go index 92cecf664..99c10d191 100644 --- a/api/command_shrug_test.go +++ b/api/command_shrug_test.go @@ -8,35 +8,24 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" ) func TestShrugCommand(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testString := "/shrug" - r1 := Client.Must(Client.Command(channel1.Id, testString, false)).Data.(*model.CommandResponse) + r1 := Client.Must(Client.Command(channel.Id, testString, false)).Data.(*model.CommandResponse) if r1 == nil { t.Fatal("Command failed to execute") } time.Sleep(100 * time.Millisecond) - p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) - if len(p1.Order) != 1 { + p1 := Client.Must(Client.GetPosts(channel.Id, 0, 2, "")).Data.(*model.PostList) + if len(p1.Order) != 2 { t.Fatal("Command failed to send") } else { if p1.Posts[p1.Order[0]].Message != `¯\\\_(ツ)\_/¯` { diff --git a/api/command_test.go b/api/command_test.go index 22e2bd666..c6500c6cf 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -8,21 +8,12 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) func TestListCommands(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient if results, err := Client.ListCommands(); err != nil { t.Fatal(err) @@ -43,7 +34,10 @@ func TestListCommands(t *testing.T) { } func TestCreateCommand(t *testing.T) { - Setup() + th := Setup().InitBasic().InitSystemAdmin() + Client := th.BasicClient + user := th.SystemAdminUser + team := th.SystemAdminTeam enableCommands := *utils.Cfg.ServiceSettings.EnableCommands defer func() { @@ -51,26 +45,13 @@ func TestCreateCommand(t *testing.T) { }() *utils.Cfg.ServiceSettings.EnableCommands = true - 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() + "corey+test@test.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") - - cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST, Trigger: "trigger"} if _, err := Client.CreateCommand(cmd); err == nil { t.Fatal("should have failed because not admin") } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client = th.SystemAdminClient var rcmd *model.Command if result, err := Client.CreateCommand(cmd); err != nil { @@ -87,7 +68,7 @@ func TestCreateCommand(t *testing.T) { t.Fatal("team ids didn't match") } - cmd = &model.Command{CreatorId: "123", TeamId: "456", URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd = &model.Command{CreatorId: "123", TeamId: "456", URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST, Trigger: "trigger"} if result, err := Client.CreateCommand(cmd); err != nil { t.Fatal(err) } else { @@ -101,27 +82,16 @@ func TestCreateCommand(t *testing.T) { } func TestListTeamCommands(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands }() *utils.Cfg.ServiceSettings.EnableCommands = true - 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)) - - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - cmd1 := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd1 := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST, Trigger: "trigger"} cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command) if result, err := Client.ListTeamCommands(); err != nil { @@ -136,27 +106,16 @@ func TestListTeamCommands(t *testing.T) { } func TestRegenToken(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands }() *utils.Cfg.ServiceSettings.EnableCommands = true - 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() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST, Trigger: "trigger"} cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command) data := make(map[string]string) @@ -172,27 +131,16 @@ func TestRegenToken(t *testing.T) { } func TestDeleteCommand(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands }() *utils.Cfg.ServiceSettings.EnableCommands = true - 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)) - - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST, Trigger: "trigger"} cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command) data := make(map[string]string) @@ -209,31 +157,18 @@ func TestDeleteCommand(t *testing.T) { } func TestTestCommand(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + channel1 := th.SystemAdminChannel + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands }() *utils.Cfg.ServiceSettings.EnableCommands = true - 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)) - - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - cmd1 := &model.Command{ - URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test", + URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test", Method: model.COMMAND_METHOD_POST, Trigger: "test", } @@ -253,7 +188,7 @@ func TestTestCommand(t *testing.T) { } cmd2 := &model.Command{ - URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test", + URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test", Method: model.COMMAND_METHOD_GET, Trigger: "test2", } diff --git a/api/context.go b/api/context.go index 56c8c86d2..8bbd5a1d2 100644 --- a/api/context.go +++ b/api/context.go @@ -11,10 +11,12 @@ import ( "strings" l4g "github.com/alecthomas/log4go" + "github.com/gorilla/mux" + goi18n "github.com/nicksnyder/go-i18n/i18n" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" - goi18n "github.com/nicksnyder/go-i18n/i18n" ) var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) @@ -39,6 +41,7 @@ type Context struct { siteURL string T goi18n.TranslateFunc Locale string + TeamId string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -94,6 +97,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.T, c.Locale = utils.GetTranslationsAndLocale(w, r) c.RequestId = model.NewId() c.IpAddress = GetIpAddress(r) + c.TeamId = mux.Vars(r)["team_id"] + h.isApi = IsApiCall(r) token := "" isTokenFromQueryString := false @@ -116,7 +121,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if (h.requireSystemAdmin || h.requireUser) && !h.trustRequester { if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML { - c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to bea CSRF attempt") token = "" } } @@ -182,6 +187,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SystemAdminRequired() } + if c.Err == nil && len(c.TeamId) > 0 { + c.HasPermissionsToTeam(c.TeamId, "TeamRoute") + } + if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 { go func() { if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil { @@ -308,13 +317,14 @@ func (c *Context) HasPermissionsToUser(userId string, where string) bool { } func (c *Context) HasPermissionsToTeam(teamId string, where string) bool { - if c.Session.TeamId == teamId { + if c.IsSystemAdmin() { return true } - // You're a mattermost system admin and you're on the VPN - if c.IsSystemAdmin() { - return true + for _, teamMember := range c.Session.TeamMembers { + if teamId == teamMember.TeamId { + return true + } } c.Err = model.NewLocAppError(where, "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", teamId="+teamId) @@ -353,10 +363,17 @@ func (c *Context) IsSystemAdmin() bool { } func (c *Context) IsTeamAdmin() bool { - if model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) || c.IsSystemAdmin() { + + if c.IsSystemAdmin() { return true } - return false + + team := c.Session.GetTeamByTeamId(c.TeamId) + if team == nil { + return false + } + + return model.IsInRole(team.Roles, model.ROLE_TEAM_ADMIN) } func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { @@ -386,7 +403,7 @@ func (c *Context) setTeamURL(url string, valid bool) { } func (c *Context) SetTeamURLFromSession() { - if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err == nil { + if result := <-Srv.Store.Team().Get(c.TeamId); result.Err == nil { c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true) } } @@ -413,6 +430,10 @@ func (c *Context) GetSiteURL() string { return c.siteURL } +func IsApiCall(r *http.Request) bool { + return strings.Index(r.URL.Path, "/api/") == 0 +} + func GetIpAddress(r *http.Request) string { address := r.Header.Get(model.HEADER_FORWARDED) @@ -427,69 +448,69 @@ func GetIpAddress(r *http.Request) string { return address } -func IsTestDomain(r *http.Request) bool { +// func IsTestDomain(r *http.Request) bool { - if strings.Index(r.Host, "localhost") == 0 { - return true - } +// if strings.Index(r.Host, "localhost") == 0 { +// return true +// } - if strings.Index(r.Host, "dockerhost") == 0 { - return true - } +// if strings.Index(r.Host, "dockerhost") == 0 { +// return true +// } - if strings.Index(r.Host, "test") == 0 { - return true - } +// if strings.Index(r.Host, "test") == 0 { +// return true +// } - if strings.Index(r.Host, "127.0.") == 0 { - return true - } +// if strings.Index(r.Host, "127.0.") == 0 { +// return true +// } - if strings.Index(r.Host, "192.168.") == 0 { - return true - } +// if strings.Index(r.Host, "192.168.") == 0 { +// return true +// } - if strings.Index(r.Host, "10.") == 0 { - return true - } +// if strings.Index(r.Host, "10.") == 0 { +// return true +// } - if strings.Index(r.Host, "176.") == 0 { - return true - } +// if strings.Index(r.Host, "176.") == 0 { +// return true +// } - return false -} +// return false +// } -func IsBetaDomain(r *http.Request) bool { +// func IsBetaDomain(r *http.Request) bool { - if strings.Index(r.Host, "beta") == 0 { - return true - } +// if strings.Index(r.Host, "beta") == 0 { +// return true +// } - if strings.Index(r.Host, "ci") == 0 { - return true - } +// if strings.Index(r.Host, "ci") == 0 { +// return true +// } - return false -} +// return false +// } -var privateIpAddress = []*net.IPNet{ - {IP: net.IPv4(10, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, - {IP: net.IPv4(176, 16, 0, 1), Mask: net.IPv4Mask(255, 255, 0, 0)}, - {IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, - {IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 252)}, -} +// var privateIpAddress = []*net.IPNet{ +// {IP: net.IPv4(10, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, +// {IP: net.IPv4(176, 16, 0, 1), Mask: net.IPv4Mask(255, 255, 0, 0)}, +// {IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, +// {IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 252)}, +// } -func IsPrivateIpAddress(ipAddress string) bool { +// func IsPrivateIpAddress(ipAddress string) bool { - for _, pips := range privateIpAddress { - if pips.Contains(net.ParseIP(ipAddress)) { - return true - } - } +// for _, pips := range privateIpAddress { +// if pips.Contains(net.ParseIP(ipAddress)) { +// return true +// } +// } - return false -} +// return false +// } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { T, _ := utils.GetTranslationsAndLocale(w, r) @@ -513,9 +534,17 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) func Handle404(w http.ResponseWriter, r *http.Request) { err := model.NewLocAppError("Handle404", "api.context.404.app_error", nil, "") + err.Translate(utils.T) err.StatusCode = http.StatusNotFound l4g.Error("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r)) - RenderWebError(err, w, r) + + if IsApiCall(r) { + w.WriteHeader(err.StatusCode) + err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'. Typo? are you missing a team_id or user_id as part of the url?" + w.Write([]byte(err.ToJson())) + } else { + RenderWebError(err, w, r) + } } func GetSession(token string) *model.Session { @@ -542,6 +571,20 @@ func GetSession(token string) *model.Session { return session } +func RemoveAllSessionsForUserId(userId string) { + + keys := sessionCache.Keys() + + for _, key := range keys { + if ts, ok := sessionCache.Get(key); ok { + session := ts.(*model.Session) + if session.UserId == userId { + sessionCache.Remove(key) + } + } + } +} + func AddSessionToCache(session *model.Session) { sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/api/context_test.go b/api/context_test.go index a9e2afa0f..c3c7a9768 100644 --- a/api/context_test.go +++ b/api/context_test.go @@ -8,32 +8,6 @@ import ( "testing" ) -var ipAddressTests = []struct { - address string - expected bool -}{ - {"126.255.255.255", false}, - {"127.0.0.1", true}, - {"127.0.0.4", false}, - {"9.255.255.255", false}, - {"10.0.0.1", true}, - {"11.0.0.1", false}, - {"176.15.155.255", false}, - {"176.16.0.1", true}, - {"176.31.0.1", false}, - {"192.167.255.255", false}, - {"192.168.0.1", true}, - {"192.169.0.1", false}, -} - -func TestIpAddress(t *testing.T) { - for _, v := range ipAddressTests { - if IsPrivateIpAddress(v.address) != v.expected { - t.Errorf("expect %v as %v", v.address, v.expected) - } - } -} - func TestContext(t *testing.T) { context := Context{} @@ -52,9 +26,26 @@ func TestContext(t *testing.T) { if !context.HasPermissionsToUser("6", "") { t.Fatal("should have permissions") } +} - // context.IpAddress = "125.0.0.1" - // if context.HasPermissionsToUser("6", "") { - // t.Fatal("shouldn't have permissions") - // } +func TestCache(t *testing.T) { + session := &model.Session{ + Id: model.NewId(), + Token: model.NewId(), + UserId: model.NewId(), + } + + sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) + + keys := sessionCache.Keys() + if len(keys) <= 0 { + t.Fatal("should have items") + } + + RemoveAllSessionsForUserId(session.UserId) + + rkeys := sessionCache.Keys() + if len(rkeys) != len(keys)-1 { + t.Fatal("should have one less") + } } diff --git a/api/export.go b/api/export.go index f2f8f87ab..da066379f 100644 --- a/api/export.go +++ b/api/export.go @@ -60,7 +60,7 @@ func ExportToFile(options *ExportOptions) (link string, err *model.AppError) { ExportToWriter(file, options) } - return "/api/v1/files/get_export", nil + return model.API_URL_SUFFIX + "/files/get_export", nil } func ExportToWriter(w io.Writer, options *ExportOptions) *model.AppError { diff --git a/api/file.go b/api/file.go index 991516bed..c51a4a046 100644 --- a/api/file.go +++ b/api/file.go @@ -57,15 +57,14 @@ const ( var fileInfoCache *utils.Cache = utils.NewLru(1000) -func InitFile(r *mux.Router) { +func InitFile() { l4g.Debug(utils.T("api.file.init.debug")) - sr := r.PathPrefix("/files").Subrouter() - sr.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") - sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandlerTrustRequester(getFile)).Methods("GET") - sr.Handle("/get_info/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFileInfo)).Methods("GET") - sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") - sr.Handle("/get_export", ApiUserRequired(getExport)).Methods("GET") + BaseRoutes.Files.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") + BaseRoutes.Files.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandlerTrustRequester(getFile)).Methods("GET") + BaseRoutes.Files.Handle("/get_info/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFileInfo)).Methods("GET") + BaseRoutes.Files.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") + BaseRoutes.Files.Handle("/get_export", ApiUserRequired(getExport)).Methods("GET") } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { @@ -101,7 +100,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) files := m.File["files"] @@ -147,7 +146,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { } } - path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename + path := "teams/" + c.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename if err := WriteFile(buf.Bytes(), path); err != nil { c.Err = err @@ -164,7 +163,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { resStruct.ClientIds = append(resStruct.ClientIds, clientId) } - handleImagesAndForget(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) + handleImagesAndForget(imageNameList, imageDataList, c.TeamId, channelId, c.Session.UserId) w.Write([]byte(resStruct.ToJson())) } @@ -319,9 +318,9 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) - path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename + path := "teams/" + c.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename var info *model.FileInfo if cached, ok := fileInfoCache.Get(path); ok { @@ -380,13 +379,13 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { data := r.URL.Query().Get("d") teamId := r.URL.Query().Get("t") - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) path := "" if len(teamId) == 26 { path = "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename } else { - path = "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename + path = "teams/" + c.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename } fileData := make(chan []byte) @@ -460,6 +459,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.FileSettings.EnablePublicLink { c.Err = model.NewLocAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden + return } props := model.MapFromJson(r.Body) @@ -480,7 +480,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { userId := matches[0][2] filename = matches[0][3] - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) newProps := make(map[string]string) newProps["filename"] = filename @@ -488,7 +488,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) - url := fmt.Sprintf("%s/api/v1/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL(), channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId) + url := fmt.Sprintf("%s/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL()+model.API_URL_SUFFIX, channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.TeamId) if !c.HasPermissionsToChannel(cchan, "getPublicLink") { return @@ -501,7 +501,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { } func getExport(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin() { + if !c.HasPermissionsToTeam(c.TeamId, "export") || !c.IsTeamAdmin() { c.Err = model.NewLocAppError("getExport", "api.file.get_export.team_admin.app_error", nil, "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return diff --git a/api/file_benchmark_test.go b/api/file_benchmark_test.go index a02bffa0e..d73097072 100644 --- a/api/file_benchmark_test.go +++ b/api/file_benchmark_test.go @@ -13,7 +13,9 @@ import ( ) func BenchmarkUploadFile(b *testing.B) { - _, _, channel := SetupBenchmark() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) @@ -25,7 +27,10 @@ func BenchmarkUploadFile(b *testing.B) { } func BenchmarkGetFile(b *testing.B) { - team, _, channel := SetupBenchmark() + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) filenames, err := testPoster.UploadTestFile() @@ -53,7 +58,9 @@ func BenchmarkGetFile(b *testing.B) { } func BenchmarkGetPublicLink(b *testing.B) { - _, _, channel := SetupBenchmark() + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) filenames, err := testPoster.UploadTestFile() diff --git a/api/file_test.go b/api/file_test.go index 3aa1a56f9..dd4a8520b 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -22,19 +22,11 @@ import ( ) func TestUploadFile(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + channel := th.BasicChannel body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -45,6 +37,9 @@ func TestUploadFile(t *testing.T) { path := utils.FindDir("tests") file, err := os.Open(path + "/test.png") + if err != nil { + t.Fatal(err) + } defer file.Close() _, err = io.Copy(part, file) @@ -57,7 +52,7 @@ func TestUploadFile(t *testing.T) { t.Fatal(err) } - _, err = field.Write([]byte(channel1.Id)) + _, err = field.Write([]byte(channel.Id)) if err != nil { t.Fatal(err) } @@ -67,7 +62,7 @@ func TestUploadFile(t *testing.T) { t.Fatal(err) } - resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + resp, appErr := Client.UploadPostAttachment(body.Bytes(), writer.FormDataContentType()) if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { if appErr != nil { t.Fatal(appErr) @@ -90,17 +85,17 @@ func TestUploadFile(t *testing.T) { // wait a bit for files to ready time.Sleep(5 * time.Second) - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename) + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename) if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg") if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg") if err != nil { t.Fatal(err) } @@ -115,17 +110,17 @@ func TestUploadFile(t *testing.T) { // wait a bit for files to ready time.Sleep(5 * time.Second) - path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -137,25 +132,18 @@ func TestUploadFile(t *testing.T) { } func TestGetFile(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + channel := th.BasicChannel + enablePublicLink := utils.Cfg.FileSettings.EnablePublicLink defer func() { utils.Cfg.FileSettings.EnablePublicLink = enablePublicLink }() utils.Cfg.FileSettings.EnablePublicLink = true - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - if utils.Cfg.FileSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -181,7 +169,7 @@ func TestGetFile(t *testing.T) { t.Fatal(err) } - _, err = field.Write([]byte(channel1.Id)) + _, err = field.Write([]byte(channel.Id)) if err != nil { t.Fatal(err) } @@ -191,7 +179,7 @@ func TestGetFile(t *testing.T) { t.Fatal(err) } - resp, upErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + resp, upErr := Client.UploadPostAttachment(body.Bytes(), writer.FormDataContentType()) if upErr != nil { t.Fatal(upErr) } @@ -217,8 +205,9 @@ func TestGetFile(t *testing.T) { team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - user2 := &model.User{TeamId: team2.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team2) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) newProps := make(map[string]string) @@ -229,6 +218,7 @@ func TestGetFile(t *testing.T) { hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) Client.LoginByEmail(team2.Name, user2.Email, "pwd") + Client.SetTeamId(team2.Id) if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t="+team.Id, false); downErr != nil { t.Fatal(downErr) @@ -278,17 +268,17 @@ func TestGetFile(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename) + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename) if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg") if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg") if err != nil { t.Fatal(err) } @@ -297,17 +287,17 @@ func TestGetFile(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -320,25 +310,18 @@ func TestGetFile(t *testing.T) { } func TestGetPublicLink(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + channel := th.BasicChannel if utils.Cfg.FileSettings.DriverName != "" { + enablePublicLink := utils.Cfg.FileSettings.EnablePublicLink + defer func() { + utils.Cfg.FileSettings.EnablePublicLink = enablePublicLink + }() + utils.Cfg.FileSettings.EnablePublicLink = true body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -364,7 +347,7 @@ func TestGetPublicLink(t *testing.T) { t.Fatal(err) } - _, err = field.Write([]byte(channel1.Id)) + _, err = field.Write([]byte(channel.Id)) if err != nil { t.Fatal(err) } @@ -374,14 +357,14 @@ func TestGetPublicLink(t *testing.T) { t.Fatal(err) } - resp, upErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + resp, upErr := Client.UploadPostAttachment(body.Bytes(), writer.FormDataContentType()) if upErr != nil { t.Fatal(upErr) } filenames := resp.Data.(*model.FileUploadResponse).Filenames - post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", Filenames: filenames} + post1 := &model.Post{ChannelId: channel.Id, Message: "a" + model.NewId() + "a", Filenames: filenames} rpost1, postErr := Client.CreatePost(post1) if postErr != nil { @@ -408,7 +391,8 @@ func TestGetPublicLink(t *testing.T) { t.Fatal("Should have errored - bad file path") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() + data["filename"] = filenames[0] if _, err := Client.GetPublicLink(data); err == nil { t.Fatal("should have errored, user not member of channel") @@ -427,17 +411,17 @@ func TestGetPublicLink(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename) + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename) if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg") if err != nil { t.Fatal(err) } - err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg") + err = bucket.Del("teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg") if err != nil { t.Fatal(err) } @@ -446,17 +430,17 @@ func TestGetPublicLink(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel.Id + "/users/" + user.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } diff --git a/api/import.go b/api/import.go index 7590277b0..c39ec5220 100644 --- a/api/import.go +++ b/api/import.go @@ -22,7 +22,7 @@ func ImportPost(post *model.Post) { } } -func ImportUser(user *model.User) *model.User { +func ImportUser(teamId string, user *model.User) *model.User { user.MakeNonNil() if result := <-Srv.Store.User().Save(user); result.Err != nil { @@ -31,8 +31,8 @@ func ImportUser(user *model.User) *model.User { } else { ruser := result.Data.(*model.User) - if err := JoinDefaultChannels(ruser, ""); err != nil { - l4g.Error(utils.T("api.import.import_user.joining_default.error"), ruser.Id, ruser.TeamId, err) + if err := JoinDefaultChannels(teamId, ruser, ""); err != nil { + l4g.Error(utils.T("api.import.import_user.joining_default.error"), ruser.Id, teamId, err) } if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { diff --git a/api/license.go b/api/license.go index 4bf8cd3b8..1dbb2b281 100644 --- a/api/license.go +++ b/api/license.go @@ -6,7 +6,6 @@ package api import ( "bytes" l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "io" @@ -19,13 +18,12 @@ const ( INVALID_LICENSE_ERROR = "api.license.add_license.invalid.app_error" ) -func InitLicense(r *mux.Router) { +func InitLicense() { l4g.Debug(utils.T("api.license.init.debug")) - 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") + BaseRoutes.License.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST") + BaseRoutes.License.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST") + BaseRoutes.License.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } func LoadLicense() { diff --git a/api/license_test.go b/api/license_test.go index 0126d6e54..c5fffd6e9 100644 --- a/api/license_test.go +++ b/api/license_test.go @@ -9,7 +9,8 @@ import ( ) func TestGetLicenceConfig(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient if result, err := Client.GetClientLicenceConfig(""); err != nil { t.Fatal(err) diff --git a/api/oauth.go b/api/oauth.go index a7119d7e5..0375f4e6f 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -4,7 +4,10 @@ package api import ( + "crypto/tls" + b64 "encoding/base64" "fmt" + "io" "net/http" "net/url" "strconv" @@ -12,31 +15,29 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) -func InitOAuth(r *mux.Router) { +func InitOAuth() { l4g.Debug(utils.T("api.oauth.init.debug")) - sr := r.PathPrefix("/oauth").Subrouter() + BaseRoutes.OAuth.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") + BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") - 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") - - mr := Srv.Router - mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") - mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + BaseRoutes.Root.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") // Handle all the old routes, to be later removed - mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") - mr.Handle("/signup/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") - mr.Handle("/login/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/signup/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/login/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -190,40 +191,40 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { uri := c.GetSiteURL() + "/signup/" + service + "/complete" - if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + if body, teamId, 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) + CreateOAuthUser(c, w, r, service, body, teamId) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_LOGIN: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_EMAIL_TO_SSO: - CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + CompleteSwitchWithOAuth(c, w, r, service, body, props["email"]) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_SSO_TO_EMAIL: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) } break default: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break } @@ -257,7 +258,7 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } var team *model.Team - if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -389,7 +390,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + session := &model.Session{UserId: user.Id, 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, "") @@ -422,24 +423,11 @@ 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 { + if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil { c.Err = err return } else { @@ -450,31 +438,19 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { 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 = model.NewLocAppError("signupWithOAuth", "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) - } + teamId := "" + inviteId := r.URL.Query().Get("id") - if IsVerifyHashRequired(nil, team, hash) { + if len(hash) > 0 { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) @@ -489,19 +465,173 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return + teamId = props["id"] + } else if len(inviteId) != 0 { + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + // soft fail, so we still create user but don't auto-join team + l4g.Error("%v", result.Err) + } else { + teamId = result.Data.(*model.Team).Id } } stateProps := map[string]string{} stateProps["action"] = model.OAUTH_ACTION_SIGNUP + if len(teamId) != 0 { + stateProps["team_id"] = teamId + } - if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil { c.Err = err return } else { http.Redirect(w, r, authUrl, http.StatusFound) } } + +func GetAuthorizationCode(c *Context, service string, props map[string]string, loginHint string) (string, *model.AppError) { + + sso := utils.Cfg.GetSSOService(service) + if sso != nil && !sso.Enable { + return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) + } + + clientId := sso.Id + endpoint := sso.AuthEndpoint + scope := sso.Scope + + props["hash"] = model.HashPassword(clientId) + state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) + + redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" + + authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) + + if len(scope) > 0 { + authUrl += "&scope=" + utils.UrlEncode(scope) + } + + if len(loginHint) > 0 { + authUrl += "&login_hint=" + utils.UrlEncode(loginHint) + } + + return authUrl, nil +} + +func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { + sso := utils.Cfg.GetSSOService(service) + if sso == nil || !sso.Enable { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service) + } + + stateStr := "" + if b, err := b64.StdEncoding.DecodeString(state); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error()) + } else { + stateStr = string(b) + } + + stateProps := model.MapFromJson(strings.NewReader(stateStr)) + + if !model.ComparePassword(stateProps["hash"], sso.Id) { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") + } + + teamId := stateProps["team_id"] + + p := url.Values{} + p.Set("client_id", sso.Id) + p.Set("client_secret", sso.Secret) + p.Set("code", code) + p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + p.Set("redirect_uri", redirectUri) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + var ar *model.AccessResponse + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) + } else { + ar = model.AccessResponseFromJson(resp.Body) + if ar == nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, "") + } + } + + if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType) + } + + if len(ar.AccessToken) == 0 { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "") + } + + p = url.Values{} + p.Set("access_token", ar.AccessToken) + req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+ar.AccessToken) + + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", + map[string]interface{}{"Service": service}, err.Error()) + } else { + return resp.Body, teamId, stateProps, nil + } + +} + +func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, email string) { + authData := "" + ssoEmail := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", + map[string]interface{}{"Service": service}, "") + return + } else { + ssoUser := provider.GetUserFromJson(userData) + authData = ssoUser.AuthData + ssoEmail = ssoUser.Email + } + + if len(authData) == 0 { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error", + map[string]interface{}{"Service": service}, "") + return + } + + if len(email) == 0 { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "") + return + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil { + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(c, user.Email, c.GetSiteURL(), strings.Title(service)+" SSO") +} diff --git a/api/oauth_test.go b/api/oauth_test.go index 57772ccc5..aa3c025a7 100644 --- a/api/oauth_test.go +++ b/api/oauth_test.go @@ -5,22 +5,14 @@ package api import ( "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/url" - "strings" "testing" ) func TestRegisterApp(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", Password: "pwd"} - ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + th := Setup().InitBasic() + Client := th.BasicClient app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -38,7 +30,7 @@ func TestRegisterApp(t *testing.T) { t.Fatal("not logged in - should have failed") } - Client.Must(Client.LoginById(ruser.Id, "pwd")) + th.LoginBasic() if result, err := Client.RegisterApp(app); err != nil { t.Fatal(err) @@ -70,19 +62,11 @@ func TestRegisterApp(t *testing.T) { } func TestAllowOAuth(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", Password: "pwd"} - ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + th := Setup().InitBasic() + Client := th.BasicClient app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - Client.Must(Client.LoginById(ruser.Id, "pwd")) - state := "123" if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { diff --git a/api/post.go b/api/post.go index 6f88c815b..cbfbf49f2 100644 --- a/api/post.go +++ b/api/post.go @@ -22,21 +22,21 @@ import ( "time" ) -func InitPost(r *mux.Router) { +func InitPost() { l4g.Debug(utils.T("api.post.init.debug")) - r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET") - r.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET") - - sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter() - sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST") - sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") - sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET") - sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET") - sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET") - sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST") - sr.Handle("/post/{post_id:[A-Za-z0-9]+}/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET") - sr.Handle("/post/{post_id:[A-Za-z0-9]+}/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET") + + BaseRoutes.Posts.Handle("/create", ApiUserRequired(createPost)).Methods("POST") + BaseRoutes.Posts.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") + BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET") + BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET") + + BaseRoutes.NeedPost.Handle("/get", ApiUserRequired(getPost)).Methods("GET") + BaseRoutes.NeedPost.Handle("/delete", ApiUserRequired(deletePost)).Methods("POST") + BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET") + BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET") } func createPost(c *Context, w http.ResponseWriter, r *http.Request) { @@ -47,7 +47,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } // Create and save post object to channel - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, post.ChannelId, c.Session.UserId) if !c.HasPermissionsToChannel(cchan, "createPost") { return @@ -228,15 +228,16 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks bool) { go func() { - tchan := Srv.Store.Team().Get(c.Session.TeamId) + tchan := Srv.Store.Team().Get(c.TeamId) cchan := Srv.Store.Channel().Get(post.ChannelId) uchan := Srv.Store.User().Get(post.UserId) - pchan := Srv.Store.User().GetProfiles(c.Session.TeamId) + pchan := Srv.Store.User().GetProfiles(c.TeamId) + dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId) mchan := Srv.Store.Channel().GetMembers(post.ChannelId) var team *model.Team if result := <-tchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.team.error"), c.Session.TeamId, result.Err) + l4g.Error(utils.T("api.post.handle_post_events_and_forget.team.error"), c.TeamId, result.Err) return } else { team = result.Data.(*model.Team) @@ -252,12 +253,22 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo var profiles map[string]*model.User if result := <-pchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.Session.TeamId, result.Err) + l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) return } else { profiles = result.Data.(map[string]*model.User) } + if result := <-dpchan; result.Err != nil { + l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) + return + } else { + dps := result.Data.(map[string]*model.User) + for k, v := range dps { + profiles[k] = v + } + } + var members []model.ChannelMember if result := <-mchan; result.Err != nil { l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err) @@ -282,7 +293,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo } if channel.Type == model.CHANNEL_DIRECT { - go makeDirectChannelVisible(c.Session.TeamId, post.ChannelId) + go makeDirectChannelVisible(c.TeamId, post.ChannelId) } }() } @@ -352,7 +363,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team return } - hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId) + hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId) hooks := []*model.OutgoingWebhook{} @@ -416,8 +427,25 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team respProps := model.MapFromJson(resp.Body) // 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, c.T, c.Locale} + mockSession := model.Session{ + UserId: hook.CreatorId, + TeamMembers: []*model.TeamMember{{TeamId: hook.TeamId, UserId: hook.CreatorId}}, + IsOAuth: false, + } + + newContext := &Context{ + Session: mockSession, + RequestId: model.NewId(), + IpAddress: "", + Path: c.Path, + Err: nil, + teamURLValid: c.teamURLValid, + teamURL: c.teamURL, + siteURL: c.siteURL, + T: c.T, + Locale: c.Locale, + TeamId: hook.TeamId, + } if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { @@ -706,7 +734,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, } httpClient := &http.Client{Transport: tr} - request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) + request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) if _, err := httpClient.Do(request); err != nil { @@ -719,7 +747,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * } } - message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) + message := model.NewMessage(c.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) @@ -780,7 +808,7 @@ func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.C } SendEphemeralPost( - c.Session.TeamId, + c.TeamId, post.UserId, &model.Post{ ChannelId: post.ChannelId, @@ -847,7 +875,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, post.ChannelId, c.Session.UserId) pchan := Srv.Store.Post().Get(post.Id) if !c.HasPermissionsToChannel(cchan, "updatePost") { @@ -889,7 +917,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { } else { rpost := result.Data.(*model.Post) - message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) + message := model.NewMessage(c.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) message.Add("post", rpost.ToJson()) PublishAndForget(message) @@ -901,7 +929,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] if len(id) != 26 { c.SetInvalidParam("getPosts", "channelId") return @@ -919,7 +947,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, id, c.Session.UserId) etagChan := Srv.Store.Post().GetEtag(id) if !c.HasPermissionsToChannel(cchan, "getPosts") { @@ -949,7 +977,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] if len(id) != 26 { c.SetInvalidParam("getPostsSince", "channelId") return @@ -961,7 +989,7 @@ func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, id, c.Session.UserId) pchan := Srv.Store.Post().GetPostsSince(id, time) if !c.HasPermissionsToChannel(cchan, "getPostsSince") { @@ -982,7 +1010,7 @@ func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { func getPost(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - channelId := params["id"] + channelId := params["channel_id"] if len(channelId) != 26 { c.SetInvalidParam("getPost", "channelId") return @@ -994,7 +1022,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) pchan := Srv.Store.Post().Get(postId) if !c.HasPermissionsToChannel(cchan, "getPost") { @@ -1041,7 +1069,7 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) { } post := list.Posts[list.Order[0]] - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, post.ChannelId, c.Session.UserId) if !c.HasPermissionsToChannel(cchan, "getPostById") { return } @@ -1058,7 +1086,7 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) { func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - channelId := params["id"] + channelId := params["channel_id"] if len(channelId) != 26 { c.SetInvalidParam("deletePost", "channelId") return @@ -1070,7 +1098,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.Session.UserId) pchan := Srv.Store.Post().Get(postId) if result := <-pchan; result.Err != nil { @@ -1106,11 +1134,11 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - message := model.NewMessage(c.Session.TeamId, post.ChannelId, c.Session.UserId, model.ACTION_POST_DELETED) + message := model.NewMessage(c.TeamId, post.ChannelId, c.Session.UserId, model.ACTION_POST_DELETED) message.Add("post", post.ToJson()) PublishAndForget(message) - DeletePostFilesAndForget(c.Session.TeamId, post) + DeletePostFilesAndForget(c.TeamId, post) result := make(map[string]string) result["id"] = postId @@ -1146,7 +1174,7 @@ func getPostsAfter(c *Context, w http.ResponseWriter, r *http.Request) { func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, before bool) { params := mux.Vars(r) - id := params["id"] + id := params["channel_id"] if len(id) != 26 { c.SetInvalidParam("getPostsBeforeOrAfter", "channelId") return @@ -1170,7 +1198,7 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b return } - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, id, c.Session.UserId) // We can do better than this etag in this situation etagChan := Srv.Store.Post().GetEtag(id) @@ -1215,7 +1243,7 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { for _, params := range paramsList { // don't allow users to search for everything if params.Terms != "*" { - channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params)) + channels = append(channels, Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params)) } } diff --git a/api/post_benchmark_test.go b/api/post_benchmark_test.go index 00eb3c468..4e5f6668f 100644 --- a/api/post_benchmark_test.go +++ b/api/post_benchmark_test.go @@ -16,7 +16,10 @@ func BenchmarkCreatePost(b *testing.B) { var ( NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) @@ -32,7 +35,10 @@ func BenchmarkUpdatePost(b *testing.B) { NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} UPDATE_POST_LEN = 100 ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE) @@ -59,7 +65,10 @@ func BenchmarkGetPosts(b *testing.B) { var ( NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) testPoster.CreateTestPosts(NUM_POSTS_RANGE) @@ -75,7 +84,10 @@ func BenchmarkSearchPosts(b *testing.B) { var ( NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) testPoster.CreateTestPosts(NUM_POSTS_RANGE) @@ -93,7 +105,10 @@ func BenchmarkEtagCache(b *testing.B) { var ( NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) testPoster.CreateTestPosts(NUM_POSTS_RANGE) @@ -111,7 +126,10 @@ func BenchmarkDeletePosts(b *testing.B) { var ( NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} ) - _, _, channel := SetupBenchmark() + + th := Setup().InitBasic() + Client := th.BasicClient + channel := th.BasicChannel testPoster := NewAutoPostCreator(Client, channel.Id) posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE) diff --git a/api/post_test.go b/api/post_test.go index 2d978d3e3..b905c143e 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -5,38 +5,24 @@ package api import ( "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" - "strings" + //"strings" + "fmt" "testing" "time" ) func TestCreatePost(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) - - team2 := &model.Team{DisplayName: "Name Team 2", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + team2 := th.CreateTeam(th.BasicClient) + user1 := th.BasicUser + user3 := th.CreateUser(th.BasicClient) + LinkUserToTeam(user3, team2) + channel1 := th.BasicChannel + channel2 := th.CreateChannel(Client, team) filenames := []string{"/12345678901234567890123456/12345678901234567890123456/12345678901234567890123456/test.png", "/" + channel1.Id + "/" + user1.Id + "/test.png", "www.mattermost.com/fake/url", "junk"} @@ -97,21 +83,17 @@ func TestCreatePost(t *testing.T) { t.Fatal("Should have been forbidden") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() + post7 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} _, err = Client.CreatePost(post7) if err.StatusCode != http.StatusForbidden { t.Fatal("Should have been forbidden") } - user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - - Client.LoginByEmail(team2.Name, user3.Email, "pwd") - - channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} - channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + Client.LoginByEmail(team2.Name, user3.Email, user3.Password) + Client.SetTeamId(team2.Id) + channel3 := th.CreateChannel(Client, team2) post8 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} _, err = Client.CreatePost(post8) @@ -125,29 +107,9 @@ func TestCreatePost(t *testing.T) { } func TestUpdatePost(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) - - team2 := &model.Team{DisplayName: "Name Team 2", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} rpost1, err := Client.CreatePost(post1) @@ -196,19 +158,9 @@ func TestUpdatePost(t *testing.T) { } func TestGetPosts(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel time.Sleep(10 * time.Millisecond) post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -261,19 +213,9 @@ func TestGetPosts(t *testing.T) { } func TestGetPostsSince(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel time.Sleep(10 * time.Millisecond) post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -331,19 +273,9 @@ func TestGetPostsSince(t *testing.T) { } func TestGetPostsBeforeAfter(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel time.Sleep(10 * time.Millisecond) post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -379,7 +311,8 @@ func TestGetPostsBeforeAfter(t *testing.T) { t.Fatal("wrong order") } - if len(r1.Posts) != 2 { + if len(r1.Posts) != 3 { + t.Log(r1.Posts) t.Fatal("wrong size") } @@ -408,19 +341,9 @@ func TestGetPostsBeforeAfter(t *testing.T) { } func TestSearchPosts(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel post1 := &model.Post{ChannelId: channel1.Id, Message: "search for post1"} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) @@ -458,19 +381,9 @@ func TestSearchPosts(t *testing.T) { } func TestSearchHashtagPosts(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel post1 := &model.Post{ChannelId: channel1.Id, Message: "#sgtitlereview with space"} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) @@ -489,19 +402,10 @@ func TestSearchHashtagPosts(t *testing.T) { } func TestSearchPostsInChannel(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel + team := th.BasicTeam post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) @@ -529,7 +433,7 @@ func TestSearchPostsInChannel(t *testing.T) { t.Fatalf("wrong number of posts returned %v", len(result.Order)) } - if result := Client.Must(Client.SearchPosts("channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 { + if result := Client.Must(Client.SearchPosts("channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 2 { t.Fatalf("wrong number of posts returned %v", len(result.Order)) } @@ -567,38 +471,29 @@ func TestSearchPostsInChannel(t *testing.T) { } func TestSearchPostsFromUser(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel + team := th.BasicTeam + user1 := th.BasicUser + user2 := th.BasicUser2 + channel2 := th.CreateChannel(Client, team) + Client.Must(Client.AddChannelMember(channel1.Id, th.BasicUser2.Id)) + Client.Must(Client.AddChannelMember(channel2.Id, th.BasicUser2.Id)) + user3 := th.CreateUser(Client) + LinkUserToTeam(user3, team) + Client.Must(Client.AddChannelMember(channel1.Id, user3.Id)) + Client.Must(Client.AddChannelMember(channel2.Id, user3.Id)) post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") - Client.Must(Client.JoinChannel(channel1.Id)) - Client.Must(Client.JoinChannel(channel2.Id)) + th.LoginBasic2() post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"} post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) - if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 { + if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 2 { t.Fatalf("wrong number of posts returned %v", len(result.Order)) } @@ -617,13 +512,7 @@ func TestSearchPostsFromUser(t *testing.T) { t.Fatalf("wrong number of posts returned %v", len(result.Order)) } - user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - - Client.LoginByEmail(team.Name, user3.Email, "pwd") - Client.Must(Client.JoinChannel(channel1.Id)) - Client.Must(Client.JoinChannel(channel2.Id)) + Client.LoginByEmail(team.Name, user3.Email, user3.Password) // wait for the join/leave messages to be created for user3 since they're done asynchronously time.Sleep(100 * time.Millisecond) @@ -649,19 +538,9 @@ func TestSearchPostsFromUser(t *testing.T) { } func TestGetPostsCache(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel time.Sleep(10 * time.Millisecond) post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -698,23 +577,10 @@ func TestGetPostsCache(t *testing.T) { } func TestDeletePosts(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) - - userAdmin := &model.User{TeamId: team.Id, Email: team.Email, Nickname: "Corey Hulen", Password: "pwd"} - userAdmin = Client.Must(Client.CreateUser(userAdmin, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(userAdmin.Id)) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel + UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam) time.Sleep(10 * time.Millisecond) post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -745,7 +611,7 @@ func TestDeletePosts(t *testing.T) { r2 := Client.Must(Client.GetPosts(channel1.Id, 0, 10, "")).Data.(*model.PostList) - if len(r2.Posts) != 4 { + if len(r2.Posts) != 5 { t.Fatal("should have returned 4 items") } @@ -753,27 +619,17 @@ func TestDeletePosts(t *testing.T) { post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post) - Client.LoginByEmail(team.Name, userAdmin.Email, "pwd") + th.LoginBasic2() Client.Must(Client.DeletePost(channel1.Id, post4.Id)) } func TestEmailMention(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) - - user1 := &model.User{TeamId: team.Id, Email: "success+test@simulator.amazonses.com", Nickname: "Bob Bobby", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - post1 := &model.Post{ChannelId: channel1.Id, Message: "bob"} + post1 := &model.Post{ChannelId: channel1.Id, Message: th.BasicUser.Username} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) // No easy way to verify the email was sent, but this will at least cause the server to throw errors if the code is broken @@ -781,19 +637,9 @@ func TestEmailMention(t *testing.T) { } func TestFuzzyPosts(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") - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel filenames := []string{"junk"} @@ -808,21 +654,13 @@ func TestFuzzyPosts(t *testing.T) { } func TestMakeDirectChannelVisible(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user1 := th.BasicUser + user2 := th.BasicUser2 - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - // user2 will be created with prefs created to show user1 in the sidebar so set that to false to get rid of it - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() preferences := &model.Preferences{ { @@ -834,9 +672,11 @@ func TestMakeDirectChannelVisible(t *testing.T) { } Client.Must(Client.SetPreferences(preferences)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.Must(Client.Logout()) + th.LoginBasic() + th.BasicClient.SetTeamId(team.Id) - channel := Client.Must(Client.CreateDirectChannel(map[string]string{"user_id": user2.Id})).Data.(*model.Channel) + channel := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel) makeDirectChannelVisible(team.Id, channel.Id) @@ -845,38 +685,17 @@ func TestMakeDirectChannelVisible(t *testing.T) { } else if pref := result.Data.(*model.Preference); pref.Value != "true" { t.Fatal("Failed to set direct channel to be visible for user1") } - - Client.LoginByEmail(team.Name, user2.Email, "pwd") - - if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user1.Id); err != nil { - t.Fatal("Errored trying to set direct channel to be visible for user2") - } else if pref := result.Data.(*model.Preference); pref.Value != "true" { - t.Fatal("Failed to set direct channel to be visible for user2") - } } func TestGetOutOfChannelMentions(t *testing.T) { - Setup() - - team1 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN} - team1 = Client.Must(Client.CreateTeam(team1)).Data.(*model.Team) - - user1 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user1"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user2"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - user3 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user3"} - user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - - Client.Must(Client.LoginByEmail(team1.Name, user1.Email, "pwd")) - - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team1.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel + team1 := th.BasicTeam + user1 := th.BasicUser + user2 := th.BasicUser2 + user3 := th.CreateUser(Client) + LinkUserToTeam(user3, team1) var allProfiles map[string]*model.User if result := <-Srv.Store.User().GetProfiles(team1.Id); result.Err != nil { @@ -893,39 +712,37 @@ func TestGetOutOfChannelMentions(t *testing.T) { } // test a post that doesn't @mention anybody - post1 := &model.Post{ChannelId: channel1.Id, Message: "user1 user2 user3"} + post1 := &model.Post{ChannelId: channel1.Id, Message: fmt.Sprintf("%v %v %v", user1.Username, user2.Username, user3.Username)} if mentioned := getOutOfChannelMentions(post1, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when no users were mentioned", mentioned) } // test a post that @mentions someone in the channel - post2 := &model.Post{ChannelId: channel1.Id, Message: "@user1 is user1"} + post2 := &model.Post{ChannelId: channel1.Id, Message: fmt.Sprintf("@%v is %v", user1.Username, user1.Username)} if mentioned := getOutOfChannelMentions(post2, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when only users in the channel were mentioned", mentioned) } // test a post that @mentions someone not in the channel - post3 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user3 aren't in the channel"} + post3 := &model.Post{ChannelId: channel1.Id, Message: fmt.Sprintf("@%v and @%v aren't in the channel", user2.Username, user3.Username)} if mentioned := getOutOfChannelMentions(post3, allProfiles, members); len(mentioned) != 2 || (mentioned[0].Id != user2.Id && mentioned[0].Id != user3.Id) || (mentioned[1].Id != user2.Id && mentioned[1].Id != user3.Id) { t.Fatalf("getOutOfChannelMentions returned %v when two users outside the channel were mentioned", mentioned) } // test a post that @mentions someone not in the channel as well as someone in the channel - post4 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user1 might be in the channel"} + post4 := &model.Post{ChannelId: channel1.Id, Message: fmt.Sprintf("@%v and @%v might be in the channel", user2.Username, user1.Username)} if mentioned := getOutOfChannelMentions(post4, allProfiles, members); len(mentioned) != 1 || mentioned[0].Id != user2.Id { t.Fatalf("getOutOfChannelMentions returned %v when someone in the channel and someone outside the channel were mentioned", mentioned) } Client.Must(Client.Logout()) - team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN} - team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - - user4 := &model.User{TeamId: team2.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user4"} - user4 = Client.Must(Client.CreateUser(user4, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user4.Id)) + team2 := th.CreateTeam(Client) + user4 := th.CreateUser(Client) + LinkUserToTeam(user4, team2) - Client.Must(Client.LoginByEmail(team2.Name, user4.Email, "pwd")) + Client.Must(Client.LoginByEmail(team2.Name, user4.Email, user4.Password)) + Client.SetTeamId(team2.Id) channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) @@ -943,7 +760,7 @@ func TestGetOutOfChannelMentions(t *testing.T) { } // test a post that @mentions someone on a different team - post5 := &model.Post{ChannelId: channel2.Id, Message: "@user2 and @user3 might be in the channel"} + post5 := &model.Post{ChannelId: channel2.Id, Message: fmt.Sprintf("@%v and @%v might be in the channel", user2.Username, user3.Username)} if mentioned := getOutOfChannelMentions(post5, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when two users on a different team were mentioned", mentioned) } diff --git a/api/preference.go b/api/preference.go index 9550b6c92..d9ddb1a21 100644 --- a/api/preference.go +++ b/api/preference.go @@ -11,14 +11,13 @@ import ( "net/http" ) -func InitPreference(r *mux.Router) { +func InitPreference() { l4g.Debug(utils.T("api.preference.init.debug")) - sr := r.PathPrefix("/preferences").Subrouter() - sr.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET") - sr.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST") - sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET") - sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET") + BaseRoutes.Preferences.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET") + BaseRoutes.Preferences.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST") + BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET") + BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET") } func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) { @@ -44,7 +43,7 @@ func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewLocAppError("savePreferences", "api.preference.save_preferences.set.app_error", nil, c.T("api.preference.save_preferences.set_details.app_error", map[string]interface{}{"SessionUserId": c.Session.UserId, "PreferenceUserId": preference.UserId})) - c.Err.StatusCode = http.StatusUnauthorized + c.Err.StatusCode = http.StatusForbidden return } } diff --git a/api/preference_test.go b/api/preference_test.go index 82bee6315..082f02527 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -5,23 +5,13 @@ package api import ( "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "testing" ) func TestGetAllPreferences(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + th := Setup().InitBasic() + Client := th.BasicClient + user1 := th.BasicUser category := model.NewId() @@ -43,7 +33,6 @@ func TestGetAllPreferences(t *testing.T) { }, } - Client.LoginByEmail(team.Name, user1.Email, "pwd") Client.Must(Client.SetPreferences(&preferences1)) if result, err := Client.GetAllPreferences(); err != nil { @@ -52,27 +41,19 @@ func TestGetAllPreferences(t *testing.T) { t.Fatal("received the wrong number of preferences") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() - // note that user2 will automatically have a preference set for them to show user1 for direct messages if result, err := Client.GetAllPreferences(); err != nil { t.Fatal(err) - } else if data := result.Data.(model.Preferences); len(data) != 2 { + } else if data := result.Data.(model.Preferences); len(data) == 0 { t.Fatal("received the wrong number of preferences") } } func TestSetPreferences(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") + th := Setup().InitBasic() + Client := th.BasicClient + user1 := th.BasicUser // save 10 preferences var preferences model.Preferences @@ -98,12 +79,7 @@ func TestSetPreferences(t *testing.T) { t.Fatal(err) } - // not able to update as a different user - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() if _, err := Client.SetPreferences(&preferences); err == nil { t.Fatal("shouldn't have been able to update another user's preferences") @@ -111,18 +87,9 @@ func TestSetPreferences(t *testing.T) { } func TestGetPreferenceCategory(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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + th := Setup().InitBasic() + Client := th.BasicClient + user1 := th.BasicUser category := model.NewId() @@ -144,11 +111,8 @@ func TestGetPreferenceCategory(t *testing.T) { }, } - Client.LoginByEmail(team.Name, user1.Email, "pwd") Client.Must(Client.SetPreferences(&preferences1)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") - if result, err := Client.GetPreferenceCategory(category); err != nil { t.Fatal(err) } else if data := result.Data.(model.Preferences); len(data) != 2 { @@ -157,7 +121,7 @@ func TestGetPreferenceCategory(t *testing.T) { t.Fatal("received incorrect preferences") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() if result, err := Client.GetPreferenceCategory(category); err != nil { t.Fatal(err) @@ -167,16 +131,9 @@ func TestGetPreferenceCategory(t *testing.T) { } func TestGetPreference(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") + th := Setup().InitBasic() + Client := th.BasicClient + user := th.BasicUser preferences := model.Preferences{ { diff --git a/api/server.go b/api/server.go index b84066cbe..6e0ca49f0 100644 --- a/api/server.go +++ b/api/server.go @@ -6,6 +6,7 @@ package api import ( l4g "github.com/alecthomas/log4go" "github.com/braintree/manners" + "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -73,11 +74,10 @@ func StartServer() { } go func() { - err := manners.ListenAndServe(utils.Cfg.ServiceSettings.ListenAddress, handler) + err := manners.ListenAndServe(utils.Cfg.ServiceSettings.ListenAddress, handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler)) if err != nil { l4g.Critical(utils.T("api.server.start_server.starting.critical"), err) time.Sleep(time.Second) - panic(utils.T("api.server.start_server.starting.panic") + err.Error()) } }() } diff --git a/api/sharding.go b/api/sharding.go deleted file mode 100644 index 2a5db408c..000000000 --- a/api/sharding.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -/* -func createSubDomain(subDomain string, target string) { - - if utils.Cfg.AWSSettings.Route53AccessKeyId == "" { - return - } - - creds := aws.Creds(utils.Cfg.AWSSettings.Route53AccessKeyId, utils.Cfg.AWSSettings.Route53SecretAccessKey, "") - r53 := route53.New(aws.DefaultConfig.Merge(&aws.Config{Credentials: creds, Region: utils.Cfg.AWSSettings.Route53Region})) - - rr := route53.ResourceRecord{ - Value: aws.String(target), - } - - rrs := make([]*route53.ResourceRecord, 1) - rrs[0] = &rr - - change := route53.Change{ - Action: aws.String("CREATE"), - ResourceRecordSet: &route53.ResourceRecordSet{ - Name: aws.String(fmt.Sprintf("%v.%v", subDomain, utils.Cfg.ServiceSettings.Domain)), - TTL: aws.Long(300), - Type: aws.String("CNAME"), - ResourceRecords: rrs, - }, - } - - changes := make([]*route53.Change, 1) - changes[0] = &change - - r53req := &route53.ChangeResourceRecordSetsInput{ - HostedZoneID: aws.String(utils.Cfg.AWSSettings.Route53ZoneId), - ChangeBatch: &route53.ChangeBatch{ - Changes: changes, - }, - } - - if _, err := r53.ChangeResourceRecordSets(r53req); err != nil { - l4g.Error("erro in createSubDomain domain=%v err=%v", subDomain, err) - return - } -} - -func doesSubDomainExist(subDomain string) bool { - - // if it's configured for testing then skip this step - if utils.Cfg.AWSSettings.Route53AccessKeyId == "" { - return false - } - - creds := aws.Creds(utils.Cfg.AWSSettings.Route53AccessKeyId, utils.Cfg.AWSSettings.Route53SecretAccessKey, "") - r53 := route53.New(aws.DefaultConfig.Merge(&aws.Config{Credentials: creds, Region: utils.Cfg.AWSSettings.Route53Region})) - - r53req := &route53.ListResourceRecordSetsInput{ - HostedZoneID: aws.String(utils.Cfg.AWSSettings.Route53ZoneId), - MaxItems: aws.String("1"), - StartRecordName: aws.String(fmt.Sprintf("%v.%v.", subDomain, utils.Cfg.ServiceSettings.Domain)), - } - - if result, err := r53.ListResourceRecordSets(r53req); err != nil { - l4g.Error("error in doesSubDomainExist domain=%v err=%v", subDomain, err) - return true - } else { - - for _, v := range result.ResourceRecordSets { - if v.Name != nil && *v.Name == fmt.Sprintf("%v.%v.", subDomain, utils.Cfg.ServiceSettings.Domain) { - return true - } - } - } - - return false -} -*/ diff --git a/api/slackimport.go b/api/slackimport.go index 5ca209c5c..4319fe409 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -112,7 +112,6 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map password := model.NewId() newUser := model.User{ - TeamId: teamId, Username: sUser.Username, FirstName: firstName, LastName: lastName, @@ -120,7 +119,7 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map Password: password, } - if mUser := ImportUser(&newUser); mUser != nil { + if mUser := ImportUser(teamId, &newUser); mUser != nil { addedUsers[sUser.Id] = mUser log.WriteString(utils.T("api.slackimport.slack_add_users.email_pwd", map[string]interface{}{"Email": newUser.Email, "Password": password})) } else { diff --git a/api/team.go b/api/team.go index 255982522..eefdc3d85 100644 --- a/api/team.go +++ b/api/team.go @@ -6,38 +6,43 @@ package api import ( "bytes" "fmt" - l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" - "github.com/mattermost/platform/einterfaces" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" - "github.com/mattermost/platform/utils" "html/template" "net/http" "net/url" "strconv" "strings" "time" + + l4g "github.com/alecthomas/log4go" + "github.com/gorilla/mux" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) -func InitTeam(r *mux.Router) { +func InitTeam() { l4g.Debug(utils.T("api.team.init.debug")) - sr := r.PathPrefix("/teams").Subrouter() - sr.Handle("/create", ApiAppHandler(createTeam)).Methods("POST") - sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") - sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST") - sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") - sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") - sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET") - sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).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") + BaseRoutes.Teams.Handle("/create", ApiAppHandler(createTeam)).Methods("POST") + BaseRoutes.Teams.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") + BaseRoutes.Teams.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") + BaseRoutes.Teams.Handle("/all", ApiAppHandler(getAll)).Methods("GET") + BaseRoutes.Teams.Handle("/all_team_listings", ApiUserRequired(GetAllTeamListings)).Methods("GET") + BaseRoutes.Teams.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") + BaseRoutes.Teams.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") + BaseRoutes.Teams.Handle("/members/{id:[A-Za-z0-9]+}", ApiUserRequired(getMembers)).Methods("GET") + + BaseRoutes.NeedTeam.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") + + BaseRoutes.NeedTeam.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") + + BaseRoutes.NeedTeam.Handle("/add_user_to_team", ApiUserRequired(addUserToTeam)).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") + BaseRoutes.Teams.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") + BaseRoutes.Teams.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET") + BaseRoutes.Teams.Handle("/add_user_to_team_from_invite", ApiUserRequired(addUserToTeamFromInvite)).Methods("POST") } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { @@ -92,67 +97,6 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } -func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Enable { - c.SetInvalidParam("createTeamFromSSO", "service") - return - } - - team := model.TeamFromJson(r.Body) - - if team == nil { - c.SetInvalidParam("createTeamFromSSO", "team") - return - } - - if !isTeamCreationAllowed(c, team.Email) { - return - } - - team.PreSave() - - team.Name = model.CleanTeamName(team.Name) - - if err := team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil { - c.Err = err - return - } - - team.Id = "" - - found := true - count := 0 - for found { - if found = FindTeamByName(c, team.Name, "true"); c.Err != nil { - return - } else if found { - team.Name = team.Name + strconv.Itoa(count) - count += 1 - } - } - - if result := <-Srv.Store.Team().Save(team); result.Err != nil { - c.Err = result.Err - return - } else { - rteam := result.Data.(*model.Team) - - if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { - c.Err = nil - return - } - - data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name} - w.Write([]byte(model.MapToJson(data))) - - } - -} - func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail { c.Err = model.NewLocAppError("createTeamFromSignup", "api.team.create_team_from_signup.email_disabled.app_error", nil, "") @@ -186,13 +130,11 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { password := teamSignup.User.Password teamSignup.User.PreSave() - teamSignup.User.TeamId = model.NewId() if err := teamSignup.User.IsValid(); err != nil { c.Err = err return } teamSignup.User.Id = "" - teamSignup.User.TeamId = "" teamSignup.User.Password = password if !model.ComparePassword(teamSignup.Hash, fmt.Sprintf("%v:%v", teamSignup.Data, utils.Cfg.EmailSettings.InviteSalt)) { @@ -206,10 +148,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { return } - found := FindTeamByName(c, teamSignup.Team.Name, "true") - if c.Err != nil { - return - } + found := FindTeamByName(teamSignup.Team.Name) if found { c.Err = model.NewLocAppError("createTeamFromSignup", "api.team.create_team_from_signup.unavailable.app_error", nil, "d="+teamSignup.Team.Name) @@ -227,15 +166,16 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamSignup.User.TeamId = rteam.Id teamSignup.User.EmailVerified = true - ruser, err := CreateUser(rteam, &teamSignup.User) + ruser, err := CreateUser(&teamSignup.User) if err != nil { c.Err = err return } + JoinUserToTeam(rteam, ruser) + InviteMembers(c, rteam, ruser, teamSignup.Invites) teamSignup.Team = *rteam @@ -245,85 +185,38 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { } } -func createTeamWithLdap(c *Context, w http.ResponseWriter, r *http.Request) { - ldap := einterfaces.GetLdapInterface() - if ldap == nil { - c.Err = model.NewLocAppError("createTeamWithLdap", "ent.ldap.do_login.licence_disable.app_error", nil, "") - return - } - - teamSignup := model.TeamSignupFromJson(r.Body) +func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { + team := model.TeamFromJson(r.Body) - if teamSignup == nil { - c.SetInvalidParam("createTeam", "teamSignup") + if team == nil { + c.SetInvalidParam("createTeam", "team") return } - teamSignup.Team.PreSave() - - if err := teamSignup.Team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil { - c.Err = err - return - } + var user *model.User + if len(c.Session.UserId) > 0 { + uchan := Srv.Store.User().Get(c.Session.UserId) - if !isTeamCreationAllowed(c, teamSignup.Team.Email) { - return + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + team.Email = user.Email + } } - teamSignup.Team.Id = "" - - found := FindTeamByName(c, teamSignup.Team.Name, "true") + rteam := CreateTeam(c, team) if c.Err != nil { return } - if found { - c.Err = model.NewLocAppError("createTeamFromSignup", "api.team.create_team_from_signup.unavailable.app_error", nil, "d="+teamSignup.Team.Name) - return - } - - user, err := ldap.GetUser(teamSignup.User.Username) - if err != nil { - c.Err = err - return - } - - err = ldap.CheckPassword(teamSignup.User.Username, teamSignup.User.Password) - if err != nil { - c.Err = err - return - } - - if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil { - c.Err = result.Err - return - } else { - rteam := result.Data.(*model.Team) - - if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { - c.Err = nil - return - } - - user.TeamId = rteam.Id - ruser, err := CreateUser(rteam, user) + if user != nil { + err := JoinUserToTeam(team, user) if err != nil { c.Err = err return } - - teamSignup.Team = *rteam - teamSignup.User = *ruser - - w.Write([]byte(teamSignup.ToJson())) - } -} - -func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { - team := model.TeamFromJson(r.Body) - rteam := CreateTeam(c, team) - if c.Err != nil { - return } w.Write([]byte(rteam.ToJson())) @@ -360,6 +253,31 @@ func CreateTeam(c *Context, team *model.Team) *model.Team { } } +func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { + + tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id} + + channelRole := "" + if team.Email == user.Email { + tm.Roles = model.ROLE_TEAM_ADMIN + channelRole = model.CHANNEL_ROLE_ADMIN + } + + if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { + return tmr.Err + } + + // Soft error if there is an issue joining the default channels + if err := JoinDefaultChannels(team.Id, user, channelRole); err != nil { + l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err) + } + + RemoveAllSessionsForUserId(user.Id) + InvalidateCacheForUser(user.Id) + + return nil +} + func isTeamCreationAllowed(c *Context, email string) bool { email = strings.ToLower(email) @@ -389,6 +307,24 @@ func isTeamCreationAllowed(c *Context, email string) bool { return true } +func GetAllTeamListings(c *Context, w http.ResponseWriter, r *http.Request) { + if result := <-Srv.Store.Team().GetAllTeamListing(); result.Err != nil { + c.Err = result.Err + return + } else { + teams := result.Data.([]*model.Team) + m := make(map[string]*model.Team) + for _, v := range teams { + m[v.Id] = v + if !c.IsSystemAdmin() { + m[v.Id].Sanitize() + } + } + + w.Write([]byte(model.TeamMapToJson(m))) + } +} + func getAll(c *Context, w http.ResponseWriter, r *http.Request) { if result := <-Srv.Store.Team().GetAll(); result.Err != nil { c.Err = result.Err @@ -435,52 +371,54 @@ func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) { } } -func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - name := strings.ToLower(strings.TrimSpace(m["name"])) - all := strings.ToLower(strings.TrimSpace(m["all"])) +func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { + invites := model.InvitesFromJson(r.Body) + if len(invites.Invites) == 0 { + c.Err = model.NewLocAppError("Team.InviteMembers", "api.team.invite_members.no_one.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } - found := FindTeamByName(c, name, all) + tchan := Srv.Store.Team().Get(c.TeamId) + uchan := Srv.Store.User().Get(c.Session.UserId) - if c.Err != nil { + var team *model.Team + if result := <-tchan; result.Err != nil { + c.Err = result.Err return + } else { + team = result.Data.(*model.Team) } - if found { - w.Write([]byte("true")) + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return } else { - w.Write([]byte("false")) + user = result.Data.(*model.User) } -} - -func FindTeamByName(c *Context, name string, all string) bool { - if name == "" || len(name) > 64 { - c.SetInvalidParam("findTeamByName", "domain") - return false + ia := make([]string, len(invites.Invites)) + for _, invite := range invites.Invites { + ia = append(ia, invite["email"]) } - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { - return false - } else { - return true - } + InviteMembers(c, team, user, ia) - return false + w.Write([]byte(invites.ToJson())) } -func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { - invites := model.InvitesFromJson(r.Body) - if len(invites.Invites) == 0 { - c.Err = model.NewLocAppError("Team.InviteMembers", "api.team.invite_members.no_one.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest +func addUserToTeam(c *Context, w http.ResponseWriter, r *http.Request) { + params := model.MapFromJson(r.Body) + userId := params["user_id"] + + if len(userId) != 26 { + c.SetInvalidParam("addUserToTeam", "user_id") return } - tchan := Srv.Store.Team().Get(c.Session.TeamId) - uchan := Srv.Store.User().Get(c.Session.UserId) + tchan := Srv.Store.Team().Get(c.TeamId) + uchan := Srv.Store.User().Get(userId) var team *model.Team if result := <-tchan; result.Err != nil { @@ -498,23 +436,116 @@ func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - var invNum int64 = 0 - for i, invite := range invites.Invites { - if result := <-Srv.Store.User().GetByEmail(c.Session.TeamId, invite["email"]); result.Err == nil || result.Err.Id != store.MISSING_ACCOUNT_ERROR { - invNum = int64(i) - c.Err = model.NewLocAppError("invite_members", "api.team.invite_members.already.app_error", nil, strconv.FormatInt(invNum, 10)) + if !c.IsTeamAdmin() { + c.Err = model.NewLocAppError("addUserToTeam", "api.team.update_team.permissions.app_error", nil, "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + err := JoinUserToTeam(team, user) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.MapToJson(params))) +} + +func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) { + + params := model.MapFromJson(r.Body) + hash := params["hash"] + data := params["data"] + inviteId := params["invite_id"] + + teamId := "" + var team *model.Team + + if len(hash) > 0 { + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + c.Err = model.NewLocAppError("addUserToTeamFromInvite", "api.user.create_user.signup_link_invalid.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("addUserToTeamFromInvite", "api.user.create_user.signup_link_expired.app_error", nil, "") + return + } + + teamId = props["id"] + + // try to load the team to make sure it exists + if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + } + + if len(inviteId) > 0 { + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + teamId = team.Id + } + } + + if len(teamId) == 0 { + c.Err = model.NewLocAppError("addUserToTeamFromInvite", "api.user.create_user.signup_link_invalid.app_error", nil, "") + return + } + + uchan := Srv.Store.User().Get(c.Session.UserId) + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + tm := c.Session.GetTeamByTeamId(teamId) + + if tm == nil { + err := JoinUserToTeam(team, user) + if err != nil { + c.Err = err return } } - ia := make([]string, len(invites.Invites)) - for _, invite := range invites.Invites { - ia = append(ia, invite["email"]) + team.Sanitize() + + w.Write([]byte(team.ToJson())) +} + +func FindTeamByName(name string) bool { + if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { + return false + } else { + return true } +} - InviteMembers(c, team, user, ia) +func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) { - w.Write([]byte(invites.ToJson())) + m := model.MapFromJson(r.Body) + name := strings.ToLower(strings.TrimSpace(m["name"])) + + found := FindTeamByName(name) + + if found { + w.Write([]byte("true")) + } else { + w.Write([]byte("false")) + } } func InviteMembers(c *Context, team *model.Team, user *model.User, invites []string) { @@ -573,7 +604,7 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - team.Id = c.Session.TeamId + team.Id = c.TeamId if !c.IsTeamAdmin() { c.Err = model.NewLocAppError("updateTeam", "api.team.update_team.permissions.app_error", nil, "userId="+c.Session.UserId) @@ -592,7 +623,6 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { oldTeam.DisplayName = team.DisplayName oldTeam.InviteId = team.InviteId oldTeam.AllowOpenInvite = team.AllowOpenInvite - oldTeam.AllowTeamListing = team.AllowTeamListing oldTeam.CompanyName = team.CompanyName oldTeam.AllowedDomains = team.AllowedDomains //oldTeam.Type = team.Type @@ -617,16 +647,11 @@ func PermanentDeleteTeam(c *Context, team *model.Team) *model.AppError { return result.Err } - if result := <-Srv.Store.User().GetForExport(team.Id); result.Err != nil { + if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil { return result.Err - } else { - users := result.Data.([]*model.User) - for _, user := range users { - PermanentDeleteUser(c, user) - } } - if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil { + if result := <-Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil { return result.Err } @@ -642,11 +667,11 @@ func PermanentDeleteTeam(c *Context, team *model.Team) *model.AppError { func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if len(c.Session.TeamId) == 0 { + if len(c.TeamId) == 0 { return } - if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.Team).Etag(), w, r) { @@ -659,7 +684,7 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { } func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasPermissionsToTeam(c.Session.TeamId, "import") || !c.IsTeamAdmin() { + if !c.HasPermissionsToTeam(c.TeamId, "import") || !c.IsTeamAdmin() { c.Err = model.NewLocAppError("importTeam", "api.team.import_team.admin.app_error", nil, "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return @@ -714,7 +739,7 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { switch importFrom { case "slack": var err *model.AppError - if err, log = SlackImport(fileData, fileSize, c.Session.TeamId); err != nil { + if err, log = SlackImport(fileData, fileSize, c.TeamId); err != nil { c.Err = err c.Err.StatusCode = http.StatusBadRequest } @@ -726,7 +751,7 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { } func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin() { + if !c.HasPermissionsToTeam(c.TeamId, "export") || !c.IsTeamAdmin() { c.Err = model.NewLocAppError("exportTeam", "api.team.export_team.admin.app_error", nil, "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return @@ -765,3 +790,23 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getMembers(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + if c.Session.GetTeamByTeamId(id) == nil { + if !c.HasSystemAdminPermissions("getMembers") { + return + } + } + + if result := <-Srv.Store.Team().GetMembers(id); result.Err != nil { + c.Err = result.Err + return + } else { + members := result.Data.([]*model.TeamMember) + w.Write([]byte(model.TeamMembersToJson(members))) + return + } +} diff --git a/api/team_test.go b/api/team_test.go index bbbc8385d..161c7e620 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -13,7 +13,9 @@ import ( ) func TestSignupTeam(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient _, err := Client.SignupTeam("test@nowhere.com", "name") if err != nil { @@ -22,7 +24,9 @@ func TestSignupTeam(t *testing.T) { } func TestCreateFromSignupTeam(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient props := make(map[string]string) props["email"] = strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com" @@ -47,6 +51,8 @@ func TestCreateFromSignupTeam(t *testing.T) { } ruser := rts.Data.(*model.TeamSignup).User + rteam := rts.Data.(*model.TeamSignup).Team + Client.SetTeamId(rteam.Id) if result, err := Client.LoginById(ruser.Id, user.Password); err != nil { t.Fatal(err) @@ -69,7 +75,9 @@ func TestCreateFromSignupTeam(t *testing.T) { } func TestCreateTeam(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, err := Client.CreateTeam(&team) @@ -77,11 +85,13 @@ func TestCreateTeam(t *testing.T) { t.Fatal(err) } - user := &model.User{TeamId: rteam.Data.(*model.Team).Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(user.Id)) Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(rteam.Data.(*model.Team).Id) c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) if len(c1.Channels) != 2 { @@ -108,26 +118,176 @@ func TestCreateTeam(t *testing.T) { } } +func TestAddUserToTeam(t *testing.T) { + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient + + props := make(map[string]string) + props["email"] = strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com" + props["name"] = "Test Company name" + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: props["email"], Type: model.TEAM_OPEN} + user := model.User{Email: props["email"], Nickname: "Corey Hulen", Password: "hello"} + + ts := model.TeamSignup{Team: team, User: user, Invites: []string{"success+test@simulator.amazonses.com"}, Data: data, Hash: hash} + + rts, err := Client.CreateTeamFromSignup(&ts) + if err != nil { + t.Fatal(err) + } + + if rts.Data.(*model.TeamSignup).Team.DisplayName != team.DisplayName { + t.Fatal("full name didn't match") + } + + ruser := rts.Data.(*model.TeamSignup).User + rteam := rts.Data.(*model.TeamSignup).Team + Client.SetTeamId(rteam.Id) + + if result, err := Client.LoginById(ruser.Id, user.Password); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Email != user.Email { + t.Fatal("email's didn't match") + } + } + + user2 := th.CreateUser(th.BasicClient) + if result, err := th.BasicClient.AddUserToTeam(user2.Id); err != nil { + t.Fatal(err) + } else { + rm := result.Data.(map[string]string) + if rm["user_id"] != user2.Id { + t.Fatal("email's didn't match") + } + } +} + +func TestAddUserToTeamFromInvite(t *testing.T) { + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient + + props := make(map[string]string) + props["email"] = strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com" + props["name"] = "Test Company name" + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: props["email"], Type: model.TEAM_OPEN} + user := model.User{Email: props["email"], Nickname: "Corey Hulen", Password: "hello"} + + ts := model.TeamSignup{Team: team, User: user, Invites: []string{"success+test@simulator.amazonses.com"}, Data: data, Hash: hash} + + rts, err := Client.CreateTeamFromSignup(&ts) + if err != nil { + t.Fatal(err) + } + + if rts.Data.(*model.TeamSignup).Team.DisplayName != team.DisplayName { + t.Fatal("full name didn't match") + } + + ruser := rts.Data.(*model.TeamSignup).User + rteam := rts.Data.(*model.TeamSignup).Team + Client.SetTeamId(rteam.Id) + + if result, err := Client.LoginById(ruser.Id, user.Password); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Email != user.Email { + t.Fatal("email's didn't match") + } + } + + user2 := th.CreateUser(th.BasicClient) + Client.Must(Client.Logout()) + Client.Must(Client.LoginByEmail("", user2.Email, user2.Password)) + + if result, err := th.BasicClient.AddUserToTeamFromInvite("", "", rteam.InviteId); err != nil { + t.Fatal(err) + } else { + rtm := result.Data.(*model.Team) + if rtm.Id != rteam.Id { + t.Fatal() + } + } +} + func TestGetAllTeams(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true} + 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 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) + + if r1, err := Client.GetAllTeams(); err != nil { + t.Fatal(err) + } else { + teams := r1.Data.(map[string]*model.Team) + if teams[team.Id].Name != team.Name { + t.Fatal() + } + if teams[team.Id].Email != "" { + t.Fatal("Non admin users shoudn't get full listings") + } + } + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing - defer func() { - *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks - }() - *utils.Cfg.TeamSettings.EnableTeamListing = true + Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeams(); err != nil { t.Fatal(err) + } else { + teams := r1.Data.(map[string]*model.Team) + if teams[team.Id].Name != team.Name { + t.Fatal() + } + if teams[team.Id].Email != team.Email { + t.Fatal() + } + } +} + +func TestGetAllTeamListings(t *testing.T) { + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowOpenInvite: true} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) + + if r1, err := Client.GetAllTeamListings(); err != nil { + t.Fatal(err) } else { teams := r1.Data.(map[string]*model.Team) if teams[team.Id].Name != team.Name { @@ -144,6 +304,7 @@ func TestGetAllTeams(t *testing.T) { UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeams(); err != nil { t.Fatal(err) @@ -159,16 +320,20 @@ func TestGetAllTeams(t *testing.T) { } func TestTeamPermDelete(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient 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) - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + LinkUserToTeam(user1, team) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -198,19 +363,23 @@ func TestTeamPermDelete(t *testing.T) { } func TestInviteMembers(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient 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 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) invite := make(map[string]string) - invite["email"] = model.NewId() + "success+test@simulator.amazonses.com" + invite["email"] = "success+" + model.NewId() + "@simulator.amazonses.com" invite["first_name"] = "Test" invite["last_name"] = "Guy" invites := &model.Invites{Invites: []map[string]string{invite}} @@ -227,20 +396,25 @@ func TestInviteMembers(t *testing.T) { } func TestUpdateTeamDisplayName(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient - 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: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_OPEN} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: team.Email, Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.SetTeamId(team.Id) vteam := &model.Team{DisplayName: team.DisplayName, Name: team.Name, Email: team.Email, Type: team.Type} vteam.DisplayName = "NewName" @@ -262,6 +436,9 @@ func TestUpdateTeamDisplayName(t *testing.T) { } func TestFuzzyTeamCreate(t *testing.T) { + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient for i := 0; i < len(utils.FUZZY_STRINGS_NAMES) || i < len(utils.FUZZY_STRINGS_EMAILS); i++ { testDisplayName := "Name" @@ -284,19 +461,24 @@ func TestFuzzyTeamCreate(t *testing.T) { } func TestGetMyTeam(t *testing.T) { - Setup() + th := Setup().InitBasic() + th.BasicClient.Logout() + Client := th.BasicClient - team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - rteam, _ := Client.CreateTeam(&team) + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(team) + team = rteam.Data.(*model.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"} + user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.SetTeamId(team.Id) if result, err := Client.GetMyTeam(""); err != nil { - t.Fatal("Failed to get user") + t.Fatal(err) } else { if result.Data.(*model.Team).DisplayName != team.DisplayName { t.Fatal("team names did not match") @@ -309,3 +491,14 @@ func TestGetMyTeam(t *testing.T) { } } } + +func TestGetTeamMembers(t *testing.T) { + th := Setup().InitBasic() + + if result, err := th.BasicClient.GetTeamMembers(th.BasicTeam.Id); err != nil { + t.Fatal(err) + } else { + members := result.Data.([]*model.TeamMember) + t.Log(members) + } +} diff --git a/api/user.go b/api/user.go index 08d096c51..16ba45dc4 100644 --- a/api/user.go +++ b/api/user.go @@ -5,8 +5,6 @@ package api import ( "bytes" - "crypto/tls" - b64 "encoding/base64" "fmt" l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" @@ -34,45 +32,44 @@ import ( "time" ) -func InitUser(r *mux.Router) { +func InitUser() { l4g.Debug(utils.T("api.user.init.debug")) - sr := r.PathPrefix("/users").Subrouter() - sr.Handle("/create", ApiAppHandler(createUser)).Methods("POST") - sr.Handle("/update", ApiUserRequired(updateUser)).Methods("POST") - sr.Handle("/update_roles", ApiUserRequired(updateRoles)).Methods("POST") - sr.Handle("/update_active", ApiUserRequired(updateActive)).Methods("POST") - sr.Handle("/update_notify", ApiUserRequired(updateUserNotify)).Methods("POST") - sr.Handle("/newpassword", ApiUserRequired(updatePassword)).Methods("POST") - sr.Handle("/send_password_reset", ApiAppHandler(sendPasswordReset)).Methods("POST") - sr.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST") - sr.Handle("/login", ApiAppHandler(login)).Methods("POST") - sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST") - sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") - sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") - sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") - sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") - sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") - sr.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") - sr.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET") - sr.Handle("/update_mfa", ApiUserRequired(updateMfa)).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") - sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}/image", ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET") - - sr.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST") - sr.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).Methods("POST") - sr.Handle("/claim/email_to_ldap", ApiAppHandler(emailToLdap)).Methods("POST") - sr.Handle("/claim/ldap_to_email", ApiAppHandler(ldapToEmail)).Methods("POST") + BaseRoutes.Users.Handle("/create", ApiAppHandler(createUser)).Methods("POST") + BaseRoutes.Users.Handle("/update", ApiUserRequired(updateUser)).Methods("POST") + BaseRoutes.Users.Handle("/update_roles", ApiUserRequired(updateRoles)).Methods("POST") + BaseRoutes.Users.Handle("/update_active", ApiUserRequired(updateActive)).Methods("POST") + BaseRoutes.Users.Handle("/update_notify", ApiUserRequired(updateUserNotify)).Methods("POST") + BaseRoutes.Users.Handle("/newpassword", ApiUserRequired(updatePassword)).Methods("POST") + BaseRoutes.Users.Handle("/send_password_reset", ApiAppHandler(sendPasswordReset)).Methods("POST") + BaseRoutes.Users.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST") + BaseRoutes.Users.Handle("/login", ApiAppHandler(login)).Methods("POST") + BaseRoutes.Users.Handle("/logout", ApiAppHandler(logout)).Methods("POST") + BaseRoutes.Users.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") + BaseRoutes.Users.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") + BaseRoutes.Users.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") + BaseRoutes.Users.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + BaseRoutes.Users.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") + BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") + BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET") + BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") + BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET") + BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") + + BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") + BaseRoutes.Users.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET") + BaseRoutes.Users.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST") + + BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST") + BaseRoutes.Users.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).Methods("POST") + BaseRoutes.Users.Handle("/claim/email_to_ldap", ApiAppHandler(emailToLdap)).Methods("POST") + BaseRoutes.Users.Handle("/claim/ldap_to_email", ApiAppHandler(ldapToEmail)).Methods("POST") + + BaseRoutes.NeedUser.Handle("/get", ApiUserRequired(getUser)).Methods("GET") + BaseRoutes.NeedUser.Handle("/sessions", ApiUserRequired(getSessions)).Methods("GET") + BaseRoutes.NeedUser.Handle("/audits", ApiUserRequired(getAudits)).Methods("GET") + BaseRoutes.NeedUser.Handle("/image", ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -89,24 +86,13 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - // the user's username is checked to be valid when they are saved to the database - - user.EmailVerified = false - - var team *model.Team - - if result := <-Srv.Store.Team().Get(user.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - hash := r.URL.Query().Get("h") - + teamId := "" + var team *model.Team sendWelcomeEmail := true + user.EmailVerified = false - if IsVerifyHashRequired(user, team, hash) { + if len(hash) > 0 { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) @@ -121,9 +107,14 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - if user.TeamId != props["id"] { - c.Err = model.NewLocAppError("createUser", "api.user.create_user.team_name.app_error", nil, data) + teamId = props["id"] + + // try to load the team to make sure it exists + if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + c.Err = result.Err return + } else { + team = result.Data.(*model.Team) } user.Email = props["email"] @@ -131,8 +122,33 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { sendWelcomeEmail = false } - if user.IsSSOUser() { - user.EmailVerified = true + inviteId := r.URL.Query().Get("iid") + if len(inviteId) > 0 { + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + teamId = team.Id + } + } + + firstAccount := false + if sessionCache.Len() == 0 { + if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil { + c.Err = cr.Err + return + } else { + count := cr.Data.(int64) + if count <= 0 { + firstAccount = true + } + } + } + + if !firstAccount && !*utils.Cfg.TeamSettings.EnableOpenServer && len(teamId) == 0 { + c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email) + return } if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) { @@ -140,14 +156,24 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - ruser, err := CreateUser(team, user) + ruser, err := CreateUser(user) if err != nil { c.Err = err return } + if len(teamId) > 0 { + err := JoinUserToTeam(team, ruser) + if err != nil { + c.Err = err + return + } + + addDirectChannelsAndForget(team.Id, ruser) + } + if sendWelcomeEmail { - sendWelcomeEmailAndForget(c, ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified) + sendWelcomeEmailAndForget(c, ruser.Id, ruser.Email, c.GetSiteURL(), ruser.EmailVerified) } w.Write([]byte(ruser.ToJson())) @@ -196,26 +222,19 @@ func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool return shouldVerifyHash } -func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppError) { +func CreateUser(user *model.User) (*model.User, *model.AppError) { - channelRole := "" - if team.Email == user.Email { - user.Roles = model.ROLE_TEAM_ADMIN - channelRole = model.CHANNEL_ROLE_ADMIN - - // Below is a speical case where the first user in the entire - // system is granted the system_admin role instead of admin - if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil { - return nil, result.Err - } else { - count := result.Data.(int64) - if count <= 0 { - user.Roles = model.ROLE_SYSTEM_ADMIN - } - } + user.Roles = "" + // Below is a speical case where the first user in the entire + // system is granted the system_admin role instead of admin + if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil { + return nil, result.Err } else { - user.Roles = "" + count := result.Data.(int64) + if count <= 0 { + user.Roles = model.ROLE_SYSTEM_ADMIN + } } user.MakeNonNil() @@ -226,13 +245,6 @@ func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppErro } else { ruser := result.Data.(*model.User) - // Soft error if there is an issue joining the default channels - if err := JoinDefaultChannels(ruser, channelRole); err != nil { - l4g.Error(utils.T("api.user.create_user.joining.error"), ruser.Id, ruser.TeamId, err) - } - - addDirectChannelsAndForget(ruser) - if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { l4g.Error(utils.T("api.user.create_user.verified.error"), cresult.Err) @@ -247,15 +259,13 @@ func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppErro ruser.Sanitize(map[string]bool{}) // This message goes to every channel, so the channelId is irrelevant - message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER) - - PublishAndForget(message) + PublishAndForget(model.NewMessage("", "", ruser.Id, model.ACTION_NEW_USER)) return ruser, nil } } -func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, team *model.Team) *model.User { +func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, teamId string) *model.User { var user *model.User provider := einterfaces.GetOauthProvider(service) if provider == nil { @@ -270,49 +280,59 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service return nil } - suchan := Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) - euchan := Srv.Store.User().GetByEmail(team.Id, user.Email) + suchan := Srv.Store.User().GetByAuth(user.AuthData, service) + euchan := Srv.Store.User().GetByEmail(user.Email) - if team.Email == "" { - team.Email = user.Email - if result := <-Srv.Store.Team().Update(team); result.Err != nil { - c.Err = result.Err - return nil - } - } else { - found := true - count := 0 - for found { - if found = IsUsernameTaken(user.Username, team.Id); c.Err != nil { - return nil - } else if found { - user.Username = user.Username + strconv.Itoa(count) - count += 1 - } + var tchan store.StoreChannel + if len(teamId) != 0 { + tchan = Srv.Store.Team().Get(teamId) + } + + found := true + count := 0 + for found { + if found = IsUsernameTaken(user.Username); found { + user.Username = user.Username + strconv.Itoa(count) + count += 1 } } if result := <-suchan; result.Err == nil { - c.Err = model.NewLocAppError("signupCompleteOAuth", "api.user.create_oauth_user.already_used.app_error", - map[string]interface{}{"Service": service, "DisplayName": team.DisplayName}, "email="+user.Email) + c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", + map[string]interface{}{"Service": service}, "email="+user.Email) return nil } if result := <-euchan; result.Err == nil { - c.Err = model.NewLocAppError("signupCompleteOAuth", "api.user.create_oauth_user.already_attached.app_error", - map[string]interface{}{"Service": service, "DisplayName": team.DisplayName}, "email="+user.Email) + c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", + map[string]interface{}{"Service": service}, "email="+user.Email) return nil } - user.TeamId = team.Id user.EmailVerified = true - ruser, err := CreateUser(team, user) + ruser, err := CreateUser(user) if err != nil { c.Err = err return nil } + if tchan != nil { + if result := <-tchan; result.Err != nil { + c.Err = result.Err + return nil + } else { + team := result.Data.(*model.Team) + err = JoinUserToTeam(team, user) + if err != nil { + c.Err = err + return nil + } + + addDirectChannelsAndForget(team.Id, user) + } + } + Login(c, w, r, ruser, "") if c.Err != nil { return nil @@ -321,23 +341,23 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service return ruser } -func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { +func sendWelcomeEmailAndForget(c *Context, userId string, email string, siteURL string, verified bool) { go func() { subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": siteURL}) 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["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": siteURL}) bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") bodyPage.Props["Button"] = c.T("api.templates.welcome_body.button") bodyPage.Props["Info2"] = c.T("api.templates.welcome_body.info2") bodyPage.Props["Info3"] = c.T("api.templates.welcome_body.info3") - bodyPage.Props["TeamURL"] = teamURL + bodyPage.Props["TeamURL"] = siteURL if !verified { - link := fmt.Sprintf("%s/do_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&email=%s", siteURL, userId, model.HashPassword(userId), email) bodyPage.Props["VerifyUrl"] = link } @@ -347,11 +367,11 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN }() } -func addDirectChannelsAndForget(user *model.User) { +func addDirectChannelsAndForget(teamId string, user *model.User) { go func() { var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { - l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, user.TeamId, result.Err.Error()) + if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { + l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) return } else { profiles = result.Data.(map[string]*model.User) @@ -381,23 +401,23 @@ func addDirectChannelsAndForget(user *model.User) { } if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { - l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, user.TeamId, result.Err.Error()) + l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) } }() } -func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendVerifyEmailAndForget(c *Context, userId, userEmail, siteURL string) { go func() { - link := fmt.Sprintf("%s/do_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&email=%s", siteURL, userId, model.HashPassword(userId), userEmail) 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"]}) + map[string]interface{}{"TeamDisplayName": utils.ClientCfg["SiteName"], "SiteName": utils.ClientCfg["SiteName"]}) 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["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": utils.ClientCfg["SiteName"]}) bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") bodyPage.Props["VerifyUrl"] = link bodyPage.Props["Button"] = c.T("api.templates.verify_body.button") @@ -408,6 +428,54 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDispl }() } +func login(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + if len(props["password"]) == 0 { + c.Err = model.NewLocAppError("login", "api.user.login.blank_pwd.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + var user *model.User + if len(props["id"]) != 0 { + user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"]) + } else if len(props["email"]) != 0 { + user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"]) + } else if len(props["username"]) != 0 { + user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"]) + } else { + c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if c.Err != nil { + return + } + + if user != nil { + user.Sanitize(map[string]bool{}) + } else { + user = &model.User{} + } + w.Write([]byte(user.ToJson())) +} + +func doUserPasswordAuthenticationAndLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, password string, mfaToken string, deviceId string) bool { + c.LogAuditWithUserId(user.Id, "attempt") + if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil { + c.LogAuditWithUserId(user.Id, "fail") + c.Err = err + c.Err.StatusCode = http.StatusUnauthorized + return false + } else { + Login(c, w, r, user, deviceId) + c.LogAuditWithUserId(user.Id, "success") + return true + } +} + func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, mfaToken, deviceId string) *model.User { if result := <-Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err @@ -415,8 +483,13 @@ func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, passw } else { user := result.Data.(*model.User) - if authenticateUserPasswordAndToken(c, user, password, mfaToken) { - Login(c, w, r, user, deviceId) + if len(user.AuthData) != 0 { + c.Err = model.NewLocAppError("LoginById", "api.user.login_by_email.sign_in.app_error", + map[string]interface{}{"AuthService": user.AuthService}, "") + return nil + } + + if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { return user } } @@ -425,18 +498,9 @@ func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, passw } func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User { - var team *model.Team - - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusUnauthorized return nil } else { user := result.Data.(*model.User) @@ -447,8 +511,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam return nil } - if authenticateUserPasswordAndToken(c, user, password, mfaToken) { - Login(c, w, r, user, deviceId) + if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { return user } } @@ -457,18 +520,9 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam } func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User { - var team *model.Team - - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { + if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil { c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - if result := <-Srv.Store.User().GetByUsername(team.Id, username); result.Err != nil { - c.Err = result.Err - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusUnauthorized return nil } else { user := result.Data.(*model.User) @@ -479,8 +533,7 @@ func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, usernam return nil } - if authenticateUserPasswordAndToken(c, user, password, mfaToken) { - Login(c, w, r, user, deviceId) + if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { return user } } @@ -488,7 +541,7 @@ func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, usernam return nil } -func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, team *model.Team) *model.User { +func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User { buf := bytes.Buffer{} buf.ReadFrom(userData) @@ -509,9 +562,9 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } var user *model.User - if result := <-Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { - if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR && team.AllowOpenInvite { - return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), team) + if result := <-Srv.Store.User().GetByAuth(authData, service); result.Err != nil { + if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { + return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "") } c.Err = result.Err return nil @@ -522,81 +575,77 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } } -func authenticateUserPasswordAndToken(c *Context, user *model.User, password string, token string) bool { - return checkUserLoginAttempts(c, user) && checkUserMfa(c, user, token) && checkUserPassword(c, user, password) -} - -func checkUserLoginAttempts(c *Context, user *model.User) bool { - if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { - c.LogAuditWithUserId(user.Id, "fail") - c.Err = model.NewLocAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id) - c.Err.StatusCode = http.StatusForbidden - return false +func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.LdapSettings.Enable { + c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return } - return true -} - -func checkUserPassword(c *Context, user *model.User, password string) bool { - if !model.ComparePassword(user.Password, password) { - c.LogAuditWithUserId(user.Id, "fail") - c.Err = model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id) - c.Err.StatusCode = http.StatusForbidden - - if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil { - c.LogError(result.Err) - } + props := model.MapFromJson(r.Body) - return false - } else { - if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { - c.LogError(result.Err) - } + password := props["password"] + id := props["id"] + mfaToken := props["token"] - return true + if len(password) == 0 { + c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return } -} -func checkUserMfa(c *Context, user *model.User, token string) bool { - if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { - return true + if len(id) == 0 { + c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.need_id.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return } - mfaInterface := einterfaces.GetMfaInterface() - if mfaInterface == nil { - c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") + ldapInterface := einterfaces.GetLdapInterface() + if ldapInterface == nil { + c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.not_available.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented - return false + return } - if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { + user, err := ldapInterface.DoLogin(id, password) + if err != nil { + if user != nil { + c.LogAuditWithUserId(user.Id, "attempt") + c.LogAuditWithUserId(user.Id, "fail") + } else { + c.LogAudit("attempt") + c.LogAudit("fail") + } c.Err = err - return false - } else if !ok { - c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") - return false - } else { - return true + c.Err.StatusCode = http.StatusUnauthorized + return } -} - -// User MUST be validated before calling Login -func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { c.LogAuditWithUserId(user.Id, "attempt") - if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { - c.Err = model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id) - c.Err.StatusCode = http.StatusForbidden + if err = checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { + c.LogAuditWithUserId(user.Id, "fail") + c.Err = err + c.Err.StatusCode = http.StatusUnauthorized return } - if user.DeleteAt > 0 { - c.Err = model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id) - c.Err.StatusCode = http.StatusForbidden - return + // User is authenticated at this point + + Login(c, w, r, user, props["device_id"]) + c.LogAuditWithUserId(user.Id, "success") + + if user != nil { + user.Sanitize(map[string]bool{}) + } else { + user = &model.User{} } + w.Write([]byte(user.ToJson())) +} - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} +// User MUST be authenticated completely before calling Login +func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { + + session := &model.Session{UserId: user.Id, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24 @@ -607,7 +656,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, // A special case where we logout of all other sessions with the same Id if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil { c.Err = result.Err - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusInternalServerError return } else { sessions := result.Data.([]*model.Session) @@ -653,7 +702,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, if result := <-Srv.Store.Session().Save(session); result.Err != nil { c.Err = result.Err - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusInternalServerError return } else { session = result.Data.(*model.Session) @@ -675,110 +724,6 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, http.SetCookie(w, sessionCookie) c.Session = *session - c.LogAuditWithUserId(user.Id, "success") -} - -func login(c *Context, w http.ResponseWriter, r *http.Request) { - props := model.MapFromJson(r.Body) - - if len(props["password"]) == 0 { - c.Err = model.NewLocAppError("login", "api.user.login.blank_pwd.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - var user *model.User - if len(props["id"]) != 0 { - user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"]) - } else if len(props["email"]) != 0 && len(props["name"]) != 0 { - user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"]) - } else if len(props["username"]) != 0 && len(props["name"]) != 0 { - user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"]) - } else { - c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - if c.Err != nil { - return - } - - if user != nil { - user.Sanitize(map[string]bool{}) - } else { - user = &model.User{} - } - w.Write([]byte(user.ToJson())) -} - -func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { - if !*utils.Cfg.LdapSettings.Enable { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - props := model.MapFromJson(r.Body) - - password := props["password"] - id := props["id"] - teamName := props["teamName"] - mfaToken := props["token"] - - if len(password) == 0 { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - if len(id) == 0 { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.need_id.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - teamc := Srv.Store.Team().GetByName(teamName) - - ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.not_available.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - var team *model.Team - if result := <-teamc; result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - user, err := ldapInterface.DoLogin(team, id, password) - if err != nil { - c.Err = err - return - } - - if !checkUserLoginAttempts(c, user) { - return - } - - if !checkUserMfa(c, user, mfaToken) { - return - } - - // User is authenticated at this point - - Login(c, w, r, user, props["device_id"]) - - if user != nil { - user.Sanitize(map[string]bool{}) - } else { - user = &model.User{} - } - w.Write([]byte(user.ToJson())) } func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { @@ -805,7 +750,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { // A special case where we logout of all other sessions with the same Id if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusInternalServerError return } else { sessions := result.Data.([]*model.Session) @@ -875,7 +820,7 @@ func RevokeAllSession(c *Context, userId string) { func getSessions(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["user_id"] if !c.HasPermissionsToUser(id, "getSessions") { return @@ -907,9 +852,11 @@ func logout(c *Context, w http.ResponseWriter, r *http.Request) { func Logout(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("") c.RemoveSessionCookie(w, r) - if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { - c.Err = result.Err - return + if c.Session.Id != "" { + if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { + c.Err = result.Err + return + } } } @@ -934,29 +881,99 @@ 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"] = "" +func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { + + il := model.InitialLoad{} + + var cchan store.StoreChannel + + if sessionCache.Len() == 0 { + // Below is a speical case when intializating a new server + // Lets check to make sure the server is really empty + + cchan = Srv.Store.User().GetTotalUsersCount() + } 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 + uchan := Srv.Store.User().Get(c.Session.UserId) + pchan := Srv.Store.Preference().GetAll(c.Session.UserId) + tchan := Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) + dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId) + + il.TeamMembers = c.Session.TeamMembers + + if ru := <-uchan; ru.Err != nil { + c.Err = ru.Err + return + } else { + il.User = ru.Data.(*model.User) + il.User.Sanitize(map[string]bool{}) + } + + if rp := <-pchan; rp.Err != nil { + c.Err = rp.Err + return + } else { + il.Preferences = rp.Data.(model.Preferences) + } + + if rt := <-tchan; rt.Err != nil { + c.Err = rt.Err + return + } else { + il.Teams = rt.Data.([]*model.Team) + + for _, team := range il.Teams { + team.Sanitize() + } + } + + if dp := <-dpchan; dp.Err != nil { + c.Err = dp.Err return } else { - team = tr.Data.(*model.Team) + profiles := dp.Data.(map[string]*model.User) + + for k, p := range profiles { + options := utils.Cfg.GetSanitizeOptions() + options["passwordupdate"] = false + + if c.IsSystemAdmin() { + options["fullname"] = true + options["email"] = true + } else { + p.ClearNonProfileFields() + } + + p.Sanitize(options) + profiles[k] = p + } + + il.DirectProfiles = profiles } - data["logged_in"] = "true" - data["team_name"] = team.Name } - w.Write([]byte(model.MapToJson(data))) + + if cchan != nil { + if cr := <-cchan; cr.Err != nil { + c.Err = cr.Err + return + } else { + count := cr.Data.(int64) + if count <= 0 { + il.NoAccounts = true + } + } + } + + il.ClientCfg = utils.ClientCfg + il.LicenseCfg = utils.ClientLicense + + w.Write([]byte(il.ToJson())) } func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["user_id"] if !c.HasPermissionsToUser(id, "getUser") { return @@ -977,17 +994,12 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id, ok := params["id"] - if ok { - // You must be system admin to access another team - if id != c.Session.TeamId { - if !c.HasSystemAdminPermissions("getProfiles") { - return - } - } + id := params["id"] - } else { - id = c.Session.TeamId + if c.Session.GetTeamByTeamId(id) == nil { + if !c.HasSystemAdminPermissions("getProfiles") { + return + } } etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string) @@ -1008,10 +1020,44 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { if c.IsSystemAdmin() { options["fullname"] = true options["email"] = true + } else { + p.ClearNonProfileFields() + } + + p.Sanitize(options) + profiles[k] = p + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write([]byte(model.UserMapToJson(profiles))) + return + } +} + +func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) { + etag := (<-Srv.Store.User().GetEtagForDirectProfiles(c.Session.UserId)).Data.(string) + if HandleEtag(etag, w, r) { + return + } + + if result := <-Srv.Store.User().GetDirectProfiles(c.Session.UserId); result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.(map[string]*model.User) + + for k, p := range profiles { + options := utils.Cfg.GetSanitizeOptions() + options["passwordupdate"] = false + + if c.IsSystemAdmin() { + options["fullname"] = true + options["email"] = true + } else { + p.ClearNonProfileFields() } p.Sanitize(options) - p.ClearNonProfileFields() profiles[k] = p } @@ -1023,7 +1069,7 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["user_id"] if !c.HasPermissionsToUser(id, "getAudits") { return @@ -1133,7 +1179,7 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + id := params["user_id"] if result := <-Srv.Store.User().Get(id); result.Err != nil { c.Err = result.Err @@ -1148,7 +1194,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { return } } else { - path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png" + path := "/users/" + id + "/profile.png" if data, err := ReadFile(path); err != nil { @@ -1249,7 +1295,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { return } - path := "teams/" + c.Session.TeamId + "/users/" + c.Session.UserId + "/profile.png" + path := "users/" + c.Session.UserId + "/profile.png" if err := WriteFile(buf.Bytes(), path); err != nil { c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "") @@ -1285,15 +1331,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { rusers := result.Data.([2]*model.User) if rusers[0].Email != rusers[1].Email { - if tresult := <-Srv.Store.Team().Get(rusers[1].TeamId); tresult.Err != nil { - l4g.Error(tresult.Err.Message) - } else { - team := tresult.Data.(*model.Team) - sendEmailChangeEmailAndForget(c, rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) + sendEmailChangeEmailAndForget(c, rusers[1].Email, rusers[0].Email, c.GetSiteURL()) - if utils.Cfg.EmailSettings.RequireEmailVerification { - SendEmailChangeVerifyEmailAndForget(c, rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } + if utils.Cfg.EmailSettings.RequireEmailVerification { + SendEmailChangeVerifyEmailAndForget(c, rusers[0].Id, rusers[0].Email, c.GetSiteURL()) } } @@ -1346,12 +1387,10 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { user := result.Data.(*model.User) - tchan := Srv.Store.Team().Get(user.TeamId) - if user.AuthData != "" { c.LogAudit("failed - tried to update user password who was logged in through oauth") c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService) - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest return } @@ -1363,17 +1402,11 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil { c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.failed.app_error", nil, uresult.Err.Error()) - c.Err.StatusCode = http.StatusForbidden return } else { c.LogAudit("completed") - if tresult := <-tchan; tresult.Err != nil { - l4g.Error(tresult.Err.Message) - } else { - team := tresult.Data.(*model.Team) - sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.update_password.menu")) - } + sendPasswordChangeEmailAndForget(c, user.Email, c.GetSiteURL(), c.T("api.user.update_password.menu")) data := make(map[string]string) data["user_id"] = uresult.Data.(string) @@ -1391,11 +1424,18 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { } new_roles := props["new_roles"] - if !model.IsValidRoles(new_roles) { + if !model.IsValidUserRoles(new_roles) { c.SetInvalidParam("updateRoles", "new_roles") return } + // If you are not the system admin then you can only demote yourself + if !c.IsSystemAdmin() && user_id != c.Session.UserId { + c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.system_admin_set.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + } + + // Only another system admin can add the system admin role if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() { c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.system_admin_set.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden @@ -1410,22 +1450,6 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if !c.HasPermissionsToTeam(user.TeamId, "updateRoles") { - return - } - - if !c.IsTeamAdmin() { - c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.permissions.app_error", nil, "userId="+user_id) - c.Err.StatusCode = http.StatusForbidden - return - } - - if user.IsInRole(model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() { - c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.system_admin_mod.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - ruser := UpdateRoles(c, user, new_roles) if c.Err != nil { return @@ -1456,29 +1480,6 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { } func UpdateRoles(c *Context, user *model.User, roles string) *model.User { - // make sure there is at least 1 other active admin - - if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) { - if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) { - if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { - c.Err = result.Err - return nil - } else { - activeAdmins := -1 - profileUsers := result.Data.(map[string]*model.User) - for _, profileUser := range profileUsers { - if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { - activeAdmins = activeAdmins + 1 - } - } - - if activeAdmins <= 0 { - c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.one_admin.app_error", nil, "") - return nil - } - } - } - } user.Roles = roles @@ -1513,35 +1514,13 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if !c.HasPermissionsToTeam(user.TeamId, "updateActive") { - return - } + // true when you're trying to de-activate yourself + isSelfDeactive := !active && user_id == c.Session.UserId - if !c.IsTeamAdmin() { + if !isSelfDeactive && !c.IsSystemAdmin() { c.Err = model.NewLocAppError("updateActive", "api.user.update_active.permissions.app_error", nil, "userId="+user_id) c.Err.StatusCode = http.StatusForbidden - return - } - - // make sure there is at least 1 other active admin - if !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) { - if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - activeAdmins := -1 - profileUsers := result.Data.(map[string]*model.User) - for _, profileUser := range profileUsers { - if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { - activeAdmins = activeAdmins + 1 - } - } - - if activeAdmins <= 0 { - c.Err = model.NewLocAppError("updateRoles", "api.user.update_roles.one_admin.app_error", nil, "userId="+user_id) - return - } - } + return } ruser := UpdateActive(c, user, active) @@ -1631,12 +1610,33 @@ func PermanentDeleteUser(c *Context, user *model.User) *model.AppError { return result.Err } + if result := <-Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil { + return result.Err + } + + if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil { + return result.Err + } + l4g.Warn(utils.T("api.user.permanent_delete_user.deleted.warn"), user.Email, user.Id) c.LogAuditWithUserId("", fmt.Sprintf("success userId=%v", user.Id)) return nil } +func PermanentDeleteAllUsers(c *Context) *model.AppError { + if result := <-Srv.Store.User().GetAll(); result.Err != nil { + return result.Err + } else { + users := result.Data.([]*model.User) + for _, user := range users { + PermanentDeleteUser(c, user) + } + } + + return nil +} + func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -1646,41 +1646,28 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { return } - name := props["name"] - if len(name) == 0 { - c.SetInvalidParam("sendPasswordReset", "name") - return - } - - var team *model.Team - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.find.app_error", nil, "email="+email+" team_id="+team.Id) + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.find.app_error", nil, "email="+email) return } else { user = result.Data.(*model.User) } if len(user.AuthData) != 0 { - c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id+", teamId="+team.Id) + c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id) return } - newProps := make(map[string]string) - newProps["user_id"] = user.Id - newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) + recovery := &model.PasswordRecovery{} + recovery.UserId = user.Id - data := model.MapToJson(newProps) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) + if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil { + c.Err = result.Err + return + } - link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) + link := fmt.Sprintf("%s/reset_password_complete?code=%s", c.GetSiteURL(), url.QueryEscape(recovery.Code)) subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") @@ -1706,110 +1693,89 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) newPassword := props["new_password"] - if len(newPassword) < 5 { + if len(newPassword) < model.MIN_PASSWORD_LENGTH { c.SetInvalidParam("resetPassword", "new_password") return } - name := props["name"] - if len(name) == 0 { - c.SetInvalidParam("resetPassword", "name") + code := props["code"] + if len(code) != model.PASSWORD_RECOVERY_CODE_SIZE { + c.SetInvalidParam("resetPassword", "code") return } - userId := props["user_id"] - hash := props["hash"] - timeStr := "" - - if !c.IsSystemAdmin() { - if len(hash) == 0 { - c.SetInvalidParam("resetPassword", "hash") - return - } + c.LogAudit("attempt") - data := model.MapFromJson(strings.NewReader(props["data"])) + userId := "" - userId = data["user_id"] + if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil { + c.LogAuditWithUserId(userId, "fail - bad code") + c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error()) + return + } else { + recovery := result.Data.(*model.PasswordRecovery) - timeStr = data["time"] - if len(timeStr) == 0 { - c.SetInvalidParam("resetPassword", "data:time") + if model.GetMillis()-recovery.CreateAt < model.PASSWORD_RECOVER_EXPIRY_TIME { + userId = recovery.UserId + } else { + c.LogAuditWithUserId(userId, "fail - link expired") + c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "") return } + + go func() { + if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil { + l4g.Error("%v", result.Err) + } + }() } - if len(userId) != 26 { - c.SetInvalidParam("resetPassword", "user_id") + if err := ResetPassword(c, userId, newPassword); err != nil { + c.Err = err return } - c.LogAuditWithUserId(userId, "attempt") + c.LogAuditWithUserId(userId, "success") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } + rdata := map[string]string{} + rdata["status"] = "ok" + w.Write([]byte(model.MapToJson(rdata))) +} +func ResetPassword(c *Context, userId, newPassword string) *model.AppError { var user *model.User if result := <-Srv.Store.User().Get(userId); result.Err != nil { - c.Err = result.Err - return + return result.Err } else { user = result.Data.(*model.User) } if len(user.AuthData) != 0 { - c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id+", teamId="+team.Id) - return - } - - if user.TeamId != team.Id { - c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.wrong_team.app_error", nil, "userId="+user.Id+", teamId="+team.Id) - c.Err.StatusCode = http.StatusForbidden - return - } - - if !c.IsSystemAdmin() { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.invalid_link.app_error", nil, "") - return - } + return model.NewLocAppError("ResetPassword", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id) - t, err := strconv.ParseInt(timeStr, 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "") - return - } } if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { - c.Err = result.Err - return - } else { - c.LogAuditWithUserId(userId, "success") + return result.Err } - sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.reset_password.method")) + sendPasswordChangeEmailAndForget(c, user.Email, c.GetSiteURL(), c.T("api.user.reset_password.method")) - props["new_password"] = "" - w.Write([]byte(model.MapToJson(props))) + return nil } -func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { +func sendPasswordChangeEmailAndForget(c *Context, email, siteURL, method string) { go func() { 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"]}) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "SiteName": utils.Cfg.TeamSettings.SiteName}) 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", - map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method})) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method})) if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_password_change_email_and_forget.error"), err) @@ -1818,19 +1784,19 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR }() } -func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { +func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, siteURL string) { go func() { subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", - map[string]interface{}{"TeamDisplayName": teamDisplayName}) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName 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", - map[string]interface{}{"TeamDisplayName": teamDisplayName, "NewEmail": newEmail})) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail})) if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err) @@ -1839,21 +1805,21 @@ func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayNa }() } -func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, siteURL string) { go func() { - link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId), newUserEmail) 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}) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName 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", - map[string]interface{}{"TeamDisplayName": teamDisplayName}) + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) bodyPage.Props["VerifyUrl"] = link bodyPage.Props["VerifyButton"] = c.T("api.templates.email_change_verify_body.button") @@ -1929,7 +1895,7 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.User().GetProfileByIds(userIds); result.Err != nil { c.Err = result.Err return } else { @@ -1937,17 +1903,6 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { statuses := map[string]string{} for _, profile := range profiles { - found := false - for _, uid := range userIds { - if uid == profile.Id { - found = true - } - } - - if !found { - continue - } - if profile.IsOffline() { statuses[profile.Id] = model.USER_OFFLINE } else if profile.IsAway() { @@ -1957,131 +1912,18 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } } - //w.Header().Set("Cache-Control", "max-age=9, public") // 2 mins w.Write([]byte(model.MapToJson(statuses))) return } } -func GetAuthorizationCode(c *Context, service, teamName string, props map[string]string, loginHint string) (string, *model.AppError) { - - sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Enable { - return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) - } - - clientId := sso.Id - endpoint := sso.AuthEndpoint - scope := sso.Scope - - props["hash"] = model.HashPassword(clientId) - props["team"] = teamName - state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" - - authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) - - if len(scope) > 0 { - authUrl += "&scope=" + utils.UrlEncode(scope) - } - - if len(loginHint) > 0 { - authUrl += "&login_hint=" + utils.UrlEncode(loginHint) - } - - return authUrl, nil -} - -func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, map[string]string, *model.AppError) { - sso := utils.Cfg.GetSSOService(service) - if sso == nil || !sso.Enable { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service) - } - - stateStr := "" - if b, err := b64.StdEncoding.DecodeString(state); err != nil { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error()) - } else { - stateStr = string(b) - } - - stateProps := model.MapFromJson(strings.NewReader(stateStr)) - - if !model.ComparePassword(stateProps["hash"], sso.Id) { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") - } - - ok := true - teamName := "" - if teamName, ok = stateProps["team"]; !ok { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state_team.app_error", nil, "") - } - - tchan := Srv.Store.Team().GetByName(teamName) - - p := url.Values{} - p.Set("client_id", sso.Id) - p.Set("client_secret", sso.Secret) - p.Set("code", code) - p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - p.Set("redirect_uri", redirectUri) - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - client := &http.Client{Transport: tr} - req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - - var ar *model.AccessResponse - if resp, err := client.Do(req); err != nil { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) - } else { - ar = model.AccessResponseFromJson(resp.Body) - if ar == nil { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, "") - } - } - - if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType) - } - - if len(ar.AccessToken) == 0 { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "") - } - - p = url.Values{} - p.Set("access_token", ar.AccessToken) - req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", "Bearer "+ar.AccessToken) - - if resp, err := client.Do(req); err != nil { - return nil, nil, nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", - map[string]interface{}{"Service": service}, err.Error()) - } else { - if result := <-tchan; result.Err != nil { - return nil, nil, nil, result.Err - } else { - return resp.Body, result.Data.(*model.Team), stateProps, nil - } - } - -} - -func IsUsernameTaken(name string, teamId string) bool { +func IsUsernameTaken(name string) bool { if !model.IsValidUsername(name) { return false } - if result := <-Srv.Store.User().GetByUsername(teamId, name); result.Err != nil { + if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil { return false } else { return true @@ -2099,12 +1941,6 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamName := props["team_name"] - if len(teamName) == 0 { - c.SetInvalidParam("emailToOAuth", "team_name") - return - } - service := props["service"] if len(service) == 0 { c.SetInvalidParam("emailToOAuth", "service") @@ -2119,17 +1955,8 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.LogAudit("fail - couldn't get team") - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2137,8 +1964,9 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, password) { - c.LogAuditWithUserId(user.Id, "fail - invalid password") + if err := checkPasswordAndAllCriteria(user, password, ""); err != nil { + c.LogAuditWithUserId(user.Id, "failed - bad authentication") + c.Err = err return } @@ -2147,7 +1975,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { stateProps["email"] = email m := map[string]string{} - if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil { c.LogAuditWithUserId(user.Id, "fail - oauth issue") c.Err = err return @@ -2159,52 +1987,6 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } -func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) { - authData := "" - ssoEmail := "" - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", - map[string]interface{}{"Service": service}, "") - return - } else { - ssoUser := provider.GetUserFromJson(userData) - authData = ssoUser.AuthData - ssoEmail = ssoUser.Email - } - - if len(authData) == 0 { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error", - map[string]interface{}{"Service": service}, "") - return - } - - if len(email) == 0 { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "") - return - } - - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user = result.Data.(*model.User) - } - - RevokeAllSession(c, user.Id) - if c.Err != nil { - return - } - - if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil { - c.Err = result.Err - return - } - - sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") -} - func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -2214,12 +1996,6 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamName := props["team_name"] - if len(teamName) == 0 { - c.SetInvalidParam("oauthToEmail", "team_name") - return - } - email := props["email"] if len(email) == 0 { c.SetInvalidParam("oauthToEmail", "email") @@ -2228,17 +2004,8 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.LogAudit("fail - couldn't get team") - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2259,7 +2026,7 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) + sendSignInChangeEmailAndForget(c, user.Email, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) RevokeAllSession(c, c.Session.UserId) if c.Err != nil { @@ -2267,7 +2034,7 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { } m := map[string]string{} - m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + m["follow_link"] = "/login?extra=signin_change" c.LogAudit("success") w.Write([]byte(model.MapToJson(m))) @@ -2288,12 +2055,6 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamName := props["team_name"] - if len(teamName) == 0 { - c.SetInvalidParam("emailToLdap", "team_name") - return - } - ldapId := props["ldap_id"] if len(ldapId) == 0 { c.SetInvalidParam("emailToLdap", "ldap_id") @@ -2308,17 +2069,8 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.LogAudit("fail - couldn't get team") - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2326,8 +2078,9 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, emailPassword) { - c.LogAuditWithUserId(user.Id, "fail - invalid email password") + if err := checkPasswordAndAllCriteria(user, emailPassword, ""); err != nil { + c.LogAuditWithUserId(user.Id, "failed - bad authentication") + c.Err = err return } @@ -2343,16 +2096,16 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { return } - if err := ldapInterface.SwitchToEmail(user.Id, ldapId, ldapPassword); err != nil { + if err := ldapInterface.SwitchToLdap(user.Id, ldapId, ldapPassword); err != nil { c.LogAuditWithUserId(user.Id, "fail - ldap switch failed") c.Err = err return } - sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "LDAP") + sendSignInChangeEmailAndForget(c, user.Email, c.GetSiteURL(), "LDAP") m := map[string]string{} - m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + m["follow_link"] = "/login?extra=signin_change" c.LogAudit("success") w.Write([]byte(model.MapToJson(m))) @@ -2373,12 +2126,6 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamName := props["team_name"] - if len(teamName) == 0 { - c.SetInvalidParam("ldapToEmail", "team_name") - return - } - ldapPassword := props["ldap_password"] if len(ldapPassword) == 0 { c.SetInvalidParam("ldapToEmail", "ldap_password") @@ -2387,17 +2134,8 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.LogAudit("fail - couldn't get team") - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2434,27 +2172,27 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) + sendSignInChangeEmailAndForget(c, user.Email, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) m := map[string]string{} - m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + m["follow_link"] = "/login?extra=signin_change" c.LogAudit("success") w.Write([]byte(model.MapToJson(m))) } -func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { +func sendSignInChangeEmailAndForget(c *Context, email, siteURL, method string) { go func() { 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"]}) + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) 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", - map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method})) + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method})) if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_sign_in_change_email_and_forget.error"), err) @@ -2488,49 +2226,34 @@ func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { } c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + c.Err.StatusCode = http.StatusBadRequest } 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 { + if result := <-Srv.Store.User().GetByEmail(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)) + SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, c.GetSiteURL()) } else { - SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + SendVerifyEmailAndForget(c, user.Id, user.Email, c.GetSiteURL()) } } } func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) { uchan := Srv.Store.User().Get(c.Session.UserId) - tchan := Srv.Store.Team().Get(c.Session.TeamId) var user *model.User if result := <-uchan; result.Err != nil { @@ -2540,14 +2263,6 @@ func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - var team *model.Team - if result := <-tchan; result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - mfaInterface := einterfaces.GetMfaInterface() if mfaInterface == nil { c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.generate_mfa_qr.not_available.app_error", nil, "") @@ -2555,7 +2270,7 @@ func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) { return } - img, err := mfaInterface.GenerateQrCode(team, user) + img, err := mfaInterface.GenerateQrCode(user) if err != nil { c.Err = err return @@ -2583,28 +2298,13 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { } } - mfaInterface := einterfaces.GetMfaInterface() - if mfaInterface == nil { - c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.update_mfa.not_available.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - if activate { - var user *model.User - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - user = result.Data.(*model.User) - } - - if err := mfaInterface.Activate(user, token); err != nil { + if err := ActivateMfa(c.Session.UserId, token); err != nil { c.Err = err return } } else { - if err := mfaInterface.Deactivate(c.Session.UserId); err != nil { + if err := DeactivateMfa(c.Session.UserId); err != nil { c.Err = err return } @@ -2615,6 +2315,43 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(rdata))) } +func ActivateMfa(userId, token string) *model.AppError { + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return err + } + + var user *model.User + if result := <-Srv.Store.User().Get(userId); result.Err != nil { + return result.Err + } else { + user = result.Data.(*model.User) + } + + if err := mfaInterface.Activate(user, token); err != nil { + return err + } + + return nil +} + +func DeactivateMfa(userId string) *model.AppError { + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return err + } + + if err := mfaInterface.Deactivate(userId); err != nil { + return err + } + + return nil +} + func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { rdata := map[string]string{} @@ -2633,33 +2370,19 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamName := props["team_name"] - if len(teamName) == 0 { - c.SetInvalidParam("checkMfa", "team_name") - return - } - loginId := props["login_id"] if len(loginId) == 0 { c.SetInvalidParam("checkMfa", "login_id") 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) - } - var uchan store.StoreChannel if method == model.USER_AUTH_SERVICE_EMAIL { - uchan = Srv.Store.User().GetByEmail(team.Id, loginId) + uchan = Srv.Store.User().GetByEmail(loginId) } else if method == model.USER_AUTH_SERVICE_USERNAME { - uchan = Srv.Store.User().GetByUsername(team.Id, loginId) + uchan = Srv.Store.User().GetByUsername(loginId) } else if method == model.USER_AUTH_SERVICE_LDAP { - uchan = Srv.Store.User().GetByAuth(team.Id, loginId, model.USER_AUTH_SERVICE_LDAP) + uchan = Srv.Store.User().GetByAuth(loginId, model.USER_AUTH_SERVICE_LDAP) } rdata := map[string]string{} diff --git a/api/user_test.go b/api/user_test.go index 33f3fdad4..3c744120c 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -6,11 +6,6 @@ package api import ( "bytes" "fmt" - "github.com/goamz/goamz/aws" - "github.com/goamz/goamz/s3" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" - "github.com/mattermost/platform/utils" "image" "image/color" "io" @@ -20,21 +15,31 @@ import ( "strings" "testing" "time" + + "github.com/goamz/goamz/aws" + "github.com/goamz/goamz/s3" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" ) func TestCreateUser(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: "hello"} + user := model.User{Email: strings.ToLower("success+"+model.NewId()) + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "hello", Username: "n" + model.NewId()} ruser, err := Client.CreateUser(&user, "") if err != nil { t.Fatal(err) } + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) + if ruser.Data.(*model.User).Nickname != user.Nickname { t.Fatal("nickname didn't match") } @@ -48,13 +53,15 @@ func TestCreateUser(t *testing.T) { } ruser.Data.(*model.User).Id = "" + ruser.Data.(*model.User).Username = "n" + model.NewId() if _, err := Client.CreateUser(ruser.Data.(*model.User), ""); err != nil { if err.Message != "An account with that email already exists." { t.Fatal(err) } } - ruser.Data.(*model.User).Email = "test2@nowhere.com" + ruser.Data.(*model.User).Email = "success+" + model.NewId() + "@simulator.amazonses.com" + ruser.Data.(*model.User).Username = user.Username if _, err := Client.CreateUser(ruser.Data.(*model.User), ""); err != nil { if err.Message != "An account with that username already exists." { t.Fatal(err) @@ -73,34 +80,16 @@ func TestCreateUser(t *testing.T) { } } -func TestCreateUserAllowedDomains(t *testing.T) { - Setup() - - team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE, AllowedDomains: "spinpunch.com, @nowh.com,@hello.com"} - 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: "hello"} - - _, err := Client.CreateUser(&user, "") - if err == nil { - t.Fatal("should have failed") - } - - user.Email = "test@nowh.com" - _, err = Client.CreateUser(&user, "") - if err != nil { - t.Fatal(err) - } -} - func TestLogin(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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", Username: "corey", Password: "pwd"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey" + model.NewId(), Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) if result, err := Client.LoginById(ruser.Data.(*model.User).Id, user.Password); err != nil { @@ -155,7 +144,7 @@ func TestLogin(t *testing.T) { team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE} rteam2 := Client.Must(Client.CreateTeam(&team2)) - user2 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} if _, err := Client.CreateUserFromSignup(&user2, "junk", "1231312"); err == nil { t.Fatal("Should have errored, signed up without hashed email") @@ -179,14 +168,11 @@ func TestLogin(t *testing.T) { } func TestLoginWithDeviceId(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)) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + Client.Must(Client.Logout()) deviceId := model.NewId() if result, err := Client.LoginByEmailWithDevice(team.Name, user.Email, user.Password, deviceId); err != nil { @@ -210,20 +196,17 @@ func TestLoginWithDeviceId(t *testing.T) { } func TestSessions(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)) + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + user := th.BasicUser + Client.Must(Client.Logout()) deviceId := model.NewId() Client.LoginByEmailWithDevice(team.Name, user.Email, user.Password, deviceId) Client.LoginByEmail(team.Name, user.Email, user.Password) - r1, err := Client.GetSessions(ruser.Id) + r1, err := Client.GetSessions(user.Id) if err != nil { t.Fatal(err) } @@ -249,7 +232,7 @@ func TestSessions(t *testing.T) { t.Fatal(err) } - r2, err := Client.GetSessions(ruser.Id) + r2, err := Client.GetSessions(user.Id) if err != nil { t.Fatal(err) } @@ -262,24 +245,28 @@ func TestSessions(t *testing.T) { } func TestGetUser(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) - user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser2, _ := Client.CreateUser(&user2, "") + LinkUserToTeam(ruser2.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id)) team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam2, _ := Client.CreateTeam(&team2) - user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user3 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser3, _ := Client.CreateUser(&user3, "") + LinkUserToTeam(ruser3.Data.(*model.User), rteam2.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id)) Client.LoginByEmail(team.Name, user.Email, user.Password) @@ -353,14 +340,36 @@ func TestGetUser(t *testing.T) { } } +func TestGetDirectProfiles(t *testing.T) { + th := Setup().InitBasic() + + th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id)) + + if result, err := th.BasicClient.GetDirectProfiles(""); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) != 1 { + t.Fatal("map was wrong length") + } + + if users[th.BasicUser2.Id] == nil { + t.Fatal("missing expected user") + } + } +} + func TestGetAudits(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) time.Sleep(100 * time.Millisecond) @@ -390,7 +399,8 @@ func TestGetAudits(t *testing.T) { } func TestUserCreateImage(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") if err != nil { @@ -412,8 +422,9 @@ func TestUserCreateImage(t *testing.T) { 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: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) Client.LoginByEmail(team.Name, user.Email, "pwd") @@ -428,26 +439,27 @@ func TestUserCreateImage(t *testing.T) { s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) - if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { + if err := bucket.Del("/users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) } } else { - path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" + path := utils.Cfg.FileSettings.Directory + "/users/" + user.Id + "/profile.png" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } } - } func TestUserUploadProfileImage(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) if utils.Cfg.FileSettings.DriverName != "" { @@ -455,13 +467,14 @@ func TestUserUploadProfileImage(t *testing.T) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr == nil { t.Fatal("Should have errored") } Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) - if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr == nil { t.Fatal("Should have errored") } @@ -486,7 +499,7 @@ func TestUserUploadProfileImage(t *testing.T) { t.Fatal(err) } - if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr == nil { t.Fatal("Should have errored") } @@ -512,7 +525,7 @@ func TestUserUploadProfileImage(t *testing.T) { t.Fatal(err) } - if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr != nil { + if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr != nil { t.Fatal(upErr) } @@ -526,11 +539,11 @@ func TestUserUploadProfileImage(t *testing.T) { s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) - if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { + if err := bucket.Del("users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) } } else { - path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" + path := utils.Cfg.FileSettings.Directory + "users/" + user.Id + "/profile.png" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -538,22 +551,24 @@ func TestUserUploadProfileImage(t *testing.T) { } else { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr.StatusCode != http.StatusNotImplemented { + if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr.StatusCode != http.StatusNotImplemented { t.Fatal("Should have failed with 501 - Not Implemented") } } } func TestUserUpdate(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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) time1 := model.GetMillis() - user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", LastActivityAt: time1, LastPingAt: time1, Roles: ""} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", LastActivityAt: time1, LastPingAt: time1, Roles: ""} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) if _, err := Client.UpdateUser(user); err == nil { @@ -561,6 +576,7 @@ func TestUserUpdate(t *testing.T) { } Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) time.Sleep(100 * time.Millisecond) @@ -569,7 +585,6 @@ func TestUserUpdate(t *testing.T) { time.Sleep(100 * time.Millisecond) user.Nickname = "Jim Jimmy" - user.TeamId = "12345678901234567890123456" user.LastActivityAt = time2 user.LastPingAt = time2 user.Roles = model.ROLE_TEAM_ADMIN @@ -581,9 +596,6 @@ func TestUserUpdate(t *testing.T) { if result.Data.(*model.User).Nickname != "Jim Jimmy" { t.Fatal("Nickname did not update properly") } - if result.Data.(*model.User).TeamId != team.Id { - t.Fatal("TeamId should not have updated") - } if result.Data.(*model.User).LastActivityAt == time2 { t.Fatal("LastActivityAt should not have updated") } @@ -598,21 +610,13 @@ func TestUserUpdate(t *testing.T) { } } - user.TeamId = "junk" - if _, err := Client.UpdateUser(user); err == nil { - t.Fatal("Should have errored - tried to change teamId to junk") - } - - user.TeamId = team.Id - if _, err := Client.UpdateUser(nil); err == nil { - t.Fatal("Should have errored") - } - - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.SetTeamId(team.Id) user.Nickname = "Tim Timmy" @@ -622,13 +626,16 @@ func TestUserUpdate(t *testing.T) { } func TestUserUpdatePassword(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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) + Client.SetTeamId(team.Id) - user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) if _, err := Client.UpdateUserPassword(user.Id, "pwd", "newpwd"); err == nil { @@ -670,8 +677,9 @@ func TestUserUpdatePassword(t *testing.T) { t.Fatal(err) } - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) Client.LoginByEmail(team.Name, user2.Email, "pwd") @@ -681,17 +689,20 @@ func TestUserUpdatePassword(t *testing.T) { } func TestUserUpdateRoles(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) data := make(map[string]string) @@ -703,6 +714,7 @@ func TestUserUpdateRoles(t *testing.T) { } Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.SetTeamId(team.Id) if _, err := Client.UpdateUserRoles(data); err == nil { t.Fatal("Should have errored, not admin") @@ -711,11 +723,13 @@ func TestUserUpdateRoles(t *testing.T) { team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - user3 := &model.User{TeamId: team2.Id, Email: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + LinkUserToTeam(user3, team2) store.Must(Srv.Store.User().VerifyEmail(user3.Id)) Client.LoginByEmail(team2.Name, user3.Email, "pwd") + Client.SetTeamId(team2.Id) data["user_id"] = user2.Id @@ -740,27 +754,25 @@ func TestUserUpdateRoles(t *testing.T) { data["user_id"] = user2.Id - if result, err := Client.UpdateUserRoles(data); err != nil { - t.Log(data["new_roles"]) - t.Fatal(err) - } else { - if result.Data.(*model.User).Roles != "admin" { - t.Fatal("Roles did not update properly") - } + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, bad role") } } func TestUserUpdateDeviceId(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) deviceId := model.PUSH_NOTIFY_APPLE + ":1234567890" if _, err := Client.AttachDeviceId(deviceId); err != nil { @@ -779,17 +791,20 @@ func TestUserUpdateDeviceId(t *testing.T) { } func TestUserUpdateActive(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) if _, err := Client.UpdateActive(user.Id, false); err == nil { @@ -797,25 +812,31 @@ func TestUserUpdateActive(t *testing.T) { } Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.SetTeamId(team.Id) if _, err := Client.UpdateActive(user.Id, false); err == nil { t.Fatal("Should have errored, not admin") } + Client.Must(Client.Logout()) + team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - user3 := &model.User{TeamId: team2.Id, Email: "test@nowhere.com", Nickname: "Corey Hulen", Password: "pwd"} + user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + LinkUserToTeam(user2, team2) store.Must(Srv.Store.User().VerifyEmail(user3.Id)) Client.LoginByEmail(team2.Name, user3.Email, "pwd") + Client.SetTeamId(team2.Id) if _, err := Client.UpdateActive(user.Id, false); err == nil { - t.Fatal("Should have errored, wrong team") + t.Fatal("Should have errored, not yourself") } Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) if _, err := Client.UpdateActive("junk", false); err == nil { t.Fatal("Should have errored, bad id") @@ -824,35 +845,22 @@ func TestUserUpdateActive(t *testing.T) { if _, err := Client.UpdateActive("12345678901234567890123456", false); err == nil { t.Fatal("Should have errored, bad id") } - - if result, err := Client.UpdateActive(user2.Id, false); err != nil { - t.Fatal(err) - } else { - if result.Data.(*model.User).DeleteAt == 0 { - t.Fatal("active did not update properly") - } - } - - if result, err := Client.UpdateActive(user2.Id, true); err != nil { - t.Fatal(err) - } else { - if result.Data.(*model.User).DeleteAt != 0 { - t.Fatal("active did not update properly true") - } - } } func TestUserPermDelete(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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) - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + LinkUserToTeam(user1, team) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -882,161 +890,117 @@ func TestUserPermDelete(t *testing.T) { } func TestSendPasswordReset(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - data := make(map[string]string) - data["email"] = user.Email - data["name"] = team.Name - - if _, err := Client.SendPasswordReset(data); err != nil { + if _, err := Client.SendPasswordReset(user.Email); err != nil { t.Fatal(err) } - data["email"] = "" - if _, err := Client.SendPasswordReset(data); err == nil { + if _, err := Client.SendPasswordReset(""); err == nil { t.Fatal("Should have errored - no email") } - data["email"] = "junk@junk.com" - if _, err := Client.SendPasswordReset(data); err == nil { + if _, err := Client.SendPasswordReset("junk@junk.com"); err == nil { t.Fatal("Should have errored - bad email") } - data["email"] = user.Email - data["name"] = "" - if _, err := Client.SendPasswordReset(data); err == nil { - t.Fatal("Should have errored - no name") - } - - data["name"] = "junk" - if _, err := Client.SendPasswordReset(data); err == nil { - t.Fatal("Should have errored - bad name") - } - - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - data["email"] = user2.Email - data["name"] = team.Name - if _, err := Client.SendPasswordReset(data); err == nil { + if _, err := Client.SendPasswordReset(user2.Email); err == nil { t.Fatal("should have errored - SSO user can't send reset password link") } } func TestResetPassword(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam - 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: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - data := make(map[string]string) - data["new_password"] = "newpwd" - props := make(map[string]string) - props["user_id"] = user.Id - props["time"] = fmt.Sprintf("%v", model.GetMillis()) - data["data"] = model.MapToJson(props) - data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) - data["name"] = team.Name + Client.Must(Client.SendPasswordReset(user.Email)) - if _, err := Client.ResetPassword(data); err != nil { - t.Fatal(err) + var recovery *model.PasswordRecovery + if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + recovery = result.Data.(*model.PasswordRecovery) } - data["new_password"] = "" - if _, err := Client.ResetPassword(data); err == nil { + if _, err := Client.ResetPassword(recovery.Code, ""); err == nil { t.Fatal("Should have errored - no password") } - data["new_password"] = "npwd" - if _, err := Client.ResetPassword(data); err == nil { + if _, err := Client.ResetPassword(recovery.Code, "newp"); err == nil { t.Fatal("Should have errored - password too short") } - data["new_password"] = "newpwd" - data["hash"] = "" - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - no hash") + if _, err := Client.ResetPassword("", "newpwd"); err == nil { + t.Fatal("Should have errored - no code") } - data["hash"] = "junk" - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - bad hash") + if _, err := Client.ResetPassword("junk", "newpwd"); err == nil { + t.Fatal("Should have errored - bad code") } - props["user_id"] = "" - data["data"] = model.MapToJson(props) - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - no user id") + code := "" + for i := 0; i < model.PASSWORD_RECOVERY_CODE_SIZE; i++ { + code += "a" } - - data["user_id"] = "12345678901234567890123456" - data["data"] = model.MapToJson(props) - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - bad user id") - } - - props["user_id"] = user.Id - props["time"] = "" - data["data"] = model.MapToJson(props) - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - no time") + if _, err := Client.ResetPassword(code, "newpwd"); err == nil { + t.Fatal("Should have errored - bad code") } - props["time"] = fmt.Sprintf("%v", model.GetMillis()) - data["data"] = model.MapToJson(props) - data["domain"] = "" - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - no domain") + if _, err := Client.ResetPassword(recovery.Code, "newpwd"); err != nil { + t.Fatal(err) } - data["domain"] = "junk" - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - bad domain") - } + Client.Logout() + Client.Must(Client.LoginById(user.Id, "newpwd")) + Client.SetTeamId(team.Id) - team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test2@nowhere.com", Type: model.TEAM_OPEN} - team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + Client.Must(Client.SendPasswordReset(user.Email)) - data["domain"] = team2.Name - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("Should have errored - domain team doesn't match user team") + if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + recovery = result.Data.(*model.PasswordRecovery) } - user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", "1", ""); result.Err != nil { + t.Fatal(result.Err) + } - data["new_password"] = "newpwd" - props["user_id"] = user2.Id - props["time"] = fmt.Sprintf("%v", model.GetMillis()) - data["data"] = model.MapToJson(props) - data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) - data["name"] = team.Name - if _, err := Client.ResetPassword(data); err == nil { - t.Fatal("should have errored - SSO user can't reset password") + if _, err := Client.ResetPassword(recovery.Code, "newpwd"); err == nil { + t.Fatal("Should have errored - sso user") } } func TestUserUpdateNotify(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Roles: ""} + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Roles: ""} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) data := make(map[string]string) @@ -1050,6 +1014,7 @@ func TestUserUpdateNotify(t *testing.T) { } Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.SetTeamId(team.Id) if result, err := Client.UpdateUserNotify(data); err != nil { t.Fatal(err) @@ -1099,7 +1064,8 @@ func TestUserUpdateNotify(t *testing.T) { } func TestFuzzyUserCreate(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := Client.CreateTeam(&team) @@ -1115,30 +1081,36 @@ func TestFuzzyUserCreate(t *testing.T) { testEmail = utils.FUZZY_STRINGS_EMAILS[i] } - user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + testEmail, Nickname: testName, Password: "hello"} + user := model.User{Email: strings.ToLower(model.NewId()) + testEmail, Nickname: testName, Password: "hello"} - _, err := Client.CreateUser(&user, "") + ruser, err := Client.CreateUser(&user, "") if err != nil { t.Fatal(err) } + + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) } } func TestStatuses(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) - user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) + LinkUserToTeam(ruser2, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.SetTeamId(team.Id) userIds := []string{ruser2.Id} @@ -1150,6 +1122,7 @@ func TestStatuses(t *testing.T) { statuses := r1.Data.(map[string]string) if len(statuses) != 1 { + t.Log(statuses) t.Fatal("invalid number of statuses") } @@ -1162,13 +1135,15 @@ func TestStatuses(t *testing.T) { } func TestEmailToOAuth(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) m := map[string]string{} @@ -1211,17 +1186,20 @@ func TestEmailToOAuth(t *testing.T) { } func TestOAuthToEmail(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) - user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) + LinkUserToTeam(ruser2, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) m := map[string]string{} @@ -1246,12 +1224,6 @@ func TestOAuthToEmail(t *testing.T) { t.Fatal("should have failed - missing email") } - m["email"] = ruser.Email - m["team_name"] = "junk" - if _, err := Client.OAuthToEmail(m); err == nil { - t.Fatal("should have failed - bad team name") - } - m["team_name"] = team.Name m["email"] = "junk" if _, err := Client.OAuthToEmail(m); err == nil { @@ -1265,13 +1237,15 @@ func TestOAuthToEmail(t *testing.T) { } func TestLDAPToEmail(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) Client.LoginByEmail(team.Name, user.Email, user.Password) @@ -1316,13 +1290,15 @@ func TestLDAPToEmail(t *testing.T) { } func TestEmailToLDAP(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) Client.LoginByEmail(team.Name, user.Email, user.Password) @@ -1377,49 +1353,83 @@ func TestEmailToLDAP(t *testing.T) { } } -func TestMeLoggedIn(t *testing.T) { - Setup() +func TestMeInitialLoad(t *testing.T) { + th := Setup().InitBasic() - team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - rteam, _ := Client.CreateTeam(&team) + if result, err := th.BasicClient.GetInitialLoad(); err != nil { + t.Fatal(err) + } else { + il := result.Data.(*model.InitialLoad) - 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)) + if il.User == nil { + t.Fatal("should be valid") + } - Client.AuthToken = "invalid" + if il.Preferences == nil { + t.Fatal("should be valid") + } - if result, err := Client.GetMeLoggedIn(); err != nil { - t.Fatal(err) - } else { - meLoggedIn := result.Data.(map[string]string) + if len(il.Teams) != 1 { + t.Fatal("should be valid") + } - if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" { - t.Fatal("Got: " + val) + if len(il.TeamMembers) != 1 { + t.Fatal("should be valid") + } + + if len(il.ClientCfg) == 0 { + t.Fatal("should be valid") + } + + if len(il.LicenseCfg) == 0 { + t.Fatal("should be valid") } } - Client.LoginByEmail(team.Name, user.Email, user.Password) + th.BasicClient.Logout() - if result, err := Client.GetMeLoggedIn(); err != nil { + if result, err := th.BasicClient.GetInitialLoad(); err != nil { t.Fatal(err) } else { - meLoggedIn := result.Data.(map[string]string) + il := result.Data.(*model.InitialLoad) + + if il.User != nil { + t.Fatal("should be valid") + } - if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" { - t.Fatal("Got: " + val) + if il.Preferences != nil { + t.Fatal("should be valid") + } + + if len(il.Teams) != 0 { + t.Fatal("should be valid") + } + + if len(il.TeamMembers) != 0 { + t.Fatal("should be valid") + } + + if len(il.ClientCfg) == 0 { + t.Fatal("should be valid") + } + + if len(il.LicenseCfg) == 0 { + t.Fatal("should be valid") } } + } func TestGenerateMfaQrCode(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Logout() @@ -1438,13 +1448,26 @@ func TestGenerateMfaQrCode(t *testing.T) { } func TestUpdateMfa(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() + + if utils.License.Features.MFA == nil { + utils.License.Features.MFA = new(bool) + } + + enableMfa := *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication + defer func() { + utils.IsLicensed = false + *utils.License.Features.MFA = false + *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa + }() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Logout() @@ -1463,20 +1486,30 @@ func TestUpdateMfa(t *testing.T) { t.Fatal("should have failed - not licensed") } - // need to add more test cases when license and config can be configured for tests + utils.IsLicensed = true + *utils.License.Features.MFA = true + *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = true + + if _, err := Client.UpdateMfa(true, "123456"); err == nil { + t.Fatal("should have failed - bad token") + } + + // need to add more test cases when enterprise bits can be loaded into tests } func TestCheckMfa(t *testing.T) { - Setup() + th := Setup() + Client := th.CreateClient() 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"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} ruser, _ := Client.CreateUser(&user, "") + LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) - if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, team.Name, user.Email); err != nil { + if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, user.Email); err != nil { t.Fatal(err) } else { resp := result.Data.(map[string]string) @@ -1485,5 +1518,5 @@ func TestCheckMfa(t *testing.T) { } } - // need to add more test cases when license and config can be configured for tests + // need to add more test cases when enterprise bits can be loaded into tests } diff --git a/api/web_conn.go b/api/web_conn.go index 515a8ab31..397af0696 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -7,7 +7,6 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/websocket" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "time" ) @@ -21,14 +20,15 @@ const ( ) type WebConn struct { - WebSocket *websocket.Conn - Send chan *model.Message - TeamId string - UserId string - ChannelAccessCache map[string]bool + WebSocket *websocket.Conn + Send chan *model.Message + SessionId string + UserId string + hasPermissionsToChannel map[string]bool + hasPermissionsToTeam map[string]bool } -func NewWebConn(ws *websocket.Conn, teamId string, userId string, sessionId string) *WebConn { +func NewWebConn(ws *websocket.Conn, userId string, sessionId string) *WebConn { go func() { achan := Srv.Store.User().UpdateUserAndSessionActivity(userId, sessionId, model.GetMillis()) pchan := Srv.Store.User().UpdateLastPingAt(userId, model.GetMillis()) @@ -42,7 +42,14 @@ func NewWebConn(ws *websocket.Conn, teamId string, userId string, sessionId stri } }() - return &WebConn{Send: make(chan *model.Message, 64), WebSocket: ws, UserId: userId, TeamId: teamId, ChannelAccessCache: make(map[string]bool)} + return &WebConn{ + Send: make(chan *model.Message, 64), + WebSocket: ws, + UserId: userId, + SessionId: sessionId, + hasPermissionsToChannel: make(map[string]bool), + hasPermissionsToTeam: make(map[string]bool), + } } func (c *WebConn) readPump() { @@ -69,7 +76,6 @@ func (c *WebConn) readPump() { if err := c.WebSocket.ReadJSON(&msg); err != nil { return } else { - msg.TeamId = c.TeamId msg.UserId = c.UserId PublishAndForget(&msg) } @@ -107,19 +113,53 @@ func (c *WebConn) writePump() { } } -func (c *WebConn) updateChannelAccessCache(channelId string) bool { - allowed := hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.UserId)) - c.ChannelAccessCache[channelId] = allowed +func (c *WebConn) InvalidateCache() { + c.hasPermissionsToChannel = make(map[string]bool) + c.hasPermissionsToTeam = make(map[string]bool) +} + +func (c *WebConn) HasPermissionsToTeam(teamId string) bool { + perm, ok := c.hasPermissionsToTeam[teamId] + if !ok { + session := GetSession(c.SessionId) + if session == nil { + perm = false + c.hasPermissionsToTeam[teamId] = perm + } else { + member := session.GetTeamByTeamId(teamId) + + if member != nil { + perm = true + c.hasPermissionsToTeam[teamId] = perm + } else { + perm = true + c.hasPermissionsToTeam[teamId] = perm + } - return allowed + } + } + + return perm } -func hasPermissionsToChannel(sc store.StoreChannel) bool { - if cresult := <-sc; cresult.Err != nil { - return false - } else if cresult.Data.(int64) != 1 { - return false +func (c *WebConn) HasPermissionsToChannel(channelId string) bool { + perm, ok := c.hasPermissionsToChannel[channelId] + if !ok { + if cresult := <-Srv.Store.Channel().CheckPermissionsToNoTeam(channelId, c.UserId); cresult.Err != nil { + perm = false + c.hasPermissionsToChannel[channelId] = perm + } else { + count := cresult.Data.(int64) + + if count == 1 { + perm = true + c.hasPermissionsToChannel[channelId] = perm + } else { + perm = false + c.hasPermissionsToChannel[channelId] = perm + } + } } - return true + return perm } diff --git a/api/web_hub.go b/api/web_hub.go index 5fe9d6ae8..241ebcef0 100644 --- a/api/web_hub.go +++ b/api/web_hub.go @@ -10,19 +10,21 @@ import ( ) type Hub struct { - teamHubs map[string]*TeamHub - register chan *WebConn - unregister chan *WebConn - broadcast chan *model.Message - stop chan string + connections map[*WebConn]bool + register chan *WebConn + unregister chan *WebConn + broadcast chan *model.Message + stop chan string + invalidateUser chan string } var hub = &Hub{ - register: make(chan *WebConn), - unregister: make(chan *WebConn), - teamHubs: make(map[string]*TeamHub), - broadcast: make(chan *model.Message), - stop: make(chan string), + register: make(chan *WebConn), + unregister: make(chan *WebConn), + connections: make(map[*WebConn]bool), + broadcast: make(chan *model.Message), + stop: make(chan string), + invalidateUser: make(chan string), } func PublishAndForget(message *model.Message) { @@ -31,16 +33,8 @@ func PublishAndForget(message *model.Message) { }() } -func UpdateChannelAccessCache(teamId, userId, channelId string) { - if nh, ok := hub.teamHubs[teamId]; ok { - nh.UpdateChannelAccessCache(userId, channelId) - } -} - -func UpdateChannelAccessCacheAndForget(teamId, userId, channelId string) { - go func() { - UpdateChannelAccessCache(teamId, userId, channelId) - }() +func InvalidateCacheForUser(userId string) { + hub.invalidateUser <- userId } func (h *Hub) Register(webConn *WebConn) { @@ -65,34 +59,92 @@ func (h *Hub) Start() { go func() { for { select { + case webCon := <-h.register: + h.connections[webCon] = true - case c := <-h.register: - nh := h.teamHubs[c.TeamId] - - if nh == nil { - nh = NewTeamHub(c.TeamId) - h.teamHubs[c.TeamId] = nh - nh.Start() + case webCon := <-h.unregister: + if _, ok := h.connections[webCon]; ok { + delete(h.connections, webCon) + close(webCon.Send) } - - nh.Register(c) - - case c := <-h.unregister: - if nh, ok := h.teamHubs[c.TeamId]; ok { - nh.Unregister(c) + case userId := <-h.invalidateUser: + for webCon := range h.connections { + if webCon.UserId == userId { + webCon.InvalidateCache() + } } + case msg := <-h.broadcast: - nh := h.teamHubs[msg.TeamId] - if nh != nil { - nh.broadcast <- msg + for webCon := range h.connections { + if shouldSendEvent(webCon, msg) { + select { + case webCon.Send <- msg: + default: + close(webCon.Send) + delete(h.connections, webCon) + } + } } + case s := <-h.stop: l4g.Debug(utils.T("api.web_hub.start.stopping.debug"), s) - for _, v := range h.teamHubs { - v.Stop() + + for webCon := range h.connections { + webCon.WebSocket.Close() } + return } } }() } + +func shouldSendEvent(webCon *WebConn, msg *model.Message) bool { + + if webCon.UserId == msg.UserId { + // Don't need to tell the user they are typing + if msg.Action == model.ACTION_TYPING { + return false + } + + // We have to make sure the user is in the channel. Otherwise system messages that + // post about users in channels they are not in trigger warnings. + if len(msg.ChannelId) > 0 { + allowed := webCon.HasPermissionsToChannel(msg.ChannelId) + + if !allowed { + return false + } + } + } else { + // Don't share a user's view or preference events with other users + if msg.Action == model.ACTION_CHANNEL_VIEWED { + return false + } else if msg.Action == model.ACTION_PREFERENCE_CHANGED { + return false + } else if msg.Action == model.ACTION_EPHEMERAL_MESSAGE { + // For now, ephemeral messages are sent directly to individual users + return false + } + + // Only report events to users who are in the team for the event + if len(msg.TeamId) > 0 { + allowed := webCon.HasPermissionsToTeam(msg.TeamId) + + if !allowed { + return false + } + } + + // Only report events to users who are in the channel for the event + if len(msg.ChannelId) > 0 { + allowed := webCon.HasPermissionsToChannel(msg.ChannelId) + + if !allowed { + return false + } + } + } + + return true +} diff --git a/api/web_socket.go b/api/web_socket.go index e15732f43..72a9c61a6 100644 --- a/api/web_socket.go +++ b/api/web_socket.go @@ -5,16 +5,15 @@ package api import ( l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "net/http" ) -func InitWebSocket(r *mux.Router) { +func InitWebSocket() { l4g.Debug(utils.T("api.web_socket.init.debug")) - r.Handle("/websocket", ApiUserRequiredTrustRequester(connect)).Methods("GET") + BaseRoutes.Users.Handle("/websocket", ApiUserRequiredTrustRequester(connect)).Methods("GET") hub.Start() } @@ -34,7 +33,7 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) { return } - wc := NewWebConn(ws, c.Session.TeamId, c.Session.UserId, c.Session.Id) + wc := NewWebConn(ws, c.Session.UserId, c.Session.Id) hub.Register(wc) go wc.writePump() wc.readPump() diff --git a/api/web_socket_test.go b/api/web_socket_test.go index 2c0ac61eb..7cb04e93e 100644 --- a/api/web_socket_test.go +++ b/api/web_socket_test.go @@ -6,7 +6,6 @@ package api import ( "github.com/gorilla/websocket" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" "testing" @@ -14,22 +13,14 @@ import ( ) func TestSocket(t *testing.T) { - Setup() + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + channel1 := th.BasicChannel + channel2 := th.CreateChannel(Client, team) + Client.Must(Client.AddChannelMember(channel1.Id, th.BasicUser2.Id)) - url := "ws://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/websocket" - 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) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "Test Web Scoket 1", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - channel2 := &model.Channel{DisplayName: "Test Web Scoket 2", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + url := "ws://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/users/websocket" header1 := http.Header{} header1.Set(model.HEADER_AUTH, "BEARER "+Client.AuthToken) @@ -39,10 +30,7 @@ func TestSocket(t *testing.T) { t.Fatal(err) } - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - Client.LoginByEmail(team.Name, user2.Email, "pwd") + th.LoginBasic2() header2 := http.Header{} header2.Set(model.HEADER_AUTH, "BEARER "+Client.AuthToken) @@ -53,21 +41,11 @@ func TestSocket(t *testing.T) { } time.Sleep(300 * time.Millisecond) - Client.Must(Client.JoinChannel(channel1.Id)) - // Read the user_added message that gets generated var rmsg model.Message - if err := c2.ReadJSON(&rmsg); err != nil { - t.Fatal(err) - } - - // Read the second user_added message that gets generated - if err := c2.ReadJSON(&rmsg); err != nil { - t.Fatal(err) - } // Test sending message without a channelId - m := model.NewMessage("", "", "", model.ACTION_TYPING) + m := model.NewMessage(team.Id, "", "", model.ACTION_TYPING) m.Add("RootId", model.NewId()) m.Add("ParentId", model.NewId()) @@ -77,6 +55,8 @@ func TestSocket(t *testing.T) { t.Fatal(err) } + t.Log(rmsg.ToJson()) + if team.Id != rmsg.TeamId { t.Fatal("Ids do not match") } @@ -86,7 +66,7 @@ func TestSocket(t *testing.T) { } // Test sending messsage to Channel you have access to - m = model.NewMessage("", channel1.Id, "", model.ACTION_TYPING) + m = model.NewMessage(team.Id, channel1.Id, "", model.ACTION_TYPING) m.Add("RootId", model.NewId()) m.Add("ParentId", model.NewId()) diff --git a/api/web_team_hub.go b/api/web_team_hub.go deleted file mode 100644 index 9d1c56f15..000000000 --- a/api/web_team_hub.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - l4g "github.com/alecthomas/log4go" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" -) - -type TeamHub struct { - connections map[*WebConn]bool - broadcast chan *model.Message - register chan *WebConn - unregister chan *WebConn - stop chan bool - teamId string -} - -func NewTeamHub(teamId string) *TeamHub { - return &TeamHub{ - broadcast: make(chan *model.Message), - register: make(chan *WebConn), - unregister: make(chan *WebConn), - connections: make(map[*WebConn]bool), - stop: make(chan bool), - teamId: teamId, - } -} - -func (h *TeamHub) Register(webConn *WebConn) { - h.register <- webConn -} - -func (h *TeamHub) Unregister(webConn *WebConn) { - h.unregister <- webConn -} - -func (h *TeamHub) Stop() { - h.stop <- true -} - -func (h *TeamHub) Start() { - go func() { - for { - select { - case webCon := <-h.register: - h.connections[webCon] = true - case webCon := <-h.unregister: - if _, ok := h.connections[webCon]; ok { - delete(h.connections, webCon) - close(webCon.Send) - } - case msg := <-h.broadcast: - for webCon := range h.connections { - if ShouldSendEvent(webCon, msg) { - select { - case webCon.Send <- msg: - default: - close(webCon.Send) - delete(h.connections, webCon) - } - } - } - case s := <-h.stop: - if s { - - l4g.Debug(utils.T("api.web_team_hun.start.debug"), h.teamId) - - for webCon := range h.connections { - webCon.WebSocket.Close() - } - - return - } - } - } - }() -} - -func (h *TeamHub) UpdateChannelAccessCache(userId string, channelId string) { - for webCon := range h.connections { - if webCon.UserId == userId { - webCon.updateChannelAccessCache(channelId) - break - } - } -} - -func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool { - - if webCon.UserId == msg.UserId { - // Don't need to tell the user they are typing - if msg.Action == model.ACTION_TYPING { - return false - } - } else { - // Don't share a user's view or preference events with other users - if msg.Action == model.ACTION_CHANNEL_VIEWED { - return false - } else if msg.Action == model.ACTION_PREFERENCE_CHANGED { - return false - } else if msg.Action == model.ACTION_EPHEMERAL_MESSAGE { - // For now, ephemeral messages are sent directly to individual users - return false - } - - // Only report events to a user who is the subject of the event, or is in the channel of the event - if len(msg.ChannelId) > 0 { - allowed, ok := webCon.ChannelAccessCache[msg.ChannelId] - if !ok { - allowed = webCon.updateChannelAccessCache(msg.ChannelId) - } - - if !allowed { - return false - } - } - } - - return true -} diff --git a/api/webhook.go b/api/webhook.go index fe1aa1175..ea628e39c 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -14,20 +14,19 @@ import ( "github.com/mattermost/platform/utils" ) -func InitWebhook(r *mux.Router) { +func InitWebhook() { l4g.Debug(utils.T("api.webhook.init.debug")) - sr := r.PathPrefix("/hooks").Subrouter() - sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST") - sr.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST") - sr.Handle("/incoming/list", ApiUserRequired(getIncomingHooks)).Methods("GET") + BaseRoutes.Hooks.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST") + BaseRoutes.Hooks.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST") + BaseRoutes.Hooks.Handle("/incoming/list", ApiUserRequired(getIncomingHooks)).Methods("GET") - sr.Handle("/outgoing/create", ApiUserRequired(createOutgoingHook)).Methods("POST") - sr.Handle("/outgoing/regen_token", ApiUserRequired(regenOutgoingHookToken)).Methods("POST") - sr.Handle("/outgoing/delete", ApiUserRequired(deleteOutgoingHook)).Methods("POST") - sr.Handle("/outgoing/list", ApiUserRequired(getOutgoingHooks)).Methods("GET") + BaseRoutes.Hooks.Handle("/outgoing/create", ApiUserRequired(createOutgoingHook)).Methods("POST") + BaseRoutes.Hooks.Handle("/outgoing/regen_token", ApiUserRequired(regenOutgoingHookToken)).Methods("POST") + BaseRoutes.Hooks.Handle("/outgoing/delete", ApiUserRequired(deleteOutgoingHook)).Methods("POST") + BaseRoutes.Hooks.Handle("/outgoing/list", ApiUserRequired(getOutgoingHooks)).Methods("GET") - sr.Handle("/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST") + BaseRoutes.Hooks.Handle("/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST") // Old route. Remove eventually. mr := Srv.Router @@ -59,10 +58,10 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { } cchan := Srv.Store.Channel().Get(hook.ChannelId) - pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, hook.ChannelId, c.Session.UserId) + pchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, hook.ChannelId, c.Session.UserId) hook.UserId = c.Session.UserId - hook.TeamId = c.Session.TeamId + hook.TeamId = c.TeamId var channel *model.Channel if result := <-cchan; result.Err != nil { @@ -73,7 +72,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { } if !c.HasPermissionsToChannel(pchan, "createIncomingHook") { - if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId { + if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.TeamId { c.LogAudit("fail - bad channel permissions") return } @@ -149,7 +148,7 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -183,11 +182,11 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } hook.CreatorId = c.Session.UserId - hook.TeamId = c.Session.TeamId + hook.TeamId = c.TeamId if len(hook.ChannelId) != 0 { cchan := Srv.Store.Channel().Get(hook.ChannelId) - pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, hook.ChannelId, c.Session.UserId) + pchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, hook.ChannelId, c.Session.UserId) var channel *model.Channel if result := <-cchan; result.Err != nil { @@ -199,11 +198,14 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { if channel.Type != model.CHANNEL_OPEN { c.LogAudit("fail - not open channel") + c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.not_open.app_error", nil, "") + return } if !c.HasPermissionsToChannel(pchan, "createOutgoingHook") { - if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId { + if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.TeamId { c.LogAudit("fail - bad channel permissions") + c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.permissions.app_error", nil, "") return } } @@ -237,7 +239,7 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -292,7 +294,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { + if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return @@ -323,7 +325,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) } else { hook = result.Data.(*model.OutgoingWebhook) - if c.Session.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() { + if c.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() { c.LogAudit("fail - inappropriate permissions") c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId) return @@ -398,7 +400,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { if len(channelName) != 0 { if channelName[0] == '@' { - if result := <-Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { + if result := <-Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) return } else { @@ -426,7 +428,11 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { pchan := 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} + c.Session = model.Session{ + UserId: hook.UserId, + TeamMembers: []*model.TeamMember{{TeamId: hook.TeamId, UserId: hook.UserId}}, + IsOAuth: false, + } if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") diff --git a/api/webhook_test.go b/api/webhook_test.go index 4f85d178d..5198056cc 100644 --- a/api/webhook_test.go +++ b/api/webhook_test.go @@ -4,416 +4,599 @@ package api import ( + "fmt" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "testing" "time" ) func TestCreateIncomingHook(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + user := th.SystemAdminUser + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + channel2 := th.CreatePrivateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks - enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook := &model.IncomingWebhook{ChannelId: channel1.Id} - 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)) + var rhook *model.IncomingWebhook + if result, err := Client.CreateIncomingWebhook(hook); err != nil { + t.Fatal(err) + } else { + rhook = result.Data.(*model.IncomingWebhook) + } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if hook.ChannelId != rhook.ChannelId { + t.Fatal("channel ids didn't match") + } - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if rhook.UserId != user.Id { + t.Fatal("user ids didn't match") + } - channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + if rhook.TeamId != team.Id { + t.Fatal("team ids didn't match") + } - hook := &model.IncomingWebhook{ChannelId: channel1.Id} + hook = &model.IncomingWebhook{ChannelId: "junk"} + if _, err := Client.CreateIncomingWebhook(hook); err == nil { + t.Fatal("should have failed - bad channel id") + } - if utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - var rhook *model.IncomingWebhook - if result, err := Client.CreateIncomingWebhook(hook); err != nil { - t.Fatal(err) - } else { - rhook = result.Data.(*model.IncomingWebhook) + hook = &model.IncomingWebhook{ChannelId: channel2.Id, UserId: "123", TeamId: "456"} + if result, err := Client.CreateIncomingWebhook(hook); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.IncomingWebhook).UserId != user.Id { + t.Fatal("bad user id wasn't overwritten") } - - if hook.ChannelId != rhook.ChannelId { - t.Fatal("channel ids didn't match") + if result.Data.(*model.IncomingWebhook).TeamId != team.Id { + t.Fatal("bad team id wasn't overwritten") } + } - if rhook.UserId != user.Id { - t.Fatal("user ids didn't match") - } + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - if rhook.TeamId != team.Id { - t.Fatal("team ids didn't match") - } + hook = &model.IncomingWebhook{ChannelId: channel1.Id} - hook = &model.IncomingWebhook{ChannelId: "junk"} - if _, err := Client.CreateIncomingWebhook(hook); err == nil { - t.Fatal("should have failed - bad channel id") - } + if _, err := Client.CreateIncomingWebhook(hook); err == nil { + t.Fatal("should have failed - not system/team admin") + } - hook = &model.IncomingWebhook{ChannelId: channel2.Id, UserId: "123", TeamId: "456"} - if result, err := Client.CreateIncomingWebhook(hook); err != nil { - t.Fatal(err) - } else { - if result.Data.(*model.IncomingWebhook).UserId != user.Id { - t.Fatal("bad user id wasn't overwritten") - } - if result.Data.(*model.IncomingWebhook).TeamId != team.Id { - t.Fatal("bad team id wasn't overwritten") - } - } - } else { - if _, err := Client.CreateIncomingWebhook(hook); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + if _, err := Client.CreateIncomingWebhook(hook); err != nil { + t.Fatal(err) + } + + hook = &model.IncomingWebhook{ChannelId: channel2.Id} + + if _, err := Client.CreateIncomingWebhook(hook); err == nil { + t.Fatal("should have failed - channel is private and not a member") + } + + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = false + + if _, err := Client.CreateIncomingWebhook(hook); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestListIncomingHooks(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks - enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook1 := &model.IncomingWebhook{ChannelId: channel1.Id} + hook1 = Client.Must(Client.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook) - 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)) + hook2 := &model.IncomingWebhook{ChannelId: channel1.Id} + hook2 = Client.Must(Client.CreateIncomingWebhook(hook2)).Data.(*model.IncomingWebhook) - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if result, err := Client.ListIncomingWebhooks(); err != nil { + t.Fatal(err) + } else { + hooks := result.Data.([]*model.IncomingWebhook) - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if len(hooks) != 2 { + t.Fatal("incorrect number of hooks") + } + } - if utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - hook1 := &model.IncomingWebhook{ChannelId: channel1.Id} - hook1 = Client.Must(Client.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook) + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - hook2 := &model.IncomingWebhook{ChannelId: channel1.Id} - hook2 = Client.Must(Client.CreateIncomingWebhook(hook2)).Data.(*model.IncomingWebhook) + if _, err := Client.ListIncomingWebhooks(); err == nil { + t.Fatal("should have errored - not system/team admin") + } - if result, err := Client.ListIncomingWebhooks(); err != nil { - t.Fatal(err) - } else { - hooks := result.Data.([]*model.IncomingWebhook) + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false - if len(hooks) != 2 { - t.Fatal("incorrect number of hooks") - } - } - } else { - if _, err := Client.ListIncomingWebhooks(); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + if _, err := Client.ListIncomingWebhooks(); err != nil { + t.Fatal(err) + } + + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = false + + if _, err := Client.ListIncomingWebhooks(); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestDeleteIncomingHook(t *testing.T) { - Setup() + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks - enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true - utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook := &model.IncomingWebhook{ChannelId: channel1.Id} + hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) - 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 _, err := Client.DeleteIncomingWebhook(hook.Id); err != nil { + t.Fatal(err) + } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if _, err := Client.DeleteIncomingWebhook("junk"); err == nil { + t.Fatal("should have failed - bad id") + } - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if _, err := Client.DeleteIncomingWebhook(""); err == nil { + t.Fatal("should have failed - empty id") + } - if utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - hook := &model.IncomingWebhook{ChannelId: channel1.Id} - hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) + hooks := Client.Must(Client.ListIncomingWebhooks()).Data.([]*model.IncomingWebhook) + if len(hooks) != 0 { + t.Fatal("delete didn't work properly") + } - data := make(map[string]string) - data["id"] = hook.Id + hook = &model.IncomingWebhook{ChannelId: channel1.Id} + hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) - if _, err := Client.DeleteIncomingWebhook(data); err != nil { - t.Fatal(err) - } + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - hooks := Client.Must(Client.ListIncomingWebhooks()).Data.([]*model.IncomingWebhook) - if len(hooks) != 0 { - t.Fatal("delete didn't work properly") - } - } else { - data := make(map[string]string) - data["id"] = "123" + if _, err := Client.DeleteIncomingWebhook(hook.Id); err == nil { + t.Fatal("should have failed - not system/team admin") + } - if _, err := Client.DeleteIncomingWebhook(data); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + if _, err := Client.DeleteIncomingWebhook(hook.Id); err == nil { + t.Fatal("should have failed - not creator or team admin") + } + + hook = &model.IncomingWebhook{ChannelId: channel1.Id} + hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) + + if _, err := Client.DeleteIncomingWebhook(hook.Id); err != nil { + t.Fatal(err) + } + + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = false + + if _, err := Client.DeleteIncomingWebhook(hook.Id); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestCreateOutgoingHook(t *testing.T) { - Setup() - enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + user := th.SystemAdminUser + team := th.SystemAdminTeam + team2 := th.CreateTeam(Client) + channel1 := th.CreateChannel(Client, team) + channel2 := th.CreatePrivateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + user3 := th.CreateUser(Client) + LinkUserToTeam(user3, team2) + enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} - 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)) + var rhook *model.OutgoingWebhook + if result, err := Client.CreateOutgoingWebhook(hook); err != nil { + t.Fatal(err) + } else { + rhook = result.Data.(*model.OutgoingWebhook) + } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if hook.ChannelId != rhook.ChannelId { + t.Fatal("channel ids didn't match") + } - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if rhook.CreatorId != user.Id { + t.Fatal("user ids didn't match") + } - channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + if rhook.TeamId != team.Id { + t.Fatal("team ids didn't match") + } - hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = &model.OutgoingWebhook{ChannelId: "junk", CallbackURLs: []string{"http://nowhere.com"}} + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have failed - bad channel id") + } - if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - var rhook *model.OutgoingWebhook - if result, err := Client.CreateOutgoingWebhook(hook); err != nil { - t.Fatal(err) - } else { - rhook = result.Data.(*model.OutgoingWebhook) + hook = &model.OutgoingWebhook{ChannelId: channel1.Id, CreatorId: "123", TeamId: "456", CallbackURLs: []string{"http://nowhere.com"}} + if result, err := Client.CreateOutgoingWebhook(hook); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.OutgoingWebhook).CreatorId != user.Id { + t.Fatal("bad user id wasn't overwritten") } - - if hook.ChannelId != rhook.ChannelId { - t.Fatal("channel ids didn't match") + if result.Data.(*model.OutgoingWebhook).TeamId != team.Id { + t.Fatal("bad team id wasn't overwritten") } + } - if rhook.CreatorId != user.Id { - t.Fatal("user ids didn't match") - } + hook = &model.OutgoingWebhook{ChannelId: channel2.Id, CallbackURLs: []string{"http://nowhere.com"}} + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have failed - private channel") + } - if rhook.TeamId != team.Id { - t.Fatal("team ids didn't match") - } + hook = &model.OutgoingWebhook{CallbackURLs: []string{"http://nowhere.com"}} + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have failed - blank channel and trigger words") + } - hook = &model.OutgoingWebhook{ChannelId: "junk", CallbackURLs: []string{"http://nowhere.com"}} - if _, err := Client.CreateOutgoingWebhook(hook); err == nil { - t.Fatal("should have failed - bad channel id") - } + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - hook = &model.OutgoingWebhook{ChannelId: channel2.Id, CreatorId: "123", TeamId: "456", CallbackURLs: []string{"http://nowhere.com"}} - if result, err := Client.CreateOutgoingWebhook(hook); err != nil { - t.Fatal(err) - } else { - if result.Data.(*model.OutgoingWebhook).CreatorId != user.Id { - t.Fatal("bad user id wasn't overwritten") - } - if result.Data.(*model.OutgoingWebhook).TeamId != team.Id { - t.Fatal("bad team id wasn't overwritten") - } - } - } else { - if _, err := Client.CreateOutgoingWebhook(hook); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + hook = &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have failed - not system/team admin") + } + + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + if _, err := Client.CreateOutgoingWebhook(hook); err != nil { + t.Fatal(err) + } + + Client.Logout() + Client.Must(Client.LoginById(user3.Id, user3.Password)) + Client.SetTeamId(team2.Id) + + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have failed - wrong team") + } + + utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = false + + if _, err := Client.CreateOutgoingWebhook(hook); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestListOutgoingHooks(t *testing.T) { - Setup() - enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook1 := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook1 = Client.Must(Client.CreateOutgoingWebhook(hook1)).Data.(*model.OutgoingWebhook) - 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)) + hook2 := &model.OutgoingWebhook{TriggerWords: []string{"trigger"}, CallbackURLs: []string{"http://nowhere.com"}} + hook2 = Client.Must(Client.CreateOutgoingWebhook(hook2)).Data.(*model.OutgoingWebhook) - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if result, err := Client.ListOutgoingWebhooks(); err != nil { + t.Fatal(err) + } else { + hooks := result.Data.([]*model.OutgoingWebhook) - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if len(hooks) != 2 { + t.Fatal("incorrect number of hooks") + } + } - if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - hook1 := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} - hook1 = Client.Must(Client.CreateOutgoingWebhook(hook1)).Data.(*model.OutgoingWebhook) + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - hook2 := &model.OutgoingWebhook{TriggerWords: []string{"trigger"}, CallbackURLs: []string{"http://nowhere.com"}} - hook2 = Client.Must(Client.CreateOutgoingWebhook(hook2)).Data.(*model.OutgoingWebhook) + if _, err := Client.ListOutgoingWebhooks(); err == nil { + t.Fatal("should have failed - not system/team admin") + } - if result, err := Client.ListOutgoingWebhooks(); err != nil { - t.Fatal(err) - } else { - hooks := result.Data.([]*model.OutgoingWebhook) + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false - if len(hooks) != 2 { - t.Fatal("incorrect number of hooks") - } - } - } else { - if _, err := Client.ListOutgoingWebhooks(); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + if _, err := Client.ListOutgoingWebhooks(); err != nil { + t.Fatal(err) + } + + utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = false + + if _, err := Client.ListOutgoingWebhooks(); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestDeleteOutgoingHook(t *testing.T) { - Setup() - enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) - 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 _, err := Client.DeleteOutgoingWebhook("junk"); err == nil { + t.Fatal("should have failed - bad hook id") + } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if _, err := Client.DeleteOutgoingWebhook(""); err == nil { + t.Fatal("should have failed - empty hook id") + } - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if _, err := Client.DeleteOutgoingWebhook(hook.Id); err != nil { + t.Fatal(err) + } - if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} - hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) + hooks := Client.Must(Client.ListOutgoingWebhooks()).Data.([]*model.OutgoingWebhook) + if len(hooks) != 0 { + t.Fatal("delete didn't work properly") + } - data := make(map[string]string) - data["id"] = hook.Id + hook = &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) - if _, err := Client.DeleteOutgoingWebhook(data); err != nil { - t.Fatal(err) - } + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - hooks := Client.Must(Client.ListOutgoingWebhooks()).Data.([]*model.OutgoingWebhook) - if len(hooks) != 0 { - t.Fatal("delete didn't work properly") - } - } else { - data := make(map[string]string) - data["id"] = "123" + if _, err := Client.DeleteOutgoingWebhook(hook.Id); err == nil { + t.Fatal("should have failed - not system/team admin") + } - if _, err := Client.DeleteOutgoingWebhook(data); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + if _, err := Client.DeleteOutgoingWebhook(hook.Id); err == nil { + t.Fatal("should have failed - not creator or team admin") + } + + hook = &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) + + if _, err := Client.DeleteOutgoingWebhook(hook.Id); err != nil { + t.Fatal(err) + } + + utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = false + + if _, err := Client.DeleteOutgoingWebhook(hook.Id); err == nil { + t.Fatal("should have errored - webhooks turned off") } } func TestRegenOutgoingHookToken(t *testing.T) { - Setup() - enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + team2 := th.CreateTeam(Client) + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + user3 := th.CreateUser(Client) + LinkUserToTeam(user3, team2) + enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations defer func() { - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks }() - utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true - 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) + hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) - 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 _, err := Client.RegenOutgoingWebhookToken("junk"); err == nil { + t.Fatal("should have failed - bad id") + } - c := &Context{} - c.RequestId = model.NewId() - c.IpAddress = "cmd_line" - UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + if _, err := Client.RegenOutgoingWebhookToken(""); err == nil { + t.Fatal("should have failed - empty id") + } - channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + if result, err := Client.RegenOutgoingWebhookToken(hook.Id); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.OutgoingWebhook).Token == hook.Token { + t.Fatal("regen didn't work properly") + } + } - if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} - hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) + Client.Logout() + Client.Must(Client.LoginById(user2.Id, user2.Password)) + Client.SetTeamId(team.Id) - data := make(map[string]string) - data["id"] = hook.Id + if _, err := Client.RegenOutgoingWebhookToken(hook.Id); err == nil { + t.Fatal("should have failed - not system/team admin") + } - if result, err := Client.RegenOutgoingWebhookToken(data); err != nil { - t.Fatal(err) - } else { - if result.Data.(*model.OutgoingWebhook).Token == hook.Token { - t.Fatal("regen didn't work properly") - } - } + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false - } else { - data := make(map[string]string) - data["id"] = "123" + hook = &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}} + hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook) - if _, err := Client.RegenOutgoingWebhookToken(data); err == nil { - t.Fatal("should have errored - webhooks turned off") - } + if _, err := Client.RegenOutgoingWebhookToken(hook.Id); err != nil { + t.Fatal(err) + } + + Client.Logout() + Client.Must(Client.LoginById(user3.Id, user3.Password)) + Client.SetTeamId(team2.Id) + + if _, err := Client.RegenOutgoingWebhookToken(hook.Id); err == nil { + t.Fatal("should have failed - wrong team") + } + + utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = false + + if _, err := Client.RegenOutgoingWebhookToken(hook.Id); err == nil { + t.Fatal("should have errored - webhooks turned off") + } +} +func TestIncomingWebhooks(t *testing.T) { + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + team := th.SystemAdminTeam + channel1 := th.CreateChannel(Client, team) + user2 := th.CreateUser(Client) + LinkUserToTeam(user2, team) + + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + defer func() { + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks + }() + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true + + hook := &model.IncomingWebhook{ChannelId: channel1.Id} + hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook) + + url := "/hooks/" + hook.Id + + if _, err := Client.DoPost(url, "{\"text\":\"this is a test\"}", "application/json"); err != nil { + t.Fatal(err) + } + + if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", channel1.Name), "application/json"); err != nil { + t.Fatal(err) + } + + if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", channel1.Name), "application/json"); err != nil { + t.Fatal(err) + } + + Client.Must(Client.CreateDirectChannel(user2.Id)) + + if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", user2.Username), "application/json"); err != nil { + t.Fatal(err) + } + + if _, err := Client.DoPost(url, "payload={\"text\":\"this is a test\"}", "application/x-www-form-urlencoded"); err != nil { + t.Fatal(err) + } + + attachmentPayload := `{ + "text": "this is a test", + "attachments": [ + { + "fallback": "Required plain-text summary of the attachment.", + + "color": "#36a64f", + + "pretext": "Optional text that appears above the attachment block", + + "author_name": "Bobby Tables", + "author_link": "http://flickr.com/bobby/", + "author_icon": "http://flickr.com/icons/bobby.jpg", + + "title": "Slack API Documentation", + "title_link": "https://api.slack.com/", + + "text": "Optional text that appears within the attachment", + + "fields": [ + { + "title": "Priority", + "value": "High", + "short": false + } + ], + + "image_url": "http://my-website.com/path/to/image.jpg", + "thumb_url": "http://example.com/path/to/thumb.png" + } + ] + }` + + if _, err := Client.DoPost(url, attachmentPayload, "application/json"); err != nil { + t.Fatal(err) + } + + if _, err := Client.DoPost(url, "{\"text\":\"\"}", "application/json"); err == nil { + t.Fatal("should have failed - no text") + } + + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = false + + if _, err := Client.DoPost(url, "{\"text\":\"this is a test\"}", "application/json"); err == nil { + t.Fatal("should have failed - webhooks turned off") } } diff --git a/config/config.json b/config/config.json index 7f3085950..3fe938fe2 100644 --- a/config/config.json +++ b/config/config.json @@ -30,9 +30,9 @@ "MaxUsersPerTeam": 50, "EnableTeamCreation": true, "EnableUserCreation": true, + "EnableOpenServer": false, "RestrictCreationToDomains": "", "RestrictTeamNames": true, - "EnableTeamListing": false, "EnableCustomBrand": false, "CustomBrandText": "" }, @@ -143,6 +143,7 @@ "LastNameAttribute": "", "EmailAttribute": "", "UsernameAttribute": "", + "NicknameAttribute": "", "IdAttribute": "", "SkipCertificateVerification": false, "QueryTimeout": 60, @@ -154,4 +155,4 @@ "Directory": "./data/", "EnableDaily": false } -} +} \ No newline at end of file diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 3917b42f8..25d591ce2 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -8,10 +8,10 @@ import ( ) type LdapInterface interface { - DoLogin(team *model.Team, id string, password string) (*model.User, *model.AppError) + DoLogin(id string, password string) (*model.User, *model.AppError) GetUser(id string) (*model.User, *model.AppError) CheckPassword(id string, password string) *model.AppError - SwitchToEmail(userId, ldapId, ldapPassword string) *model.AppError + SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError ValidateFilter(filter string) *model.AppError } diff --git a/einterfaces/mfa.go b/einterfaces/mfa.go index 0703fb766..25f3ed913 100644 --- a/einterfaces/mfa.go +++ b/einterfaces/mfa.go @@ -8,7 +8,7 @@ import ( ) type MfaInterface interface { - GenerateQrCode(team *model.Team, user *model.User) ([]byte, *model.AppError) + GenerateQrCode(user *model.User) ([]byte, *model.AppError) Activate(user *model.User, token string) *model.AppError Deactivate(userId string) *model.AppError ValidateToken(secret, token string) (bool, *model.AppError) diff --git a/i18n/en.json b/i18n/en.json index a135e04be..990db5277 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1313,11 +1313,11 @@ }, { "id": "api.templates.singin_change_email.body.info", - "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .TeamURL }} to {{.Method}}.
If this change wasn't initiated by you, please contact your system administrator." + "translation": "You updated your sign-in method for on {{ .SiteName }} to {{.Method}}.
If this change wasn't initiated by you, please contact your system administrator." }, { "id": "api.templates.singin_change_email.subject", - "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .SiteName }}" + "translation": "You updated your sign-in method on {{ .SiteName }}" }, { "id": "api.templates.verify_body.button", @@ -1425,11 +1425,11 @@ }, { "id": "api.user.create_oauth_user.already_attached.app_error", - "translation": "Team {{.DisplayName}} already has a user with the email address attached to your {{.Service}} account" + "translation": "An existing user is already attached to your {{.Service}} account" }, { "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "This {{.Service}} account has already been used to sign up for team {{.DisplayName}}" + "translation": "This {{.Service}} account has already been used to sign up" }, { "id": "api.user.create_oauth_user.create.app_error", @@ -1451,6 +1451,10 @@ "id": "api.user.create_profile_image.initial.app_error", "translation": "Could not add user initial to default profile picture" }, + { + "id": "api.user.create_user.no_open_server", + "translation": "This server does not allow open signups. Please speak with your Administrator to receive an invitation." + }, { "id": "api.user.create_user.accepted_domain.app_error", "translation": "The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email." @@ -1759,6 +1763,14 @@ "id": "api.webhook.create_incoming.disabled.app_errror", "translation": "Incoming webhooks have been disabled by the system admin." }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Outgoing webhooks can only be created for public channels." + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Inappropriate permissions to create outcoming webhook." + }, { "id": "api.webhook.create_outgoing.disabled.app_error", "translation": "Outgoing webhooks have been disabled by the system admin." @@ -2111,6 +2123,18 @@ "id": "model.channel.is_valid.type.app_error", "translation": "Invalid type" }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.team_member.is_valid.role.app_error", + "translation": "Invalid role" + }, { "id": "model.channel.is_valid.update_at.app_error", "translation": "Update at must be a valid time" @@ -2943,6 +2967,22 @@ "id": "store.sql_channel.update_member.app_error", "translation": "We encountered an error updating the channel member" }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "A team member with that id already exists" + }, + { + "id": "store.sql_team.save_member.save.app_error", + "translation": "We couldn't save the team member" + }, + { + "id": "store.sql_team.get_members.app_error", + "translation": "We couldn't get the team members" + }, + { + "id": "store.sql_team.remove_member.app_error", + "translation": "We couldn't remove the team member" + }, { "id": "store.sql_command.analytics_command_count.app_error", "translation": "We couldn't count the commands" @@ -3237,7 +3277,7 @@ }, { "id": "store.sql_session.remove_all_sessions_for_team.app_error", - "translation": "We couldn't remove all the sessions for the team" + "translation": "We couldn't remove all the sessions" }, { "id": "store.sql_session.save.app_error", diff --git a/i18n/es.json b/i18n/es.json index 7686e2b88..26378b148 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -1311,14 +1311,6 @@ "id": "api.templates.signup_team_subject", "translation": "Configuración del equipo en {{ .SiteName }}" }, - { - "id": "api.templates.singin_change_email.body.info", - "translation": "Haz actualizado el método con el que inicias sesión en {{.TeamURL}} para el equipo {{.TeamDisplayName}} por {{.Method}}.
Si este cambio no fue realizado por ti, por favor contacta a un administrador del sistema." - }, - { - "id": "api.templates.singin_change_email.subject", - "translation": "Cambio del método de inicio de sesión para {{.TeamDisplayName}} en {{ .SiteName }}" - }, { "id": "api.templates.verify_body.button", "translation": "Confirmar Correo" @@ -1423,14 +1415,6 @@ "id": "api.user.complete_switch_with_oauth.unavailable.app_error", "translation": "OAuth para {{.Service}} no está disponible en este servidor" }, - { - "id": "api.user.create_oauth_user.already_attached.app_error", - "translation": "El Equipo {{.DisplayName}} ya tiene un usuario con esta dirección de correo asociada a tu cuenta de {{.Service}}" - }, - { - "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "Esta cuenta de {{.Service}} ya fue utilizada para registrarse en el equipo {{.DisplayName}}" - }, { "id": "api.user.create_oauth_user.create.app_error", "translation": "No se pudo crear el usuario basandose en el objeto de {{.Service}}" @@ -3235,10 +3219,6 @@ "id": "store.sql_session.remove.app_error", "translation": "No pudimos remover la sesión" }, - { - "id": "store.sql_session.remove_all_sessions_for_team.app_error", - "translation": "No pudimos remover todas las sesiones para el equipo" - }, { "id": "store.sql_session.save.app_error", "translation": "No pudimos guardar la sesión" diff --git a/i18n/fr.json b/i18n/fr.json index eaf16fbdd..488b49b69 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1235,14 +1235,6 @@ "id": "api.templates.signup_team_subject", "translation": "Paramétrage Équipe {{ .SiteName }}" }, - { - "id": "api.templates.singin_change_email.body.info", - "translation": "Vous avez mis à jour la méthode de connexion de {{.TeamDisplayName}} sur {{.TeamURL}} pour {{.Method}}.
Si cette modification n'a pas été effectuée par vous, veuillez prendre contact avec votre administrateur système." - }, - { - "id": "api.templates.singin_change_email.subject", - "translation": "Vous avez mis à jour la méthode de connexion de {{.TeamDisplayName}} sur {{ .SiteName }}" - }, { "id": "api.templates.verify_body.button", "translation": "Vérifier l'adresse électronique" @@ -1339,14 +1331,6 @@ "id": "api.user.complete_switch_with_oauth.unavailable.app_error", "translation": "{{.Service}} oauth non disponible sur ce serveur" }, - { - "id": "api.user.create_oauth_user.already_attached.app_error", - "translation": "L'équipe {{.DisplayName}} dispose déjà d'un utilisateur ayant la même adresse électronique que celle attachée à votre compte {{.Service}}" - }, - { - "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "Ce compte {{.Service}} a déjà été utilisé pour pour se connecter à l'équipe {{.DisplayName}}" - }, { "id": "api.user.create_oauth_user.create.app_error", "translation": "Impossible de créer un utilisateur à partir du user object {{.Service}}" diff --git a/i18n/pt.json b/i18n/pt.json index b9e0c49c9..635e254d1 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -1311,14 +1311,6 @@ "id": "api.templates.signup_team_subject", "translation": "{{ .SiteName }} Configuração da Equipe" }, - { - "id": "api.templates.singin_change_email.body.info", - "translation": "Você atualizou seu método de login para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.
Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema." - }, - { - "id": "api.templates.singin_change_email.subject", - "translation": "Você atualizou seu método de login para {{.TeamDisplayName}} em {{ .SiteName }}" - }, { "id": "api.templates.verify_body.button", "translation": "Verificar Email" @@ -1423,14 +1415,6 @@ "id": "api.user.complete_switch_with_oauth.unavailable.app_error", "translation": "{{.Service}} oauth não disponível neste servidor" }, - { - "id": "api.user.create_oauth_user.already_attached.app_error", - "translation": "Equipe {{.DisplayName}} já tem um usuário com o endereço de email anexado a sua conta {{.Service}}" - }, - { - "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "Está conta {{.Service}} já foi utilizada para se inscrever na equipe {{.DisplayName}}" - }, { "id": "api.user.create_oauth_user.create.app_error", "translation": "Não foi possível criar o usuário fora do {{.Service}} do objeto de usuário" @@ -3235,10 +3219,6 @@ "id": "store.sql_session.remove.app_error", "translation": "Não foi possível remover a sessão" }, - { - "id": "store.sql_session.remove_all_sessions_for_team.app_error", - "translation": "Não foi possível remover todas as sessões para a equipe" - }, { "id": "store.sql_session.save.app_error", "translation": "Não foi possível salvar a sessão" diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index 2f1096fd5..77a5b89c8 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -66,7 +66,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { team := &model.Team{ DisplayName: teamDisplayName[0], Name: utils.RandomName(utils.Range{20, 20}, utils.LOWERCASE), - Email: utils.RandomEmail(utils.Range{20, 20}, utils.LOWERCASE), + Email: "success+" + model.NewId() + "simulator.amazonses.com", Type: model.TEAM_OPEN, } @@ -88,8 +88,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { // Create user for testing user := &model.User{ - TeamId: teamID, - Email: utils.RandomEmail(utils.Range{20, 20}, utils.LOWERCASE), + Email: "success+" + model.NewId() + "simulator.amazonses.com", Nickname: username[0], Password: api.USER_PASSWORD} @@ -98,7 +97,10 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { c.Err = err return } - api.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) + + <-api.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) + <-api.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: result.Data.(*model.User).Id}) + newuser := result.Data.(*model.User) userID = newuser.Id diff --git a/mattermost.go b/mattermost.go index a417fb3ec..a16bf53a7 100644 --- a/mattermost.go +++ b/mattermost.go @@ -22,6 +22,7 @@ import ( "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/manualtesting" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mattermost/platform/web" @@ -36,13 +37,18 @@ import ( //ENTERPRISE_IMPORTS +var flagCmdUpdateDb30 bool var flagCmdCreateTeam bool var flagCmdCreateUser bool var flagCmdAssignRole bool var flagCmdVersion bool var flagCmdResetPassword bool +var flagCmdResetMfa bool var flagCmdPermanentDeleteUser bool var flagCmdPermanentDeleteTeam bool +var flagCmdPermanentDeleteAllUsers bool +var flagCmdResetDatabase bool +var flagUsername string var flagCmdUploadLicense bool var flagConfigFile string var flagLicenseFile string @@ -69,6 +75,10 @@ func main() { l4g.Info(utils.T("mattermost.working_dir"), pwd) l4g.Info(utils.T("mattermost.config_file"), utils.FindConfigFile(flagConfigFile)) + // Speical case for upgrading the db to 3.0 + // ADDED for 3.0 REMOVE for 3.4 + cmdUpdateDb30() + api.NewServer() api.InitApi() web.InitWeb() @@ -223,19 +233,24 @@ func parseCmds() { } flag.StringVar(&flagConfigFile, "config", "config.json", "") + flag.StringVar(&flagUsername, "username", "", "") flag.StringVar(&flagLicenseFile, "license", "", "") flag.StringVar(&flagEmail, "email", "", "") flag.StringVar(&flagPassword, "password", "", "") flag.StringVar(&flagTeamName, "team_name", "", "") flag.StringVar(&flagRole, "role", "", "") + flag.BoolVar(&flagCmdUpdateDb30, "upgrade_db_30", false, "") flag.BoolVar(&flagCmdCreateTeam, "create_team", false, "") flag.BoolVar(&flagCmdCreateUser, "create_user", false, "") flag.BoolVar(&flagCmdAssignRole, "assign_role", false, "") flag.BoolVar(&flagCmdVersion, "version", false, "") flag.BoolVar(&flagCmdResetPassword, "reset_password", false, "") + flag.BoolVar(&flagCmdResetMfa, "reset_mfa", false, "") flag.BoolVar(&flagCmdPermanentDeleteUser, "permanent_delete_user", false, "") flag.BoolVar(&flagCmdPermanentDeleteTeam, "permanent_delete_team", false, "") + flag.BoolVar(&flagCmdPermanentDeleteAllUsers, "permanent_delete_all_users", false, "") + flag.BoolVar(&flagCmdResetDatabase, "reset_database", false, "") flag.BoolVar(&flagCmdUploadLicense, "upload_license", false, "") flag.Parse() @@ -244,9 +259,12 @@ func parseCmds() { flagCmdCreateUser || flagCmdAssignRole || flagCmdResetPassword || + flagCmdResetMfa || flagCmdVersion || flagCmdPermanentDeleteUser || flagCmdPermanentDeleteTeam || + flagCmdPermanentDeleteAllUsers || + flagCmdResetDatabase || flagCmdUploadLicense) } @@ -256,11 +274,258 @@ func runCmds() { cmdCreateUser() cmdAssignRole() cmdResetPassword() + cmdResetMfa() cmdPermDeleteUser() cmdPermDeleteTeam() + cmdPermDeleteAllUsers() + cmdResetDatabase() cmdUploadLicense() } +type TeamForUpgrade struct { + Id string + Name string +} + +// ADDED for 3.0 REMOVE for 3.4 +func cmdUpdateDb30() { + if flagCmdUpdateDb30 { + api.Srv = &api.Server{} + api.Srv.Store = store.NewSqlStoreForUpgrade30() + store := api.Srv.Store.(*store.SqlStore) + + l4g.Info("Attempting to run speical upgrade of the database schema to version 3.0 for user model changes") + time.Sleep(time.Second) + + if !store.DoesColumnExist("Users", "TeamId") { + fmt.Println("**WARNING** the database schema appears to be upgraded to 3.0") + flushLogAndExit(1) + } + + if !(store.SchemaVersion == "2.2.0" || + store.SchemaVersion == "2.1.0" || + store.SchemaVersion == "2.0.0") { + fmt.Println("**WARNING** the database schema needs to be version 2.2.0, 2.1.0 or 2.0.0 to upgrade") + flushLogAndExit(1) + } + + var confirmBackup string + fmt.Println("\nPlease see http://www.mattermost.org/upgrade-to-3-0/") + fmt.Println("**WARNING** This upgrade process will be irreversible.") + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirmBackup) + if confirmBackup != "YES" { + fmt.Fprintln(os.Stderr, "ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var flagTeamName string + var teams []*TeamForUpgrade + + if _, err := store.GetMaster().Select(&teams, "SELECT Id, Name FROM Teams"); err != nil { + l4g.Error("Failed to load all teams details=%v", err) + flushLogAndExit(1) + } + + fmt.Println(fmt.Sprintf("We found %v teams.", len(teams))) + + for _, team := range teams { + fmt.Println(team.Name) + } + + fmt.Print("Please pick a primary team from the list above: ") + fmt.Scanln(&flagTeamName) + + var team *TeamForUpgrade + for _, t := range teams { + if t.Name == flagTeamName { + team = t + break + } + } + + if team == nil { + l4g.Error("Failed to find primary team details") + flushLogAndExit(1) + } + + l4g.Info("Starting speical 3.0 database upgrade with performed_backup=YES team_name=%v", team.Name) + l4g.Info("Primary team %v will be left unchanged", team.Name) + l4g.Info("Upgrading primary team %v", team.Name) + + uniqueEmails := make(map[string]bool) + uniqueUsernames := make(map[string]bool) + primaryUsers := convertTeamTo30(team, uniqueEmails, uniqueUsernames) + + l4g.Info("Upgraded %v users", len(primaryUsers)) + + for _, otherTeam := range teams { + if otherTeam.Id != team.Id { + l4g.Info("Upgrading team %v", otherTeam.Name) + users := convertTeamTo30(otherTeam, uniqueEmails, uniqueUsernames) + l4g.Info("Upgraded %v users", len(users)) + + } + } + + l4g.Info("Altering other scheme changes needed 3.0 for user model changes") + + if _, err := store.GetMaster().Exec(` + UPDATE Channels + SET + TeamId = '' + WHERE + Type = 'D' + `, + ); err != nil { + l4g.Error("Failed to update direct channel types details=%v", err) + flushLogAndExit(1) + } + + extraLength := store.GetMaxLengthOfColumnIfExists("Audits", "ExtraInfo") + if len(extraLength) > 0 && extraLength != "1024" { + store.AlterColumnTypeIfExists("Audits", "ExtraInfo", "VARCHAR(1024)", "VARCHAR(1024)") + } + + actionLength := store.GetMaxLengthOfColumnIfExists("Audits", "Action") + if len(actionLength) > 0 && actionLength != "512" { + store.AlterColumnTypeIfExists("Audits", "Action", "VARCHAR(512)", "VARCHAR(512)") + } + + if store.DoesColumnExist("Sessions", "TeamId") { + store.RemoveColumnIfExists("Sessions", "TeamId") + store.GetMaster().Exec(`TRUNCATE Sessions`) + } + + // ADDED for 2.2 REMOVE for 2.6 + store.CreateColumnIfNotExists("Users", "MfaActive", "tinyint(1)", "boolean", "0") + store.CreateColumnIfNotExists("Users", "MfaSecret", "varchar(128)", "character varying(128)", "") + + // ADDED for 2.2 REMOVE for 2.6 + if store.DoesColumnExist("Users", "TeamId") { + store.RemoveIndexIfExists("idx_users_team_id", "Users") + store.CreateUniqueIndexIfNotExists("idx_users_email_unique", "Users", "Email") + store.CreateUniqueIndexIfNotExists("idx_users_username_unique", "Users", "Username") + store.RemoveColumnIfExists("Teams", "AllowTeamListing") + store.RemoveColumnIfExists("Users", "TeamId") + } + + l4g.Info("Finished running speical upgrade of the database schema to version 3.0 for user model changes") + + if result := <-store.System().Update(&model.System{Name: "Version", Value: model.CurrentVersion}); result.Err != nil { + l4g.Error("Failed to update system schema version details=%v", result.Err) + flushLogAndExit(1) + } + + l4g.Info(utils.T("store.sql.upgraded.warn"), model.CurrentVersion) + fmt.Println("**SUCCESS** with upgrade") + + flushLogAndExit(0) + } +} + +type UserForUpgrade struct { + Id string + Username string + Email string + Roles string + TeamId string +} + +func convertTeamTo30(team *TeamForUpgrade, uniqueEmails map[string]bool, uniqueUsernames map[string]bool) []*UserForUpgrade { + store := api.Srv.Store.(*store.SqlStore) + var users []*UserForUpgrade + if _, err := store.GetMaster().Select(&users, "SELECT Users.Id, Users.Username, Users.Email, Users.Roles, Users.TeamId FROM Users WHERE Users.TeamId = :TeamId", map[string]interface{}{"TeamId": team.Id}); err != nil { + l4g.Error("Failed to load profiles for team details=%v", err) + flushLogAndExit(1) + } + + var members []*model.TeamMember + if result := <-api.Srv.Store.Team().GetMembers(team.Id); result.Err != nil { + l4g.Error("Failed to load team membership details=%v", result.Err) + flushLogAndExit(1) + } else { + members = result.Data.([]*model.TeamMember) + } + + for _, user := range users { + shouldUpdateUser := false + previousRole := user.Roles + previousEmail := user.Email + previousUsername := user.Username + + member := &model.TeamMember{ + TeamId: team.Id, + UserId: user.Id, + } + + if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) { + member.Roles = model.ROLE_TEAM_ADMIN + user.Roles = "" + shouldUpdateUser = true + } + + exists := false + for _, member := range members { + if member.UserId == user.Id { + exists = true + break + } + } + + if !exists { + if result := <-api.Srv.Store.Team().SaveMember(member); result.Err != nil { + l4g.Error("Failed to save membership for %v details=%v", user.Email, result.Err) + flushLogAndExit(1) + } + } + + if uniqueEmails[user.Email] { + shouldUpdateUser = true + emailParts := strings.Split(user.Email, "@") + if len(emailParts) == 2 { + user.Email = emailParts[0] + "+" + team.Name + "@" + emailParts[1] + } else { + user.Email = user.Email + "." + team.Name + } + } + + if uniqueUsernames[user.Username] { + shouldUpdateUser = true + user.Username = user.Username + "." + team.Name + } + + if shouldUpdateUser { + if _, err := store.GetMaster().Exec(` + UPDATE Users + SET + Email = :Email, + Username = :Username, + Roles = :Roles + WHERE + Id = :Id + `, + map[string]interface{}{ + "Email": user.Email, + "Username": user.Username, + "Roles": user.Roles, + "Id": user.Id, + }, + ); err != nil { + l4g.Error("Failed to update user %v details=%v", user.Email, err) + flushLogAndExit(1) + } + + l4g.Info("modified user_id=%v, changed email from=%v to=%v, changed username from=%v to %v changed roles from=%v to=%v", user.Id, previousEmail, user.Email, previousUsername, user.Username, previousRole, user.Roles) + } + + uniqueEmails[user.Email] = true + uniqueUsernames[user.Username] = true + } + + return users +} + func cmdCreateTeam() { if flagCmdCreateTeam { if len(flagTeamName) == 0 { @@ -327,10 +592,9 @@ func cmdCreateUser() { flushLogAndExit(1) } else { team = result.Data.(*model.Team) - user.TeamId = team.Id } - _, err := api.CreateUser(team, user) + ruser, err := api.CreateUser(user) if err != nil { if err.Id != "store.sql_user.save.email_exists.app_error" { l4g.Error("%v", err) @@ -338,6 +602,12 @@ func cmdCreateUser() { } } + err = api.JoinUserToTeam(team, ruser) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + os.Exit(0) } } @@ -368,7 +638,7 @@ func cmdAssignRole() { os.Exit(1) } - if !model.IsValidRoles(flagRole) { + if !model.IsValidUserRoles(flagRole) { fmt.Fprintln(os.Stderr, "flag invalid argument: -role") flag.Usage() os.Exit(1) @@ -376,16 +646,8 @@ func cmdAssignRole() { c := getMockContext() - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { - l4g.Error("%v", result.Err) - flushLogAndExit(1) - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil { + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -426,16 +688,8 @@ func cmdResetPassword() { os.Exit(1) } - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { - l4g.Error("%v", result.Err) - flushLogAndExit(1) - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil { + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -451,14 +705,42 @@ func cmdResetPassword() { } } -func cmdPermDeleteUser() { - if flagCmdPermanentDeleteUser { - if len(flagTeamName) == 0 { - fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") +func cmdResetMfa() { + if flagCmdResetMfa { + if len(flagEmail) == 0 && len(flagUsername) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email OR -username") flag.Usage() os.Exit(1) } + var user *model.User + if len(flagEmail) > 0 { + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + } else { + if result := <-api.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + } + + if err := api.DeactivateMfa(user.Id); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdPermDeleteUser() { + if flagCmdPermanentDeleteUser { if len(flagEmail) == 0 { fmt.Fprintln(os.Stderr, "flag needs an argument: -email") flag.Usage() @@ -467,16 +749,8 @@ func cmdPermDeleteUser() { c := getMockContext() - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { - l4g.Error("%v", result.Err) - flushLogAndExit(1) - } else { - team = result.Data.(*model.Team) - } - var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil { + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -487,6 +761,7 @@ func cmdPermDeleteUser() { fmt.Print("Have you performed a database backup? (YES/NO): ") fmt.Scanln(&confirmBackup) if confirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") flushLogAndExit(1) } @@ -494,6 +769,7 @@ func cmdPermDeleteUser() { fmt.Printf("Are you sure you want to delete the user %v? All data will be permanently deleted? (YES/NO): ", user.Email) fmt.Scanln(&confirm) if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") flushLogAndExit(1) } @@ -501,6 +777,7 @@ func cmdPermDeleteUser() { l4g.Error("%v", err) flushLogAndExit(1) } else { + fmt.Print("SUCCESS: User deleted.") flushLogAndExit(0) } } @@ -528,6 +805,7 @@ func cmdPermDeleteTeam() { fmt.Print("Have you performed a database backup? (YES/NO): ") fmt.Scanln(&confirmBackup) if confirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") flushLogAndExit(1) } @@ -535,6 +813,7 @@ func cmdPermDeleteTeam() { fmt.Printf("Are you sure you want to delete the team %v? All data will be permanently deleted? (YES/NO): ", team.Name) fmt.Scanln(&confirm) if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") flushLogAndExit(1) } @@ -542,11 +821,67 @@ func cmdPermDeleteTeam() { l4g.Error("%v", err) flushLogAndExit(1) } else { + fmt.Print("SUCCESS: Team deleted.") flushLogAndExit(0) } } } +func cmdPermDeleteAllUsers() { + if flagCmdPermanentDeleteAllUsers { + c := getMockContext() + + var confirmBackup string + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirmBackup) + if confirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete all the users? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + if err := api.PermanentDeleteAllUsers(c); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } else { + fmt.Print("SUCCESS: All users deleted.") + flushLogAndExit(0) + } + } +} + +func cmdResetDatabase() { + if flagCmdResetDatabase { + var confirmBackup string + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirmBackup) + if confirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete everything? ALL data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + api.Srv.Store.DropAllTables() + fmt.Print("SUCCESS: Database reset.") + flushLogAndExit(0) + } + +} + func cmdUploadLicense() { if flagCmdUploadLicense { if model.BuildEnterpriseReady != "true" { @@ -574,7 +909,7 @@ func cmdUploadLicense() { flushLogAndExit(0) } - os.Exit(0) + flushLogAndExit(0) } } @@ -604,6 +939,8 @@ USAGE: FLAGS: -config="config.json" Path to the config file + -username="someuser" Username used in other commands + -license="ex.mattermost-license" Path to your license file -email="user@example.com" Email address used in other commands @@ -644,14 +981,33 @@ COMMANDS: Example: platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword" + -reset_mfa Turns off multi-factor authentication for a user. It requires the + -email or -username flag. + Example: + platform -reset_mfa -username="someuser" + + -reset_database Completely erases the database causing the loss of all data. This + will reset Mattermost to it's initial state. (note this will not + erase your configuration.) + + Example: + platform -reset_mfa -username="someuser" + -permanent_delete_user Permanently deletes a user and all related information + including posts from the database. It requires the + -email flag. You may need to restart the + server to invalidate the cache + Example: + platform -permanent_delete_user -email="user@example.com" + + -permanent_delete_all_users Permanently deletes all users and all related information including posts from the database. It requires the -team_name, and -email flag. You may need to restart the server to invalidate the cache Example: - platform -permanent_delete_user -team_name="name" -email="user@example.com" + platform -permanent_delete_all_users -team_name="name" -email="user@example.com" - -permanent_delete_team Permanently deletes a team and all users along with + -permanent_delete_team Permanently deletes a team allong with all related information including posts from the database. It requires the -team_name flag. You may need to restart the server to invalidate the cache. diff --git a/model/client.go b/model/client.go index 89b4d134f..4edb859e2 100644 --- a/model/client.go +++ b/model/client.go @@ -28,7 +28,10 @@ const ( HEADER_AUTH = "Authorization" HEADER_REQUESTED_WITH = "X-Requested-With" HEADER_REQUESTED_WITH_XML = "XMLHttpRequest" - API_URL_SUFFIX = "/api/v1" + + API_URL_SUFFIX_V1 = "/api/v1" + API_URL_SUFFIX_V3 = "/api/v3" + API_URL_SUFFIX = API_URL_SUFFIX_V3 ) type Result struct { @@ -39,16 +42,52 @@ type Result struct { type Client struct { Url string // The location of the server like "http://localhost:8065" - ApiUrl string // The api location of the server like "http://localhost:8065/api/v1" + ApiUrl string // The api location of the server like "http://localhost:8065/api/v3" HttpClient *http.Client // The http client AuthToken string AuthType string + TeamId string } // NewClient constructs a new client with convienence methods for talking to // the server. func NewClient(url string) *Client { - return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", ""} + return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", ""} +} + +func (c *Client) SetOAuthToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_TOKEN +} + +func (c *Client) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HEADER_BEARER +} + +func (c *Client) SetTeamId(teamId string) { + c.TeamId = teamId +} + +func (c *Client) GetTeamId() string { + if len(c.TeamId) == 0 { + println(`You are trying to use a route that requires a team_id, + but you have not called SetTeamId() in client.go`) + } + + return c.TeamId +} + +func (c *Client) ClearTeamId() { + c.TeamId = "" +} + +func (c *Client) GetTeamRoute() string { + return fmt.Sprintf("/teams/%v", c.GetTeamId()) +} + +func (c *Client) GetChannelRoute(channelId string) string { + return fmt.Sprintf("/teams/%v/channels/%v", c.GetTeamId(), channelId) } func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) { @@ -162,10 +201,19 @@ func (c *Client) GetAllTeams() (*Result, *AppError) { } } -func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) { +func (c *Client) GetAllTeamListings() (*Result, *AppError) { + if r, err := c.DoApiGet("/teams/all_team_listings", "", ""); 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) FindTeamByName(name string) (*Result, *AppError) { m := make(map[string]string) m["name"] = name - m["all"] = fmt.Sprintf("%v", allServers) if r, err := c.DoApiPost("/teams/find_team_by_name", MapToJson(m)); err != nil { return nil, err } else { @@ -179,8 +227,32 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro } } +func (c *Client) AddUserToTeam(userId string) (*Result, *AppError) { + data := make(map[string]string) + data["user_id"] = userId + if r, err := c.DoApiPost(c.GetTeamRoute()+"/add_user_to_team", MapToJson(data)); 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) AddUserToTeamFromInvite(hash, dataToHash, inviteId string) (*Result, *AppError) { + data := make(map[string]string) + data["hash"] = hash + data["data"] = dataToHash + data["invite_id"] = inviteId + if r, err := c.DoApiPost("/teams/add_user_to_team_from_invite", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil + } +} + func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { - if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/invite_members", invites.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -189,7 +261,7 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { } func (c *Client) UpdateTeam(team *Team) (*Result, *AppError) { - if r, err := c.DoApiPost("/teams/update", team.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/update", team.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -206,6 +278,18 @@ func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { } } +func (c *Client) CreateUserWithInvite(user *User, hash string, data string, inviteId string) (*Result, *AppError) { + + url := "/users/create?d=" + url.QueryEscape(data) + "&h=" + url.QueryEscape(hash) + "&iid=" + url.QueryEscape(inviteId) + + if r, err := c.DoApiPost(url, user.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) { if r, err := c.DoApiPost("/users/create?d="+url.QueryEscape(data)+"&h="+hash, user.ToJson()); err != nil { return nil, err @@ -216,7 +300,7 @@ func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Re } func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/"+id, "", etag); err != nil { + if r, err := c.DoApiGet("/users/"+id+"/get", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -242,6 +326,24 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { } } +func (c *Client) GetProfilesForTeam(teamId string, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet("/users/profiles/"+teamId+"?skip_direct=true", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) { + if r, err := c.DoApiGet("/users/direct_profiles", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil + } +} + func (c *Client) LoginById(id string, password string) (*Result, *AppError) { m := make(map[string]string) m["id"] = id @@ -274,6 +376,26 @@ func (c *Client) LoginByEmailWithDevice(name string, email string, password stri return c.login(m) } +func (c *Client) LoginByLdap(userid string, password string, mfatoken string) (*Result, *AppError) { + m := make(map[string]string) + m["id"] = userid + m["password"] = password + m["token"] = mfatoken + if r, err := c.DoApiPost("/users/login_ldap", MapToJson(m)); err != nil { + return nil, err + } else { + c.AuthToken = r.Header.Get(HEADER_TOKEN) + c.AuthType = HEADER_BEARER + sessionToken := getCookie(SESSION_COOKIE_TOKEN, r) + + if c.AuthToken != sessionToken.Value { + NewLocAppError("/users/login_ldap", "model.client.login.app_error", nil, "") + } + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) login(m map[string]string) (*Result, *AppError) { if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { return nil, err @@ -297,16 +419,16 @@ func (c *Client) Logout() (*Result, *AppError) { } else { c.AuthToken = "" c.AuthType = HEADER_BEARER + c.TeamId = "" return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil } } -func (c *Client) CheckMfa(method, teamName, loginId string) (*Result, *AppError) { +func (c *Client) CheckMfa(method, loginId string) (*Result, *AppError) { m := make(map[string]string) m["method"] = method - m["team_name"] = teamName m["login_id"] = loginId if r, err := c.DoApiPost("/users/mfa", MapToJson(m)); err != nil { @@ -339,14 +461,16 @@ func (c *Client) UpdateMfa(activate bool, token string) (*Result, *AppError) { } } -func (c *Client) SetOAuthToken(token string) { - c.AuthToken = token - c.AuthType = HEADER_TOKEN -} +func (c *Client) AdminResetMfa(userId string) (*Result, *AppError) { + m := make(map[string]string) + m["user_id"] = userId -func (c *Client) ClearOAuthToken() { - c.AuthToken = "" - c.AuthType = HEADER_BEARER + if r, err := c.DoApiPost("/admin/reset_mfa", MapToJson(m)); 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) RevokeSession(sessionAltId string) (*Result, *AppError) { @@ -411,7 +535,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul m["command"] = command m["channelId"] = channelId m["suggest"] = strconv.FormatBool(suggest) - if r, err := c.DoApiPost("/commands/execute", MapToJson(m)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/execute", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -420,7 +544,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul } func (c *Client) ListCommands() (*Result, *AppError) { - if r, err := c.DoApiGet("/commands/list", "", ""); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/commands/list", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -429,7 +553,7 @@ func (c *Client) ListCommands() (*Result, *AppError) { } func (c *Client) ListTeamCommands() (*Result, *AppError) { - if r, err := c.DoApiGet("/commands/list_team_commands", "", ""); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/commands/list_team_commands", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -438,7 +562,7 @@ func (c *Client) ListTeamCommands() (*Result, *AppError) { } func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) { - if r, err := c.DoApiPost("/commands/create", cmd.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/create", cmd.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -447,7 +571,7 @@ func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) { } func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/commands/regen_token", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/regen_token", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -456,7 +580,7 @@ func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) } func (c *Client) DeleteCommand(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/commands/delete", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/delete", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -582,7 +706,7 @@ func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) { } func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -590,8 +714,10 @@ func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { } } -func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/create_direct", MapToJson(data)); err != nil { +func (c *Client) CreateDirectChannel(userId string) (*Result, *AppError) { + data := make(map[string]string) + data["user_id"] = userId + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create_direct", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -600,7 +726,7 @@ func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError } func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/update", channel.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -609,7 +735,7 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { } func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/update_header", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_header", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -618,7 +744,7 @@ func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError } func (c *Client) UpdateChannelPurpose(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/update_purpose", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_purpose", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -627,7 +753,7 @@ func (c *Client) UpdateChannelPurpose(data map[string]string) (*Result, *AppErro } func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/update_notify_props", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_notify_props", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -636,7 +762,7 @@ func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) } func (c *Client) GetChannels(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/channels/", "", etag); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -645,7 +771,7 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/channels/"+id+"/", "", etag); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -654,7 +780,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { } func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/channels/more", "", etag); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/more", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -663,7 +789,7 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/channels/counts", "", etag); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/counts", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -672,7 +798,7 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { } func (c *Client) JoinChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+id+"/join", ""); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -681,7 +807,7 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) { } func (c *Client) LeaveChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+id+"/leave", ""); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/leave", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -690,7 +816,7 @@ func (c *Client) LeaveChannel(id string) (*Result, *AppError) { } func (c *Client) DeleteChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+id+"/delete", ""); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/delete", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -701,7 +827,7 @@ func (c *Client) DeleteChannel(id string) (*Result, *AppError) { func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoApiPost("/channels/"+id+"/add", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/add", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -712,7 +838,7 @@ func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoApiPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/remove", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -721,7 +847,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { } func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_last_viewed_at", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -730,7 +856,7 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { } func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/channels/"+id+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -739,7 +865,7 @@ func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (* } func (c *Client) CreatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(post.ChannelId)+"/posts/create", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -748,7 +874,7 @@ func (c *Client) CreatePost(post *Post) (*Result, *AppError) { } func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(post.ChannelId)+"/posts/update", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -757,7 +883,7 @@ func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { } func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/page/%v/%v", offset, limit), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -766,7 +892,7 @@ func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) } func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/since/%v", time), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -775,7 +901,7 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError } func (c *Client) GetPostsBefore(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/before/%v/%v", channelId, postid, offset, limit), "", etag); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/before/%v/%v", postid, offset, limit), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -784,7 +910,7 @@ func (c *Client) GetPostsBefore(channelId string, postid string, offset int, lim } func (c *Client) GetPostsAfter(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/after/%v/%v", channelId, postid, offset, limit), "", etag); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/posts/%v/after/%v/%v", postid, offset, limit), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -793,7 +919,7 @@ func (c *Client) GetPostsAfter(channelId string, postid string, offset int, limi } func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get", postId), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -802,7 +928,7 @@ func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, } func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoApiPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil { + if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/delete", postId), ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -811,7 +937,7 @@ func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError } func (c *Client) SearchPosts(terms string) (*Result, *AppError) { - if r, err := c.DoApiGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -819,8 +945,16 @@ func (c *Client) SearchPosts(terms string) (*Result, *AppError) { } } -func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) { - rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) +func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) { + return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType) +} + +func (c *Client) UploadPostAttachment(data []byte, contentType string) (*Result, *AppError) { + return c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", data, contentType) +} + +func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) { + rq, _ := http.NewRequest("POST", url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) if len(c.AuthToken) > 0 { @@ -842,7 +976,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { if isFullUrl { rq, _ = http.NewRequest("GET", url, nil) } else { - rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get"+url, nil) } if len(c.AuthToken) > 0 { @@ -861,7 +995,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { func (c *Client) GetFileInfo(url string) (*Result, *AppError) { var rq *http.Request - rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get_info"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get_info"+url, nil) if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -878,7 +1012,7 @@ func (c *Client) GetFileInfo(url string) (*Result, *AppError) { } func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/files/get_public_link", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/files/get_public_link", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -950,7 +1084,9 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) } } -func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) { +func (c *Client) SendPasswordReset(email string) (*Result, *AppError) { + data := map[string]string{} + data["email"] = email if r, err := c.DoApiPost("/users/send_password_reset", MapToJson(data)); err != nil { return nil, err } else { @@ -959,7 +1095,10 @@ func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) } } -func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { +func (c *Client) ResetPassword(code, newPassword string) (*Result, *AppError) { + data := map[string]string{} + data["code"] = code + data["new_password"] = newPassword if r, err := c.DoApiPost("/users/reset_password", MapToJson(data)); err != nil { return nil, err } else { @@ -968,6 +1107,18 @@ func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { } } +func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppError) { + data := map[string]string{} + data["user_id"] = userId + data["new_password"] = newPassword + if r, err := c.DoApiPost("/admin/reset_password", MapToJson(data)); 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) GetStatuses(data []string) (*Result, *AppError) { if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil { return nil, err @@ -978,7 +1129,7 @@ func (c *Client) GetStatuses(data []string) (*Result, *AppError) { } func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/teams/me", "", etag); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/me", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -986,6 +1137,15 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { } } +func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) { + if r, err := c.DoApiGet("/teams/members/"+teamId, "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil + } +} + func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) { if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil { return nil, err @@ -1014,7 +1174,7 @@ func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { } func (c *Client) CreateIncomingWebhook(hook *IncomingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost("/hooks/incoming/create", hook.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/incoming/create", hook.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1031,8 +1191,10 @@ func (c *Client) PostToWebhook(id, payload string) (*Result, *AppError) { } } -func (c *Client) DeleteIncomingWebhook(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/hooks/incoming/delete", MapToJson(data)); err != nil { +func (c *Client) DeleteIncomingWebhook(id string) (*Result, *AppError) { + data := make(map[string]string) + data["id"] = id + if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/incoming/delete", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1041,7 +1203,7 @@ func (c *Client) DeleteIncomingWebhook(data map[string]string) (*Result, *AppErr } func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { - if r, err := c.DoApiGet("/hooks/incoming/list", "", ""); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/hooks/incoming/list", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1085,7 +1247,7 @@ func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) { } func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost("/hooks/outgoing/create", hook.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1093,8 +1255,10 @@ func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppErro } } -func (c *Client) DeleteOutgoingWebhook(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/hooks/outgoing/delete", MapToJson(data)); err != nil { +func (c *Client) DeleteOutgoingWebhook(id string) (*Result, *AppError) { + data := make(map[string]string) + data["id"] = id + if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/delete", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1103,7 +1267,7 @@ func (c *Client) DeleteOutgoingWebhook(data map[string]string) (*Result, *AppErr } func (c *Client) ListOutgoingWebhooks() (*Result, *AppError) { - if r, err := c.DoApiGet("/hooks/outgoing/list", "", ""); err != nil { + if r, err := c.DoApiGet(c.GetTeamRoute()+"/hooks/outgoing/list", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1111,8 +1275,10 @@ func (c *Client) ListOutgoingWebhooks() (*Result, *AppError) { } } -func (c *Client) RegenOutgoingWebhookToken(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/hooks/outgoing/regen_token", MapToJson(data)); err != nil { +func (c *Client) RegenOutgoingWebhookToken(id string) (*Result, *AppError) { + data := make(map[string]string) + data["id"] = id + if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/regen_token", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1134,11 +1300,11 @@ func (c *Client) GetClientLicenceConfig(etag string) (*Result, *AppError) { } } -func (c *Client) GetMeLoggedIn() (*Result, *AppError) { - if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil { +func (c *Client) GetInitialLoad() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/initial_load", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + r.Header.Get(HEADER_ETAG_SERVER), InitialLoadFromJson(r.Body)}, nil } } diff --git a/model/command.go b/model/command.go index b854ae76a..4d5f7ace9 100644 --- a/model/command.go +++ b/model/command.go @@ -99,7 +99,7 @@ func (o *Command) IsValid() *AppError { return NewLocAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "") } - if len(o.Trigger) > 128 { + if len(o.Trigger) == 0 || len(o.Trigger) > 128 { return NewLocAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "") } diff --git a/model/command_test.go b/model/command_test.go index d362d8f2c..2376e2ef7 100644 --- a/model/command_test.go +++ b/model/command_test.go @@ -19,63 +19,115 @@ func TestCommandJson(t *testing.T) { } func TestCommandIsValid(t *testing.T) { - o := Command{} + o := Command{ + Id: NewId(), + Token: NewId(), + CreateAt: GetMillis(), + UpdateAt: GetMillis(), + CreatorId: NewId(), + TeamId: NewId(), + Trigger: "trigger", + URL: "http://example.com", + Method: COMMAND_METHOD_GET, + DisplayName: "", + Description: "", + } + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Id = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } o.Id = NewId() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Token = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.CreateAt = GetMillis() + o.Token = NewId() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.CreateAt = 0 if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.UpdateAt = GetMillis() + o.CreateAt = GetMillis() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.UpdateAt = 0 if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.CreatorId = "123" + o.UpdateAt = GetMillis() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.CreatorId = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } o.CreatorId = NewId() - if err := o.IsValid(); err == nil { - t.Fatal("should be invalid") + if err := o.IsValid(); err != nil { + t.Fatal(err) } - o.Token = "123" + o.TeamId = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.Token = NewId() + o.TeamId = NewId() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Trigger = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.TeamId = "123" + o.Trigger = strings.Repeat("1", 129) if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.TeamId = NewId() + o.Trigger = strings.Repeat("1", 128) + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.URL = "" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.URL = "nowhere.com/" + o.URL = "1234" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.URL = "http://nowhere.com/" + o.URL = "https://example.com" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Method = "https://example.com" if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } @@ -85,6 +137,11 @@ func TestCommandIsValid(t *testing.T) { t.Fatal(err) } + o.Method = COMMAND_METHOD_POST + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + o.DisplayName = strings.Repeat("1", 65) if err := o.IsValid(); err == nil { t.Fatal("should be invalid") diff --git a/model/config.go b/model/config.go index 6803fe069..fd033c7cf 100644 --- a/model/config.go +++ b/model/config.go @@ -155,9 +155,9 @@ type TeamSettings struct { MaxUsersPerTeam int EnableTeamCreation bool EnableUserCreation bool + EnableOpenServer *bool RestrictCreationToDomains string RestrictTeamNames *bool - EnableTeamListing *bool EnableCustomBrand *bool CustomBrandText *string } @@ -180,6 +180,7 @@ type LdapSettings struct { LastNameAttribute *string EmailAttribute *string UsernameAttribute *string + NicknameAttribute *string IdAttribute *string // Advanced @@ -297,11 +298,6 @@ func (o *Config) SetDefaults() { *o.TeamSettings.RestrictTeamNames = true } - if o.TeamSettings.EnableTeamListing == nil { - o.TeamSettings.EnableTeamListing = new(bool) - *o.TeamSettings.EnableTeamListing = false - } - if o.TeamSettings.EnableCustomBrand == nil { o.TeamSettings.EnableCustomBrand = new(bool) *o.TeamSettings.EnableCustomBrand = false @@ -312,6 +308,11 @@ func (o *Config) SetDefaults() { *o.TeamSettings.CustomBrandText = "" } + if o.TeamSettings.EnableOpenServer == nil { + o.TeamSettings.EnableOpenServer = new(bool) + *o.TeamSettings.EnableOpenServer = false + } + if o.EmailSettings.EnableSignInWithEmail == nil { o.EmailSettings.EnableSignInWithEmail = new(bool) @@ -476,6 +477,11 @@ func (o *Config) SetDefaults() { o.LdapSettings.SkipCertificateVerification = new(bool) *o.LdapSettings.SkipCertificateVerification = false } + + if o.LdapSettings.NicknameAttribute == nil { + o.LdapSettings.NicknameAttribute = new(string) + *o.LdapSettings.NicknameAttribute = "" + } } func (o *Config) IsValid() *AppError { diff --git a/model/gitlab.go b/model/gitlab.go new file mode 100644 index 000000000..3dfb1016a --- /dev/null +++ b/model/gitlab.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +const ( + USER_AUTH_SERVICE_GITLAB = "gitlab" +) diff --git a/model/gitlab/gitlab.go b/model/gitlab/gitlab.go index 3ca499976..7df29c139 100644 --- a/model/gitlab/gitlab.go +++ b/model/gitlab/gitlab.go @@ -12,10 +12,6 @@ import ( "strings" ) -const ( - USER_AUTH_SERVICE_GITLAB = "gitlab" -) - type GitLabProvider struct { } @@ -29,7 +25,7 @@ type GitLabUser struct { func init() { provider := &GitLabProvider{} - einterfaces.RegisterOauthProvider(USER_AUTH_SERVICE_GITLAB, provider) + einterfaces.RegisterOauthProvider(model.USER_AUTH_SERVICE_GITLAB, provider) } func userFromGitLabUser(glu *GitLabUser) *model.User { @@ -51,7 +47,7 @@ func userFromGitLabUser(glu *GitLabUser) *model.User { } user.Email = glu.Email user.AuthData = strconv.FormatInt(glu.Id, 10) - user.AuthService = USER_AUTH_SERVICE_GITLAB + user.AuthService = model.USER_AUTH_SERVICE_GITLAB return user } @@ -84,7 +80,7 @@ func (glu *GitLabUser) getAuthData() string { } func (m *GitLabProvider) GetIdentifier() string { - return USER_AUTH_SERVICE_GITLAB + return model.USER_AUTH_SERVICE_GITLAB } func (m *GitLabProvider) GetUserFromJson(data io.Reader) *model.User { diff --git a/model/initial_load.go b/model/initial_load.go new file mode 100644 index 000000000..d7587e6d4 --- /dev/null +++ b/model/initial_load.go @@ -0,0 +1,40 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type InitialLoad struct { + User *User `json:"user"` + TeamMembers []*TeamMember `json:"team_members"` + Teams []*Team `json:"teams"` + DirectProfiles map[string]*User `json:"direct_profiles"` + Preferences Preferences `json:"preferences"` + ClientCfg map[string]string `json:"client_cfg"` + LicenseCfg map[string]string `json:"license_cfg"` + NoAccounts bool `json:"no_accounts"` +} + +func (me *InitialLoad) ToJson() string { + b, err := json.Marshal(me) + if err != nil { + return "" + } else { + return string(b) + } +} + +func InitialLoadFromJson(data io.Reader) *InitialLoad { + decoder := json.NewDecoder(data) + var o InitialLoad + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/initial_load_test.go b/model/initial_load_test.go new file mode 100644 index 000000000..24a07e412 --- /dev/null +++ b/model/initial_load_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestInitialLoadJson(t *testing.T) { + u := &User{Id: NewId()} + o := InitialLoad{User: u} + json := o.ToJson() + ro := InitialLoadFromJson(strings.NewReader(json)) + + if o.User.Id != ro.User.Id { + t.Fatal("Ids do not match") + } +} diff --git a/model/password_recovery.go b/model/password_recovery.go new file mode 100644 index 000000000..303d4a12e --- /dev/null +++ b/model/password_recovery.go @@ -0,0 +1,37 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +const ( + PASSWORD_RECOVERY_CODE_SIZE = 128 + PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour +) + +type PasswordRecovery struct { + UserId string + Code string + CreateAt int64 +} + +func (p *PasswordRecovery) IsValid() *AppError { + + if len(p.UserId) != 26 { + return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.user_id.app_error", nil, "") + } + + if len(p.Code) != PASSWORD_RECOVERY_CODE_SIZE { + return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.code.app_error", nil, "") + } + + if p.CreateAt == 0 { + return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.create_at.app_error", nil, "") + } + + return nil +} + +func (p *PasswordRecovery) PreSave() { + p.Code = NewRandomString(PASSWORD_RECOVERY_CODE_SIZE) + p.CreateAt = GetMillis() +} diff --git a/model/session.go b/model/session.go index bf0d9531e..8a5eec74c 100644 --- a/model/session.go +++ b/model/session.go @@ -17,17 +17,17 @@ const ( ) type Session struct { - Id string `json:"id"` - Token string `json:"token"` - CreateAt int64 `json:"create_at"` - ExpiresAt int64 `json:"expires_at"` - LastActivityAt int64 `json:"last_activity_at"` - UserId string `json:"user_id"` - TeamId string `json:"team_id"` - DeviceId string `json:"device_id"` - Roles string `json:"roles"` - IsOAuth bool `json:"is_oauth"` - Props StringMap `json:"props"` + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + ExpiresAt int64 `json:"expires_at"` + LastActivityAt int64 `json:"last_activity_at"` + UserId string `json:"user_id"` + DeviceId string `json:"device_id"` + Roles string `json:"roles"` + IsOAuth bool `json:"is_oauth"` + Props StringMap `json:"props"` + TeamMembers []*TeamMember `json:"team_members" db:"-"` } func (me *Session) ToJson() string { @@ -95,6 +95,16 @@ func (me *Session) AddProp(key string, value string) { me.Props[key] = value } +func (me *Session) GetTeamByTeamId(teamId string) *TeamMember { + for _, team := range me.TeamMembers { + if team.TeamId == teamId { + return team + } + } + + return nil +} + func SessionsToJson(o []*Session) string { if b, err := json.Marshal(o); err != nil { return "[]" diff --git a/model/team.go b/model/team.go index d95dea110..072e0a8c0 100644 --- a/model/team.go +++ b/model/team.go @@ -18,19 +18,18 @@ const ( ) type Team struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Email string `json:"email"` - Type string `json:"type"` - CompanyName string `json:"company_name"` - AllowedDomains string `json:"allowed_domains"` - InviteId string `json:"invite_id"` - AllowOpenInvite bool `json:"allow_open_invite"` - AllowTeamListing bool `json:"allow_team_listing"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` + InviteId string `json:"invite_id"` + AllowOpenInvite bool `json:"allow_open_invite"` } type Invites struct { diff --git a/model/team_member.go b/model/team_member.go new file mode 100644 index 000000000..80ca9f2a3 --- /dev/null +++ b/model/team_member.go @@ -0,0 +1,78 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strings" +) + +const ( + ROLE_TEAM_ADMIN = "admin" +) + +type TeamMember struct { + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` +} + +func (o *TeamMember) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func TeamMemberFromJson(data io.Reader) *TeamMember { + decoder := json.NewDecoder(data) + var o TeamMember + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func TeamMembersToJson(o []*TeamMember) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func TeamMembersFromJson(data io.Reader) []*TeamMember { + decoder := json.NewDecoder(data) + var o []*TeamMember + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} + +func (o *TeamMember) IsValid() *AppError { + + if len(o.TeamId) != 26 { + return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.team_id.app_error", nil, "") + } + + if len(o.UserId) != 26 { + return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.user_id.app_error", nil, "") + } + + for _, role := range strings.Split(o.Roles, " ") { + if !(role == "" || role == ROLE_TEAM_ADMIN) { + return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.role.app_error", nil, "role="+role) + } + } + + return nil +} diff --git a/model/team_member_test.go b/model/team_member_test.go new file mode 100644 index 000000000..d5b2e3b79 --- /dev/null +++ b/model/team_member_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestTeamMemberJson(t *testing.T) { + o := TeamMember{TeamId: NewId(), UserId: NewId()} + json := o.ToJson() + ro := TeamMemberFromJson(strings.NewReader(json)) + + if o.TeamId != ro.TeamId { + t.Fatal("Ids do not match") + } +} + +func TestTeamMemberIsValid(t *testing.T) { + o := TeamMember{} + + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.TeamId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.UserId = NewId() + o.Roles = "blahblah" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Roles = "" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} diff --git a/model/user.go b/model/user.go index 173fe2b4e..f43fc2089 100644 --- a/model/user.go +++ b/model/user.go @@ -15,7 +15,6 @@ import ( ) const ( - ROLE_TEAM_ADMIN = "admin" ROLE_SYSTEM_ADMIN = "system_admin" USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute @@ -28,6 +27,7 @@ const ( DEFAULT_LOCALE = "en" USER_AUTH_SERVICE_EMAIL = "email" USER_AUTH_SERVICE_USERNAME = "username" + MIN_PASSWORD_LENGTH = 5 ) type User struct { @@ -35,7 +35,6 @@ type User struct { CreateAt int64 `json:"create_at,omitempty"` UpdateAt int64 `json:"update_at,omitempty"` DeleteAt int64 `json:"delete_at"` - TeamId string `json:"team_id"` Username string `json:"username"` Password string `json:"password,omitempty"` AuthData string `json:"auth_data,omitempty"` @@ -76,10 +75,6 @@ func (u *User) IsValid() *AppError { return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id) } - if len(u.TeamId) != 26 { - return NewLocAppError("User.IsValid", "model.user.is_valid.team_id.app_error", nil, "") - } - if !IsValidUsername(u.Username) { return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id) } @@ -228,6 +223,7 @@ func (u *User) IsAway() bool { func (u *User) Sanitize(options map[string]bool) { u.Password = "" u.AuthData = "" + u.MfaSecret = "" if len(options) != 0 && !options["email"] { u.Email = "" @@ -246,6 +242,8 @@ func (u *User) ClearNonProfileFields() { u.Password = "" u.AuthData = "" u.AuthService = "" + u.MfaActive = false + u.MfaSecret = "" u.EmailVerified = false u.LastPingAt = 0 u.AllowMarketing = false @@ -301,7 +299,7 @@ func (u *User) GetDisplayName() string { } } -func IsValidRoles(userRoles string) bool { +func IsValidUserRoles(userRoles string) bool { roles := strings.Split(userRoles, " ") @@ -319,10 +317,6 @@ func isValidRole(role string) bool { return true } - if role == ROLE_TEAM_ADMIN { - return true - } - if role == ROLE_SYSTEM_ADMIN { return true } @@ -351,8 +345,8 @@ func IsInRole(userRoles string, inRole string) bool { return false } -func (u *User) IsSSOUser() bool { - if len(u.AuthData) != 0 && len(u.AuthService) != 0 && u.AuthService != USER_AUTH_SERVICE_LDAP { +func (u *User) IsOAuthUser() bool { + if u.AuthService == USER_AUTH_SERVICE_GITLAB { return true } return false diff --git a/model/user_test.go b/model/user_test.go index 662ae35a6..286c92a66 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -63,11 +63,6 @@ func TestUserIsValid(t *testing.T) { t.Fatal() } - user.TeamId = NewId() - if err := user.IsValid(); err == nil { - t.Fatal() - } - user.Username = NewId() + "^hello#" if err := user.IsValid(); err == nil { t.Fatal() @@ -195,11 +190,11 @@ func TestCleanUsername(t *testing.T) { func TestRoles(t *testing.T) { - if !IsValidRoles("admin") { + if IsValidUserRoles("admin") { t.Fatal() } - if IsValidRoles("junk") { + if IsValidUserRoles("junk") { t.Fatal() } diff --git a/model/utils.go b/model/utils.go index bb02f345d..03215490d 100644 --- a/model/utils.go +++ b/model/utils.go @@ -41,12 +41,10 @@ func (er *AppError) Error() string { } func (er *AppError) Translate(T goi18n.TranslateFunc) { - if len(er.Message) == 0 { - if er.params == nil { - er.Message = T(er.Id) - } else { - er.Message = T(er.Id, er.params) - } + if er.params == nil { + er.Message = T(er.Id) + } else { + er.Message = T(er.Id, er.params) } } @@ -83,6 +81,7 @@ func NewLocAppError(where string, id string, params map[string]interface{}, deta ap := &AppError{} ap.Id = id ap.params = params + ap.Message = id ap.Where = where ap.DetailedError = details ap.StatusCode = 500 diff --git a/model/version.go b/model/version.go index e9a2c2bd0..4a47f06ef 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maitained in chronological order with most current // release at the front of the list. var versions = []string{ + "3.0.0", "2.2.0", "2.1.0", "2.0.0", diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go index 7609ebc25..772a549a3 100644 --- a/store/sql_audit_store.go +++ b/store/sql_audit_store.go @@ -28,17 +28,6 @@ func NewSqlAuditStore(sqlStore *SqlStore) AuditStore { } func (s SqlAuditStore) UpgradeSchemaIfNeeded() { - // ADDED for 2.2 REMOVE for 2.6 - extraLength := s.GetMaxLengthOfColumnIfExists("Audits", "ExtraInfo") - if len(extraLength) > 0 && extraLength != "1024" { - s.AlterColumnTypeIfExists("Audits", "ExtraInfo", "VARCHAR(1024)", "VARCHAR(1024)") - } - - // ADDED for 2.2 REMOVE for 2.6 - actionLength := s.GetMaxLengthOfColumnIfExists("Audits", "Action") - if len(actionLength) > 0 && actionLength != "512" { - s.AlterColumnTypeIfExists("Audits", "Action", "VARCHAR(512)", "VARCHAR(512)") - } } func (s SqlAuditStore) CreateIndexesIfNotExists() { diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index c7ffddd56..46f56e7eb 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -95,6 +95,7 @@ func (s SqlChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 if transaction, err := s.GetMaster().Begin(); err != nil { result.Err = model.NewLocAppError("SqlChannelStore.SaveDirectChannel", "store.sql_channel.save_direct_channel.open_transaction.app_error", nil, err.Error()) } else { + directchannel.TeamId = "" channelResult := s.saveChannelT(transaction, directchannel) if channelResult.Err != nil { @@ -330,7 +331,7 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel result := StoreResult{} var data []channelWithMember - _, err := s.GetReplica().Select(&data, "SELECT * FROM Channels, ChannelMembers WHERE Id = ChannelId AND TeamId = :TeamId AND UserId = :UserId AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + _, err := s.GetReplica().Select(&data, "SELECT * FROM Channels, ChannelMembers WHERE Id = ChannelId AND UserId = :UserId AND DeleteAt = 0 AND (TeamId = :TeamId OR TeamId = '') ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) if err != nil { result.Err = model.NewLocAppError("SqlChannelStore.GetChannels", "store.sql_channel.get_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error()) @@ -411,7 +412,7 @@ func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) StoreCha result := StoreResult{} var data []channelIdWithCountAndUpdateAt - _, err := s.GetReplica().Select(&data, "SELECT Id, TotalMsgCount, UpdateAt FROM Channels WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) AND TeamId = :TeamId AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + _, err := s.GetReplica().Select(&data, "SELECT Id, TotalMsgCount, UpdateAt FROM Channels WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) AND (TeamId = :TeamId OR TeamId = '') AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) if err != nil { result.Err = model.NewLocAppError("SqlChannelStore.GetChannelCounts", "store.sql_channel.get_channel_counts.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error()) @@ -441,7 +442,7 @@ func (s SqlChannelStore) GetByName(teamId string, name string) StoreChannel { channel := model.Channel{} - if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name= :Name AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId, "Name": name}); err != nil { + if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND Name = :Name AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId, "Name": name}); err != nil { if err == sql.ErrNoRows { result.Err = model.NewLocAppError("SqlChannelStore.GetByName", MISSING_CHANNEL_ERROR, nil, "teamId="+teamId+", "+"name="+name+", "+err.Error()) } else { @@ -719,6 +720,37 @@ func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) StoreChanne return storeChannel } +func (s SqlChannelStore) CheckPermissionsToNoTeam(channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + count, err := s.GetReplica().SelectInt( + `SELECT + COUNT(0) + FROM + Channels, + ChannelMembers + WHERE + Channels.Id = ChannelMembers.ChannelId + AND Channels.DeleteAt = 0 + AND ChannelMembers.ChannelId = :ChannelId + AND ChannelMembers.UserId = :UserId`, + map[string]interface{}{"ChannelId": channelId, "UserId": userId}) + if err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.CheckPermissionsTo", "store.sql_channel.check_permissions.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } else { + result.Data = count + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlChannelStore) CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel { storeChannel := make(StoreChannel) @@ -733,7 +765,7 @@ func (s SqlChannelStore) CheckPermissionsTo(teamId string, channelId string, use ChannelMembers WHERE Channels.Id = ChannelMembers.ChannelId - AND Channels.TeamId = :TeamId + AND (Channels.TeamId = :TeamId OR Channels.TeamId = '') AND Channels.DeleteAt = 0 AND ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId`, @@ -765,7 +797,7 @@ func (s SqlChannelStore) CheckPermissionsToByName(teamId string, channelName str ChannelMembers WHERE Channels.Id = ChannelMembers.ChannelId - AND Channels.TeamId = :TeamId + AND (Channels.TeamId = :TeamId OR Channels.TeamId = '') AND Channels.Name = :Name AND Channels.DeleteAt = 0 AND ChannelMembers.UserId = :UserId`, diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index 2213aa795..1b3ea6fe5 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -67,17 +67,17 @@ func TestChannelStoreSaveDirectChannel(t *testing.T) { o1.Name = "a" + model.NewId() + "b" o1.Type = model.CHANNEL_DIRECT - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() u1.Nickname = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) - u2 := model.User{} - u2.TeamId = model.NewId() + u2 := &model.User{} u2.Email = model.NewId() u2.Nickname = model.NewId() - Must(store.User().Save(&u2)) + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id})) m1 := model.ChannelMember{} m1.ChannelId = o1.Id @@ -163,17 +163,17 @@ func TestChannelStoreGet(t *testing.T) { t.Fatal("Missing id should have failed") } - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() u1.Nickname = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) u2 := model.User{} - u2.TeamId = model.NewId() u2.Email = model.NewId() u2.Nickname = model.NewId() Must(store.User().Save(&u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id})) o2 := model.Channel{} o2.TeamId = model.NewId() @@ -309,16 +309,16 @@ func TestChannelMemberStore(t *testing.T) { t1 := c1t1.ExtraUpdateAt u1 := model.User{} - u1.TeamId = model.NewId() u1.Email = model.NewId() u1.Nickname = model.NewId() Must(store.User().Save(&u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) u2 := model.User{} - u2.TeamId = model.NewId() u2.Email = model.NewId() u2.Nickname = model.NewId() Must(store.User().Save(&u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id})) o1 := model.ChannelMember{} o1.ChannelId = c1.Id @@ -405,16 +405,16 @@ func TestChannelDeleteMemberStore(t *testing.T) { t1 := c1t1.ExtraUpdateAt u1 := model.User{} - u1.TeamId = model.NewId() u1.Email = model.NewId() u1.Nickname = model.NewId() Must(store.User().Save(&u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) u2 := model.User{} - u2.TeamId = model.NewId() u2.Email = model.NewId() u2.Nickname = model.NewId() Must(store.User().Save(&u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id})) o1 := model.ChannelMember{} o1.ChannelId = c1.Id @@ -469,6 +469,11 @@ func TestChannelStorePermissionsTo(t *testing.T) { t.Fatal("should have permissions") } + count = (<-store.Channel().CheckPermissionsToNoTeam(o1.Id, m1.UserId)).Data.(int64) + if count != 1 { + t.Fatal("should have permissions") + } + count = (<-store.Channel().CheckPermissionsTo("junk", o1.Id, m1.UserId)).Data.(int64) if count != 0 { t.Fatal("shouldn't have permissions") @@ -479,11 +484,21 @@ func TestChannelStorePermissionsTo(t *testing.T) { t.Fatal("shouldn't have permissions") } + count = (<-store.Channel().CheckPermissionsToNoTeam("junk", m1.UserId)).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + count = (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, "junk")).Data.(int64) if count != 0 { t.Fatal("shouldn't have permissions") } + count = (<-store.Channel().CheckPermissionsToNoTeam(o1.Id, "junk")).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + channelId := (<-store.Channel().CheckPermissionsToByName(o1.TeamId, o1.Name, m1.UserId)).Data.(string) if channelId != o1.Id { t.Fatal("should have permissions") @@ -786,12 +801,12 @@ func TestGetMemberCount(t *testing.T) { t.Logf("c1.Id = %v", c1.Id) - u1 := model.User{ - TeamId: teamId, + u1 := &model.User{ Email: model.NewId(), DeleteAt: 0, } - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) m1 := model.ChannelMember{ ChannelId: c1.Id, @@ -807,11 +822,11 @@ func TestGetMemberCount(t *testing.T) { } u2 := model.User{ - TeamId: teamId, Email: model.NewId(), DeleteAt: 0, } Must(store.User().Save(&u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) m2 := model.ChannelMember{ ChannelId: c1.Id, @@ -828,11 +843,11 @@ func TestGetMemberCount(t *testing.T) { // make sure members of other channels aren't counted u3 := model.User{ - TeamId: teamId, Email: model.NewId(), DeleteAt: 0, } Must(store.User().Save(&u3)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id})) m3 := model.ChannelMember{ ChannelId: c2.Id, @@ -848,12 +863,12 @@ func TestGetMemberCount(t *testing.T) { } // make sure inactive users aren't counted - u4 := model.User{ - TeamId: teamId, + u4 := &model.User{ Email: model.NewId(), DeleteAt: 10000, } - Must(store.User().Save(&u4)) + Must(store.User().Save(u4)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id})) m4 := model.ChannelMember{ ChannelId: c1.Id, @@ -892,12 +907,12 @@ func TestUpdateExtrasByUser(t *testing.T) { t.Logf("c1.Id = %v", c1.Id) - u1 := model.User{ - TeamId: teamId, + u1 := &model.User{ Email: model.NewId(), DeleteAt: 0, } - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) m1 := model.ChannelMember{ ChannelId: c1.Id, @@ -907,7 +922,7 @@ func TestUpdateExtrasByUser(t *testing.T) { Must(store.Channel().SaveMember(&m1)) u1.DeleteAt = model.GetMillis() - Must(store.User().Update(&u1, true)) + Must(store.User().Update(u1, true)) if result := <-store.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil { t.Fatal("failed to update extras by user: %v", result.Err) @@ -920,7 +935,7 @@ func TestUpdateExtrasByUser(t *testing.T) { } u1.DeleteAt = 0 - Must(store.User().Update(&u1, true)) + Must(store.User().Update(u1, true)) if result := <-store.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil { t.Fatal("failed to update extras by user: %v", result.Err) diff --git a/store/sql_command_store_test.go b/store/sql_command_store_test.go index 644ebc9ae..ae1c61df3 100644 --- a/store/sql_command_store_test.go +++ b/store/sql_command_store_test.go @@ -16,6 +16,7 @@ func TestCommandStoreSave(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" if err := (<-store.Command().Save(&o1)).Err; err != nil { t.Fatal("couldn't save item", err) @@ -34,6 +35,7 @@ func TestCommandStoreGet(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) @@ -58,6 +60,7 @@ func TestCommandStoreGetByTeam(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) @@ -86,6 +89,7 @@ func TestCommandStoreDelete(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) @@ -115,6 +119,7 @@ func TestCommandStoreDeleteByUser(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) @@ -144,6 +149,7 @@ func TestCommandStoreUpdate(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) @@ -162,6 +168,7 @@ func TestCommandCount(t *testing.T) { o1.Method = model.COMMAND_METHOD_POST o1.TeamId = model.NewId() o1.URL = "http://nowhere.com/" + o1.Trigger = "trigger" o1 = (<-store.Command().Save(o1)).Data.(*model.Command) diff --git a/store/sql_compliance_store_test.go b/store/sql_compliance_store_test.go index 1a41fa389..b7b270a42 100644 --- a/store/sql_compliance_store_test.go +++ b/store/sql_compliance_store_test.go @@ -58,16 +58,16 @@ func TestComplianceExport(t *testing.T) { t1 = Must(store.Team().Save(t1)).(*model.Team) u1 := &model.User{} - u1.TeamId = t1.Id u1.Email = model.NewId() u1.Username = model.NewId() u1 = Must(store.User().Save(u1)).(*model.User) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id})) u2 := &model.User{} - u2.TeamId = t1.Id u2.Email = model.NewId() u2.Username = model.NewId() u2 = Must(store.User().Save(u2)).(*model.User) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id})) c1 := &model.Channel{} c1.TeamId = t1.Id diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 401306862..54b526191 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -652,7 +652,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP ChannelMembers WHERE Id = ChannelId - AND TeamId = :TeamId + AND (TeamId = :TeamId OR TeamId = '') AND UserId = :UserId AND DeleteAt = 0 CHANNEL_FILTER) @@ -693,9 +693,11 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP SELECT Id FROM - Users + Users, + TeamMembers WHERE - TeamId = :TeamId + TeamMembers.TeamId = :TeamId + AND Users.Id = TeamMembers.UserId AND Username IN (`+inClause+`))`, 1) } else if len(params.FromUsers) == 1 { queryParams["FromUser"] = params.FromUsers[0] @@ -704,9 +706,11 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP SELECT Id FROM - Users + Users, + TeamMembers WHERE - TeamId = :TeamId + TeamMembers.TeamId = :TeamId + AND Users.Id = TeamMembers.UserId AND Username = :FromUser)`, 1) } else { searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1) diff --git a/store/sql_recovery_store.go b/store/sql_recovery_store.go new file mode 100644 index 000000000..17a444ebb --- /dev/null +++ b/store/sql_recovery_store.go @@ -0,0 +1,124 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type SqlPasswordRecoveryStore struct { + *SqlStore +} + +func NewSqlPasswordRecoveryStore(sqlStore *SqlStore) PasswordRecoveryStore { + s := &SqlPasswordRecoveryStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.PasswordRecovery{}, "PasswordRecovery").SetKeys(false, "UserId") + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("Code").SetMaxSize(128) + } + + return s +} + +func (s SqlPasswordRecoveryStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlPasswordRecoveryStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_password_recovery_code", "PasswordRecovery", "Code") +} + +func (s SqlPasswordRecoveryStore) SaveOrUpdate(recovery *model.PasswordRecovery) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + recovery.PreSave() + if result.Err = recovery.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetReplica().SelectOne(&model.PasswordRecovery{}, "SELECT * FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": recovery.UserId}); err == nil { + if _, err := s.GetMaster().Update(recovery); err != nil { + result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.SaveOrUpdate", "store.sql_recover.update.app_error", nil, "") + } + } else { + if err := s.GetMaster().Insert(recovery); err != nil { + result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.SaveOrUpdate", "store.sql_recover.save.app_error", nil, "") + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPasswordRecoveryStore) Delete(userId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := s.GetMaster().Exec("DELETE FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.Delete", "store.sql_recover.delete.app_error", nil, "") + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPasswordRecoveryStore) Get(userId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + recovery := model.PasswordRecovery{} + + if err := s.GetReplica().SelectOne(&recovery, "SELECT * FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.Get", "store.sql_recover.get.app_error", nil, "") + } + + result.Data = &recovery + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPasswordRecoveryStore) GetByCode(code string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + recovery := model.PasswordRecovery{} + + if err := s.GetReplica().SelectOne(&recovery, "SELECT * FROM PasswordRecovery WHERE Code = :Code", map[string]interface{}{"Code": code}); err != nil { + result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.GetByCode", "store.sql_recover.get_by_code.app_error", nil, "") + } + + result.Data = &recovery + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_recovery_store_test.go b/store/sql_recovery_store_test.go new file mode 100644 index 000000000..cf1048482 --- /dev/null +++ b/store/sql_recovery_store_test.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestSqlPasswordRecoveryGet(t *testing.T) { + Setup() + + recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} + Must(store.PasswordRecovery().SaveOrUpdate(recovery)) + + result := <-store.PasswordRecovery().Get(recovery.UserId) + rrecovery := result.Data.(*model.PasswordRecovery) + if rrecovery.Code != recovery.Code { + t.Fatal("codes didn't match") + } + + result2 := <-store.PasswordRecovery().GetByCode(recovery.Code) + rrecovery2 := result2.Data.(*model.PasswordRecovery) + if rrecovery2.Code != recovery.Code { + t.Fatal("codes didn't match") + } +} + +func TestSqlPasswordRecoverySaveOrUpdate(t *testing.T) { + Setup() + + recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} + + if err := (<-store.PasswordRecovery().SaveOrUpdate(recovery)).Err; err != nil { + t.Fatal(err) + } + + // not duplicate, testing update + if err := (<-store.PasswordRecovery().SaveOrUpdate(recovery)).Err; err != nil { + t.Fatal(err) + } +} + +func TestSqlPasswordRecoveryDelete(t *testing.T) { + Setup() + + recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} + Must(store.PasswordRecovery().SaveOrUpdate(recovery)) + + if err := (<-store.PasswordRecovery().Delete(recovery.UserId)).Err; err != nil { + t.Fatal(err) + } +} diff --git a/store/sql_session_store.go b/store/sql_session_store.go index 337ad16e6..525d0e5b2 100644 --- a/store/sql_session_store.go +++ b/store/sql_session_store.go @@ -21,7 +21,6 @@ func NewSqlSessionStore(sqlStore *SqlStore) SessionStore { table.ColMap("Id").SetMaxSize(26) table.ColMap("Token").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26) - table.ColMap("TeamId").SetMaxSize(26) table.ColMap("DeviceId").SetMaxSize(512) table.ColMap("Roles").SetMaxSize(64) table.ColMap("Props").SetMaxSize(1000) @@ -63,12 +62,22 @@ func (me SqlSessionStore) Save(session *model.Session) StoreChannel { l4g.Error(utils.T("store.sql_session.save.cleanup.error"), cur.Err) } + tcs := me.Team().GetTeamsForUser(session.UserId) + if err := me.GetMaster().Insert(session); err != nil { result.Err = model.NewLocAppError("SqlSessionStore.Save", "store.sql_session.save.app_error", nil, "id="+session.Id+", "+err.Error()) + return } else { result.Data = session } + if rtcs := <-tcs; rtcs.Err != nil { + result.Err = model.NewLocAppError("SqlSessionStore.Save", "store.sql_session.save.app_error", nil, "id="+session.Id+", "+rtcs.Err.Error()) + return + } else { + session.TeamMembers = rtcs.Data.([]*model.TeamMember) + } + storeChannel <- result close(storeChannel) }() @@ -91,6 +100,14 @@ func (me SqlSessionStore) Get(sessionIdOrToken string) StoreChannel { result.Err = model.NewLocAppError("SqlSessionStore.Get", "store.sql_session.get.app_error", nil, "sessionIdOrToken="+sessionIdOrToken) } else { result.Data = sessions[0] + + tcs := me.Team().GetTeamsForUser(sessions[0].UserId) + if rtcs := <-tcs; rtcs.Err != nil { + result.Err = model.NewLocAppError("SqlSessionStore.Get", "store.sql_session.get.app_error", nil, "sessionIdOrToken="+sessionIdOrToken+", "+rtcs.Err.Error()) + return + } else { + sessions[0].TeamMembers = rtcs.Data.([]*model.TeamMember) + } } storeChannel <- result @@ -111,9 +128,10 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel { } result := StoreResult{} - var sessions []*model.Session + tcs := me.Team().GetTeamsForUser(userId) + if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = :UserId ORDER BY LastActivityAt DESC", map[string]interface{}{"UserId": userId}); err != nil { result.Err = model.NewLocAppError("SqlSessionStore.GetSessions", "store.sql_session.get_sessions.app_error", nil, err.Error()) } else { @@ -121,6 +139,15 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel { result.Data = sessions } + if rtcs := <-tcs; rtcs.Err != nil { + result.Err = model.NewLocAppError("SqlSessionStore.GetSessions", "store.sql_session.get_sessions.app_error", nil, rtcs.Err.Error()) + return + } else { + for _, session := range sessions { + session.TeamMembers = rtcs.Data.([]*model.TeamMember) + } + } + storeChannel <- result close(storeChannel) }() @@ -146,15 +173,15 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel { return storeChannel } -func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel { +func (me SqlSessionStore) RemoveAllSessions() StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}) + _, err := me.GetMaster().Exec("DELETE FROM Sessions") if err != nil { - result.Err = model.NewLocAppError("SqlSessionStore.RemoveAllSessionsForTeam", "store.sql_session.remove_all_sessions_for_team.app_error", nil, "id="+teamId+", err="+err.Error()) + result.Err = model.NewLocAppError("SqlSessionStore.RemoveAllSessions", "store.sql_session.remove_all_sessions_for_team.app_error", nil, err.Error()) } storeChannel <- result @@ -256,7 +283,7 @@ func (me SqlSessionStore) UpdateDeviceId(id, deviceId string) StoreChannel { return storeChannel } -func (me SqlSessionStore) AnalyticsSessionCount(teamId string) StoreChannel { +func (me SqlSessionStore) AnalyticsSessionCount() StoreChannel { storeChannel := make(StoreChannel) go func() { @@ -269,11 +296,7 @@ func (me SqlSessionStore) AnalyticsSessionCount(teamId string) StoreChannel { Sessions WHERE ExpiresAt > :Time` - if len(teamId) > 0 { - query += " AND TeamId = :TeamId" - } - - if c, err := me.GetReplica().SelectInt(query, map[string]interface{}{"Time": model.GetMillis(), "TeamId": teamId}); err != nil { + if c, err := me.GetReplica().SelectInt(query, map[string]interface{}{"Time": model.GetMillis()}); err != nil { result.Err = model.NewLocAppError("SqlSessionStore.AnalyticsSessionCount", "store.sql_session.analytics_session_count.app_error", nil, err.Error()) } else { result.Data = c diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go index 506695f0e..d7f07254d 100644 --- a/store/sql_session_store_test.go +++ b/store/sql_session_store_test.go @@ -13,7 +13,6 @@ func TestSessionStoreSave(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() if err := (<-store.Session().Save(&s1)).Err; err != nil { t.Fatal(err) @@ -25,17 +24,14 @@ func TestSessionGet(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) s2 := model.Session{} s2.UserId = s1.UserId - s2.TeamId = s1.TeamId Must(store.Session().Save(&s2)) s3 := model.Session{} s3.UserId = s1.UserId - s3.TeamId = s1.TeamId s3.ExpiresAt = 1 Must(store.Session().Save(&s3)) @@ -62,7 +58,6 @@ func TestSessionRemove(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { @@ -85,7 +80,6 @@ func TestSessionRemoveAll(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { @@ -96,7 +90,7 @@ func TestSessionRemoveAll(t *testing.T) { } } - Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId)) + Must(store.Session().RemoveAllSessions()) if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil { t.Fatal("should have been removed") @@ -108,7 +102,6 @@ func TestSessionRemoveByUser(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { @@ -131,7 +124,6 @@ func TestSessionRemoveToken(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { @@ -162,7 +154,6 @@ func TestSessionUpdateDeviceId(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if rs1 := (<-store.Session().UpdateDeviceId(s1.Id, model.PUSH_NOTIFY_APPLE+":1234567890")); rs1.Err != nil { @@ -171,7 +162,6 @@ func TestSessionUpdateDeviceId(t *testing.T) { s2 := model.Session{} s2.UserId = model.NewId() - s2.TeamId = model.NewId() Must(store.Session().Save(&s2)) if rs2 := (<-store.Session().UpdateDeviceId(s2.Id, model.PUSH_NOTIFY_APPLE+":1234567890")); rs2.Err != nil { @@ -184,7 +174,6 @@ func TestSessionStoreUpdateLastActivityAt(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() Must(store.Session().Save(&s1)) if err := (<-store.Session().UpdateLastActivityAt(s1.Id, 1234567890)).Err; err != nil { @@ -206,23 +195,14 @@ func TestSessionCount(t *testing.T) { s1 := model.Session{} s1.UserId = model.NewId() - s1.TeamId = model.NewId() s1.ExpiresAt = model.GetMillis() + 100000 Must(store.Session().Save(&s1)) - if r1 := <-store.Session().AnalyticsSessionCount(""); r1.Err != nil { + if r1 := <-store.Session().AnalyticsSessionCount(); r1.Err != nil { t.Fatal(r1.Err) } else { if r1.Data.(int64) == 0 { t.Fatal("should have at least 1 session") } } - - if r2 := <-store.Session().AnalyticsSessionCount(s1.TeamId); r2.Err != nil { - t.Fatal(r2.Err) - } else { - if r2.Data.(int64) != 1 { - t.Fatal("should have 1 session") - } - } } diff --git a/store/sql_store.go b/store/sql_store.go index 8ff5da6f7..688e1b116 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -36,25 +36,26 @@ const ( ) type SqlStore struct { - master *gorp.DbMap - replicas []*gorp.DbMap - team TeamStore - channel ChannelStore - post PostStore - user UserStore - audit AuditStore - compliance ComplianceStore - session SessionStore - oauth OAuthStore - system SystemStore - webhook WebhookStore - command CommandStore - preference PreferenceStore - license LicenseStore -} - -func NewSqlStore() Store { - + master *gorp.DbMap + replicas []*gorp.DbMap + team TeamStore + channel ChannelStore + post PostStore + user UserStore + audit AuditStore + compliance ComplianceStore + session SessionStore + oauth OAuthStore + system SystemStore + webhook WebhookStore + command CommandStore + preference PreferenceStore + license LicenseStore + recovery PasswordRecoveryStore + SchemaVersion string +} + +func initConnection() *SqlStore { sqlStore := &SqlStore{} sqlStore.master = setupConnection("master", utils.Cfg.SqlSettings.DriverName, @@ -75,25 +76,43 @@ func NewSqlStore() Store { } } - schemaVersion := sqlStore.GetCurrentSchemaVersion() + sqlStore.SchemaVersion = sqlStore.GetCurrentSchemaVersion() + return sqlStore +} + +func NewSqlStore() Store { + + sqlStore := initConnection() // If the version is already set then we are potentially in an 'upgrade needed' state - if schemaVersion != "" { + if sqlStore.SchemaVersion != "" { // Check to see if it's the most current database schema version - if !model.IsCurrentVersion(schemaVersion) { + if !model.IsCurrentVersion(sqlStore.SchemaVersion) { // If we are upgrading from the previous version then print a warning and continue - if model.IsPreviousVersionsSupported(schemaVersion) { - l4g.Warn(utils.T("store.sql.schema_out_of_date.warn"), schemaVersion) + if model.IsPreviousVersionsSupported(sqlStore.SchemaVersion) { + l4g.Warn(utils.T("store.sql.schema_out_of_date.warn"), sqlStore.SchemaVersion) l4g.Warn(utils.T("store.sql.schema_upgrade_attempt.warn"), model.CurrentVersion) } else { // If this is an 'upgrade needed' state but the user is attempting to skip a version then halt the world - l4g.Critical(utils.T("store.sql.schema_version.critical"), schemaVersion) + l4g.Critical(utils.T("store.sql.schema_version.critical"), sqlStore.SchemaVersion) time.Sleep(time.Second) - panic(fmt.Sprintf(utils.T("store.sql.schema_version.critical"), schemaVersion)) + panic(fmt.Sprintf(utils.T("store.sql.schema_version.critical"), sqlStore.SchemaVersion)) } } } + // This is a speical case for upgrading the schema to the 3.0 user model + // ADDED for 3.0 REMOVE for 3.4 + if sqlStore.SchemaVersion == "2.2.0" || + sqlStore.SchemaVersion == "2.1.0" || + sqlStore.SchemaVersion == "2.0.0" { + l4g.Critical("The database version of %v cannot be automatically upgraded to 3.0 schema", sqlStore.SchemaVersion) + l4g.Critical("You will need to run the command line tool './platform -upgrade_db_30'") + l4g.Critical("Please see 'http://www.mattermost.org/upgrade-to-3-0/' for more information on how to upgrade.") + time.Sleep(time.Second) + os.Exit(1) + } + sqlStore.team = NewSqlTeamStore(sqlStore) sqlStore.channel = NewSqlChannelStore(sqlStore) sqlStore.post = NewSqlPostStore(sqlStore) @@ -107,10 +126,13 @@ func NewSqlStore() Store { sqlStore.command = NewSqlCommandStore(sqlStore) sqlStore.preference = NewSqlPreferenceStore(sqlStore) sqlStore.license = NewSqlLicenseStore(sqlStore) + sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore) err := sqlStore.master.CreateTablesIfNotExists() if err != nil { l4g.Critical(utils.T("store.sql.creating_tables.critical"), err) + time.Sleep(time.Second) + os.Exit(1) } sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded() @@ -126,6 +148,7 @@ func NewSqlStore() Store { sqlStore.command.(*SqlCommandStore).UpgradeSchemaIfNeeded() sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded() sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded() + sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() @@ -140,22 +163,44 @@ func NewSqlStore() Store { sqlStore.command.(*SqlCommandStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists() + sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() - if model.IsPreviousVersionsSupported(schemaVersion) && !model.IsCurrentVersion(schemaVersion) { + if model.IsPreviousVersionsSupported(sqlStore.SchemaVersion) && !model.IsCurrentVersion(sqlStore.SchemaVersion) { sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion}) + sqlStore.SchemaVersion = model.CurrentVersion l4g.Warn(utils.T("store.sql.upgraded.warn"), model.CurrentVersion) } - if schemaVersion == "" { + if sqlStore.SchemaVersion == "" { sqlStore.system.Save(&model.System{Name: "Version", Value: model.CurrentVersion}) + sqlStore.SchemaVersion = model.CurrentVersion l4g.Info(utils.T("store.sql.schema_set.info"), model.CurrentVersion) } return sqlStore } +// ADDED for 3.0 REMOVE for 3.4 +// This is a speical case for upgrading the schema to the 3.0 user model +func NewSqlStoreForUpgrade30() *SqlStore { + sqlStore := initConnection() + + sqlStore.team = NewSqlTeamStore(sqlStore) + sqlStore.user = NewSqlUserStore(sqlStore) + sqlStore.system = NewSqlSystemStore(sqlStore) + + err := sqlStore.master.CreateTablesIfNotExists() + if err != nil { + l4g.Critical(utils.T("store.sql.creating_tables.critical"), err) + time.Sleep(time.Second) + os.Exit(1) + } + + return sqlStore +} + func setupConnection(con_type string, driver string, dataSource string, maxIdle int, maxOpen int, trace bool) *gorp.DbMap { db, err := dbsql.Open(driver, dataSource) @@ -426,15 +471,24 @@ func (ss SqlStore) AlterColumnTypeIfExists(tableName string, columnName string, return true } +func (ss SqlStore) CreateUniqueIndexIfNotExists(indexName string, tableName string, columnName string) { + ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT, true) +} + func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) { - ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT) + ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT, false) } func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName string, columnName string) { - ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_FULL_TEXT) + ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_FULL_TEXT, false) } -func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, indexType string) { +func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, indexType string, unique bool) { + + uniqueStr := "" + if unique { + uniqueStr = "UNIQUE " + } if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { _, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName) @@ -447,7 +501,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co if indexType == INDEX_TYPE_FULL_TEXT { query = "CREATE INDEX " + indexName + " ON " + tableName + " USING gin(to_tsvector('english', " + columnName + "))" } else { - query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")" + query = "CREATE " + uniqueStr + "INDEX " + indexName + " ON " + tableName + " (" + columnName + ")" } _, err = ss.GetMaster().Exec(query) @@ -474,7 +528,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co fullTextIndex = " FULLTEXT " } - _, err = ss.GetMaster().Exec("CREATE " + fullTextIndex + " INDEX " + indexName + " ON " + tableName + " (" + columnName + ")") + _, err = ss.GetMaster().Exec("CREATE " + uniqueStr + fullTextIndex + " INDEX " + indexName + " ON " + tableName + " (" + columnName + ")") if err != nil { l4g.Critical(utils.T("store.sql.create_index.critical"), err) time.Sleep(time.Second) @@ -623,6 +677,14 @@ func (ss SqlStore) License() LicenseStore { return ss.license } +func (ss SqlStore) PasswordRecovery() PasswordRecoveryStore { + return ss.recovery +} + +func (ss SqlStore) DropAllTables() { + ss.master.TruncateTables() +} + type mattermConverter struct{} func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { diff --git a/store/sql_team_store.go b/store/sql_team_store.go index 8a345bfa0..c17a45d97 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -6,7 +6,6 @@ package store import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "strings" ) type SqlTeamStore struct { @@ -25,6 +24,11 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { table.ColMap("CompanyName").SetMaxSize(64) table.ColMap("AllowedDomains").SetMaxSize(500) table.ColMap("InviteId").SetMaxSize(32) + + tablem := db.AddTableWithName(model.TeamMember{}, "TeamMembers").SetKeys(false, "TeamId", "UserId") + tablem.ColMap("TeamId").SetMaxSize(26) + tablem.ColMap("UserId").SetMaxSize(26) + tablem.ColMap("Roles").SetMaxSize(64) } return s @@ -36,6 +40,9 @@ func (s SqlTeamStore) UpgradeSchemaIfNeeded() { func (s SqlTeamStore) CreateIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_teams_name", "Teams", "Name") s.CreateIndexIfNotExists("idx_teams_invite_id", "Teams", "InviteId") + + s.CreateIndexIfNotExists("idx_teammembers_team_id", "TeamMembers", "TeamId") + s.CreateIndexIfNotExists("idx_teammembers_user_id", "TeamMembers", "UserId") } func (s SqlTeamStore) Save(team *model.Team) StoreChannel { @@ -218,17 +225,15 @@ func (s SqlTeamStore) GetByName(name string) StoreChannel { return storeChannel } -func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel { +func (s SqlTeamStore) GetAll() StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - email = strings.ToLower(email) - var data []*model.Team - if _, err := s.GetReplica().Select(&data, "SELECT Teams.* FROM Teams, Users WHERE Teams.Id = Users.TeamId AND Users.Email = :Email", map[string]interface{}{"Email": email}); err != nil { - result.Err = model.NewLocAppError("SqlTeamStore.GetTeamsForEmail", "store.sql_team.get_teams_for_email.app_error", nil, "email="+email+", "+err.Error()) + if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil { + result.Err = model.NewLocAppError("SqlTeamStore.GetAllTeams", "store.sql_team.get_all.app_error", nil, err.Error()) } for _, team := range data { @@ -246,15 +251,15 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel { return storeChannel } -func (s SqlTeamStore) GetAll() StoreChannel { +func (s SqlTeamStore) GetTeamsByUserId(userId string) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} var data []*model.Team - if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil { - result.Err = model.NewLocAppError("SqlTeamStore.GetAllTeams", "store.sql_team.get_all.app_error", nil, err.Error()) + if _, err := s.GetReplica().Select(&data, "SELECT Teams.* FROM Teams, TeamMembers WHERE TeamMembers.TeamId = Teams.Id AND TeamMembers.UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlTeamStore.GetTeamsByUserId", "store.sql_team.get_all.app_error", nil, err.Error()) } for _, team := range data { @@ -278,10 +283,10 @@ func (s SqlTeamStore) GetAllTeamListing() StoreChannel { go func() { result := StoreResult{} - query := "SELECT * FROM Teams WHERE AllowTeamListing = 1" + query := "SELECT * FROM Teams WHERE AllowOpenInvite = 1" if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { - query = "SELECT * FROM Teams WHERE AllowTeamListing = true" + query = "SELECT * FROM Teams WHERE AllowOpenInvite = true" } var data []*model.Team @@ -339,3 +344,165 @@ func (s SqlTeamStore) AnalyticsTeamCount() StoreChannel { return storeChannel } + +func (s SqlTeamStore) SaveMember(member *model.TeamMember) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = member.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if count, err := s.GetMaster().SelectInt("SELECT COUNT(0) FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": member.TeamId}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.member_count.app_error", nil, "teamId="+member.TeamId+", "+err.Error()) + storeChannel <- result + close(storeChannel) + return + } else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam { + result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.max_accounts.app_error", nil, "teamId="+member.TeamId) + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(member); err != nil { + if IsUniqueConstraintError(err.Error(), "TeamId", "teammembers_pkey") { + result.Err = model.NewLocAppError("SqlTeamStore.SaveMember", "store.sql_team.save_member.exists.app_error", nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error()) + } else { + result.Err = model.NewLocAppError("SqlTeamStore.SaveMember", "store.sql_team.save_member.save.app_error", nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error()) + } + } else { + result.Data = member + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) UpdateMember(member *model.TeamMember) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = member.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if _, err := s.GetMaster().Update(member); err != nil { + result.Err = model.NewLocAppError("SqlTeamStore.UpdateMember", "store.sql_team.save_member.save.app_error", nil, err.Error()) + } else { + result.Data = member + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) GetMembers(teamId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var members []*model.TeamMember + _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}) + if err != nil { + result.Err = model.NewLocAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "teamId="+teamId+" "+err.Error()) + } else { + result.Data = members + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) GetTeamsForUser(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var members []*model.TeamMember + _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}) + if err != nil { + result.Err = model.NewLocAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "userId="+userId+" "+err.Error()) + } else { + result.Data = members + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) RemoveMember(teamId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE TeamId = :TeamId AND UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + if err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) RemoveAllMembersByTeam(teamId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}) + if err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) RemoveAllMembersByUser(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}) + if err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go index 743ef053f..d5ee15bc6 100644 --- a/store/sql_team_store_test.go +++ b/store/sql_team_store_test.go @@ -14,7 +14,7 @@ func TestTeamStoreSave(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN @@ -37,7 +37,7 @@ func TestTeamStoreUpdate(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN if err := (<-store.Team().Save(&o1)).Err; err != nil { @@ -66,7 +66,7 @@ func TestTeamStoreUpdateDisplayName(t *testing.T) { o1 := &model.Team{} o1.DisplayName = "Display Name" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN o1 = (<-store.Team().Save(o1)).Data.(*model.Team) @@ -88,7 +88,7 @@ func TestTeamStoreGet(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN Must(store.Team().Save(&o1)) @@ -111,7 +111,7 @@ func TestTeamStoreGetByName(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN @@ -137,7 +137,7 @@ func TestTeamStoreGetByIniviteId(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN o1.InviteId = model.NewId() @@ -180,33 +180,32 @@ func TestTeamStoreGetByIniviteId(t *testing.T) { } } -func TestTeamStoreGetForEmail(t *testing.T) { +func TestTeamStoreByUserId(t *testing.T) { Setup() - o1 := model.Team{} + o1 := &model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN - Must(store.Team().Save(&o1)) + o1.InviteId = model.NewId() + o1 = Must(store.Team().Save(o1)).(*model.Team) - u1 := model.User{} - u1.TeamId = o1.Id - u1.Email = model.NewId() - Must(store.User().Save(&u1)) + m1 := &model.TeamMember{TeamId: o1.Id, UserId: model.NewId()} + Must(store.Team().SaveMember(m1)) - if r1 := <-store.Team().GetTeamsForEmail(u1.Email); r1.Err != nil { + if r1 := <-store.Team().GetTeamsByUserId(m1.UserId); r1.Err != nil { t.Fatal(r1.Err) } else { teams := r1.Data.([]*model.Team) + if len(teams) == 0 { + t.Fatal("Should return a team") + } if teams[0].Id != o1.Id { - t.Fatal("failed to lookup by email") + t.Fatal("should be a member") } - } - if r1 := <-store.Team().GetTeamsForEmail("missing"); r1.Err != nil { - t.Fatal(r1.Err) } } @@ -215,10 +214,10 @@ func TestAllTeamListing(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN - o1.AllowTeamListing = true + o1.AllowOpenInvite = true Must(store.Team().Save(&o1)) o2 := model.Team{} @@ -244,10 +243,10 @@ func TestDelete(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN - o1.AllowTeamListing = true + o1.AllowOpenInvite = true Must(store.Team().Save(&o1)) o2 := model.Team{} @@ -267,10 +266,10 @@ func TestTeamCount(t *testing.T) { o1 := model.Team{} o1.DisplayName = "DisplayName" - o1.Name = "a" + model.NewId() + "b" + o1.Name = "z-z-z" + model.NewId() + "b" o1.Email = model.NewId() + "@nowhere.com" o1.Type = model.TEAM_OPEN - o1.AllowTeamListing = true + o1.AllowOpenInvite = true Must(store.Team().Save(&o1)) if r1 := <-store.Team().AnalyticsTeamCount(); r1.Err != nil { @@ -281,3 +280,126 @@ func TestTeamCount(t *testing.T) { } } } + +func TestTeamMembers(t *testing.T) { + Setup() + + teamId1 := model.NewId() + teamId2 := model.NewId() + + m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()} + m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()} + m3 := &model.TeamMember{TeamId: teamId2, UserId: model.NewId()} + + if r1 := <-store.Team().SaveMember(m1); r1.Err != nil { + t.Fatal(r1.Err) + } + + Must(store.Team().SaveMember(m2)) + Must(store.Team().SaveMember(m3)) + + if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 2 { + t.Fatal() + } + } + + if r1 := <-store.Team().GetMembers(teamId2); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 1 { + t.Fatal() + } + + if ms[0].UserId != m3.UserId { + t.Fatal() + + } + } + + if r1 := <-store.Team().GetTeamsForUser(m1.UserId); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 1 { + t.Fatal() + } + + if ms[0].TeamId != m1.TeamId { + t.Fatal() + + } + } + + if r1 := <-store.Team().RemoveMember(teamId1, m1.UserId); r1.Err != nil { + t.Fatal(r1.Err) + } + + if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 1 { + t.Fatal() + } + + if ms[0].UserId != m2.UserId { + t.Fatal() + + } + } + + Must(store.Team().SaveMember(m1)) + + if r1 := <-store.Team().RemoveAllMembersByTeam(teamId1); r1.Err != nil { + t.Fatal(r1.Err) + } + + if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 0 { + t.Fatal() + } + } + + uid := model.NewId() + m4 := &model.TeamMember{TeamId: teamId1, UserId: uid} + m5 := &model.TeamMember{TeamId: teamId2, UserId: uid} + Must(store.Team().SaveMember(m4)) + Must(store.Team().SaveMember(m5)) + + if r1 := <-store.Team().GetTeamsForUser(uid); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 2 { + t.Fatal() + } + } + + if r1 := <-store.Team().RemoveAllMembersByUser(uid); r1.Err != nil { + t.Fatal(r1.Err) + } + + if r1 := <-store.Team().GetTeamsForUser(m1.UserId); r1.Err != nil { + t.Fatal(r1.Err) + } else { + ms := r1.Data.([]*model.TeamMember) + + if len(ms) != 0 { + t.Fatal() + } + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 2b52dfbd7..ea83458e9 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -6,9 +6,10 @@ package store import ( "database/sql" "fmt" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" + "strconv" "strings" + + "github.com/mattermost/platform/model" ) const ( @@ -26,12 +27,11 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { for _, db := range sqlStore.GetAllConns() { table := db.AddTableWithName(model.User{}, "Users").SetKeys(false, "Id") table.ColMap("Id").SetMaxSize(26) - table.ColMap("TeamId").SetMaxSize(26) - table.ColMap("Username").SetMaxSize(64) + table.ColMap("Username").SetMaxSize(64).SetUnique(true) table.ColMap("Password").SetMaxSize(128) table.ColMap("AuthData").SetMaxSize(128) table.ColMap("AuthService").SetMaxSize(32) - table.ColMap("Email").SetMaxSize(128) + table.ColMap("Email").SetMaxSize(128).SetUnique(true) table.ColMap("Nickname").SetMaxSize(64) table.ColMap("FirstName").SetMaxSize(64) table.ColMap("LastName").SetMaxSize(64) @@ -41,8 +41,6 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("ThemeProps").SetMaxSize(2000) table.ColMap("Locale").SetMaxSize(5) table.ColMap("MfaSecret").SetMaxSize(128) - table.SetUniqueTogether("Email", "TeamId") - table.SetUniqueTogether("Username", "TeamId") } return us @@ -51,13 +49,9 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { func (us SqlUserStore) UpgradeSchemaIfNeeded() { // ADDED for 2.0 REMOVE for 2.4 us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE) - // ADDED for 2.2 REMOVE for 2.6 - us.CreateColumnIfNotExists("Users", "MfaActive", "tinyint(1)", "boolean", "0") - us.CreateColumnIfNotExists("Users", "MfaSecret", "varchar(128)", "character varying(128)", "") } func (us SqlUserStore) CreateIndexesIfNotExists() { - us.CreateIndexIfNotExists("idx_users_team_id", "Users", "TeamId") us.CreateIndexIfNotExists("idx_users_email", "Users", "Email") } @@ -82,18 +76,6 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel { return } - if count, err := us.GetMaster().SelectInt("SELECT COUNT(0) FROM Users WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": user.TeamId}); err != nil { - result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.member_count.app_error", nil, "teamId="+user.TeamId+", "+err.Error()) - storeChannel <- result - close(storeChannel) - return - } else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam { - result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.max_accounts.app_error", nil, "teamId="+user.TeamId) - storeChannel <- result - close(storeChannel) - return - } - if err := us.GetMaster().Insert(user); err != nil { if IsUniqueConstraintError(err.Error(), "Email", "users_email_teamid_key") { result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.email_exists.app_error", nil, "user_id="+user.Id+", "+err.Error()) @@ -140,7 +122,6 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha user.Password = oldUser.Password user.LastPasswordUpdate = oldUser.LastPasswordUpdate user.LastPictureUpdate = oldUser.LastPictureUpdate - user.TeamId = oldUser.TeamId user.LastActivityAt = oldUser.LastActivityAt user.LastPingAt = oldUser.LastPingAt user.EmailVerified = oldUser.EmailVerified @@ -153,7 +134,7 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha user.DeleteAt = oldUser.DeleteAt } - if user.IsSSOUser() { + if user.IsOAuthUser() { user.Email = oldUser.Email } else if !user.IsLDAPUser() && user.Email != oldUser.Email { user.EmailVerified = false @@ -421,13 +402,76 @@ func (us SqlUserStore) Get(id string) StoreChannel { return storeChannel } +func (us SqlUserStore) GetAll() StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []*model.User + if _, err := us.GetReplica().Select(&data, "SELECT * FROM Users"); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetAll", "store.sql_user.get.app_error", nil, err.Error()) + } + + result.Data = data + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (s SqlUserStore) GetEtagForDirectProfiles(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt, err := s.GetReplica().SelectInt(` + SELECT + UpdateAt + FROM + Users + WHERE + Id IN (SELECT DISTINCT + UserId + FROM + ChannelMembers + WHERE + ChannelMembers.UserId != :UserId + AND ChannelMembers.ChannelId IN (SELECT + Channels.Id + FROM + Channels, + ChannelMembers + WHERE + Channels.Type = 'D' + AND Channels.Id = ChannelMembers.ChannelId + AND ChannelMembers.UserId = :UserId)) + `, map[string]interface{}{"UserId": userId}) + if err != nil { + result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis()) + } else { + result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users WHERE TeamId = :TeamId ORDER BY UpdateAt DESC LIMIT 1", map[string]interface{}{"TeamId": teamId}) + updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId ORDER BY UpdateAt DESC LIMIT 1", map[string]interface{}{"TeamId": teamId}) if err != nil { result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis()) } else { @@ -450,7 +494,7 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel { var users []*model.User - if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil { + if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId", map[string]interface{}{"TeamId": teamId}); err != nil { result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error()) } else { @@ -472,6 +516,99 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel { return storeChannel } +func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + + if _, err := us.GetReplica().Select(&users, ` + SELECT + Users.* + FROM + Users + WHERE + Id IN (SELECT DISTINCT + UserId + FROM + ChannelMembers + WHERE + ChannelMembers.UserId != :UserId + AND ChannelMembers.ChannelId IN (SELECT + Channels.Id + FROM + Channels, + ChannelMembers + WHERE + Channels.Type = 'D' + AND Channels.Id = ChannelMembers.ChannelId + AND ChannelMembers.UserId = :UserId))`, map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetDirectProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error()) + } else { + + userMap := make(map[string]*model.User) + + for _, u := range users { + u.Password = "" + u.AuthData = "" + userMap[u.Id] = u + } + + result.Data = userMap + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) GetProfileByIds(userIds []string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + props := make(map[string]interface{}) + idQuery := "" + + for index, userId := range userIds { + if len(idQuery) > 0 { + idQuery += ", " + } + + props["userId"+strconv.Itoa(index)] = userId + idQuery += ":userId" + strconv.Itoa(index) + } + + if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE Users.Id IN ("+idQuery+")", props); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetProfileByIds", "store.sql_user.get_profiles.app_error", nil, err.Error()) + } else { + + userMap := make(map[string]*model.User) + + for _, u := range users { + u.Password = "" + u.AuthData = "" + userMap[u.Id] = u + } + + result.Data = userMap + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel { storeChannel := make(StoreChannel) @@ -503,7 +640,7 @@ func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel { return storeChannel } -func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel { +func (us SqlUserStore) GetByEmail(email string) StoreChannel { storeChannel := make(StoreChannel) @@ -514,8 +651,8 @@ func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel { user := model.User{} - if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId = :TeamId AND Email = :Email", map[string]interface{}{"TeamId": teamId, "Email": email}); err != nil { - result.Err = model.NewLocAppError("SqlUserStore.GetByEmail", MISSING_ACCOUNT_ERROR, nil, "teamId="+teamId+", email="+email+", "+err.Error()) + if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE Email = :Email", map[string]interface{}{"Email": email}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetByEmail", MISSING_ACCOUNT_ERROR, nil, "email="+email+", "+err.Error()) } result.Data = &user @@ -527,7 +664,7 @@ func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel { return storeChannel } -func (us SqlUserStore) GetByAuth(teamId string, authData string, authService string) StoreChannel { +func (us SqlUserStore) GetByAuth(authData string, authService string) StoreChannel { storeChannel := make(StoreChannel) @@ -536,11 +673,11 @@ func (us SqlUserStore) GetByAuth(teamId string, authData string, authService str user := model.User{} - if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId = :TeamId AND AuthData = :AuthData AND AuthService = :AuthService", map[string]interface{}{"TeamId": teamId, "AuthData": authData, "AuthService": authService}); err != nil { + if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE AuthData = :AuthData AND AuthService = :AuthService", map[string]interface{}{"AuthData": authData, "AuthService": authService}); err != nil { if err == sql.ErrNoRows { - result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", MISSING_AUTH_ACCOUNT_ERROR, nil, "teamId="+teamId+", authData="+authData+", authService="+authService+", "+err.Error()) + result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", MISSING_AUTH_ACCOUNT_ERROR, nil, "authData="+authData+", authService="+authService+", "+err.Error()) } else { - result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", "store.sql_user.get_by_auth.other.app_error", nil, "teamId="+teamId+", authData="+authData+", authService="+authService+", "+err.Error()) + result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", "store.sql_user.get_by_auth.other.app_error", nil, "authData="+authData+", authService="+authService+", "+err.Error()) } } @@ -553,7 +690,7 @@ func (us SqlUserStore) GetByAuth(teamId string, authData string, authService str return storeChannel } -func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel { +func (us SqlUserStore) GetByUsername(username string) StoreChannel { storeChannel := make(StoreChannel) @@ -562,9 +699,9 @@ func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChanne user := model.User{} - if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId = :TeamId AND Username = :Username", map[string]interface{}{"TeamId": teamId, "Username": username}); err != nil { + if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE Username = :Username", map[string]interface{}{"Username": username}); err != nil { result.Err = model.NewLocAppError("SqlUserStore.GetByUsername", "store.sql_user.get_by_username.app_error", - nil, "teamId="+teamId+", username="+username+", "+err.Error()) + nil, err.Error()) } result.Data = &user @@ -604,8 +741,8 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel { var users []*model.User - if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil { - result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_for_export.app_error", nil, err.Error()) + if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId", map[string]interface{}{"TeamId": teamId}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetForExport", "store.sql_user.get_for_export.app_error", nil, err.Error()) } else { for _, u := range users { u.Password = "" @@ -690,7 +827,7 @@ func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel { query := "SELECT COUNT(DISTINCT Email) FROM Users" if len(teamId) > 0 { - query += " WHERE TeamId = :TeamId" + query += ", TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId" } v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index dcd2440ac..2d17c5888 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -13,15 +13,18 @@ import ( func TestUserStoreSave(t *testing.T) { Setup() + teamId := model.NewId() + u1 := model.User{} u1.Email = model.NewId() u1.Username = model.NewId() - u1.TeamId = model.NewId() if err := (<-store.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save user", err) } + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) + if err := (<-store.User().Save(&u1)).Err; err == nil { t.Fatal("shouldn't be able to update user from save") } @@ -49,37 +52,44 @@ func TestUserStoreSave(t *testing.T) { if err := (<-store.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save item", err) } + + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) } u1.Id = "" u1.Email = model.NewId() u1.Username = model.NewId() - if err := (<-store.User().Save(&u1)).Err; err == nil { - t.Fatal("should be the limit", err) + if err := (<-store.User().Save(&u1)).Err; err != nil { + t.Fatal("couldn't save item", err) } + + if err := (<-store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})).Err; err == nil { + t.Fatal("should be the limit") + } + } func TestUserStoreUpdate(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) time.Sleep(100 * time.Millisecond) - if err := (<-store.User().Update(&u1, false)).Err; err != nil { + if err := (<-store.User().Update(u1, false)).Err; err != nil { t.Fatal(err) } u1.Id = "missing" - if err := (<-store.User().Update(&u1, false)).Err; err == nil { + if err := (<-store.User().Update(u1, false)).Err; err == nil { t.Fatal("Update should have failed because of missing key") } u1.Id = model.NewId() - if err := (<-store.User().Update(&u1, false)).Err; err == nil { + if err := (<-store.User().Update(u1, false)).Err; err == nil { t.Fatal("Update should have faile because id change") } } @@ -87,10 +97,10 @@ func TestUserStoreUpdate(t *testing.T) { func TestUserStoreUpdateLastPingAt(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if err := (<-store.User().UpdateLastPingAt(u1.Id, 1234567890)).Err; err != nil { t.Fatal(err) @@ -109,10 +119,10 @@ func TestUserStoreUpdateLastPingAt(t *testing.T) { func TestUserStoreUpdateLastActivityAt(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if err := (<-store.User().UpdateLastActivityAt(u1.Id, 1234567890)).Err; err != nil { t.Fatal(err) @@ -131,10 +141,10 @@ func TestUserStoreUpdateLastActivityAt(t *testing.T) { func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if err := (<-store.User().UpdateFailedPasswordAttempts(u1.Id, 3)).Err; err != nil { t.Fatal(err) @@ -153,14 +163,13 @@ func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) { func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) s1 := model.Session{} s1.UserId = u1.Id - s1.TeamId = u1.TeamId Must(store.Session().Save(&s1)) if err := (<-store.User().UpdateUserAndSessionActivity(u1.Id, s1.Id, 1234567890)).Err; err != nil { @@ -188,10 +197,10 @@ func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) { func TestUserStoreGet(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if r1 := <-store.User().Get(u1.Id); r1.Err != nil { t.Fatal(r1.Err) @@ -209,10 +218,10 @@ func TestUserStoreGet(t *testing.T) { func TestUserCount(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if result := <-store.User().GetTotalUsersCount(); result.Err != nil { t.Fatal(result.Err) @@ -227,10 +236,11 @@ func TestUserCount(t *testing.T) { func TestActiveUserCount(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) + <-store.User().UpdateLastActivityAt(u1.Id, model.GetMillis()) if result := <-store.User().GetTotalActiveUsersCount(); result.Err != nil { t.Fatal(result.Err) @@ -245,17 +255,89 @@ func TestActiveUserCount(t *testing.T) { func TestUserStoreGetProfiles(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) + + u2 := &model.User{} + u2.Email = model.NewId() + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) + + if r1 := <-store.User().GetProfiles(teamId); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.(map[string]*model.User) + if len(users) != 2 { + t.Fatal("invalid returned users") + } + + if users[u1.Id].Id != u1.Id { + t.Fatal("invalid returned user") + } + } + + if r2 := <-store.User().GetProfiles("123"); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if len(r2.Data.(map[string]*model.User)) != 0 { + t.Fatal("should have returned empty map") + } + } +} + +func TestUserStoreGetDirectProfiles(t *testing.T) { + Setup() + + teamId := model.NewId() + + u1 := &model.User{} + u1.Email = model.NewId() + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) + + u2 := &model.User{} + u2.Email = model.NewId() + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) + + if r1 := <-store.User().GetDirectProfiles(u1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.(map[string]*model.User) + if len(users) != 0 { + t.Fatal("invalid returned users") + } + } + + if r2 := <-store.User().GetDirectProfiles("123"); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if len(r2.Data.(map[string]*model.User)) != 0 { + t.Fatal("should have returned empty map") + } + } +} + +func TestUserStoreGetProfilesByIds(t *testing.T) { + Setup() + + teamId := model.NewId() + + u1 := &model.User{} + u1.Email = model.NewId() + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) - u2 := model.User{} - u2.TeamId = u1.TeamId + u2 := &model.User{} u2.Email = model.NewId() - Must(store.User().Save(&u2)) + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) - if r1 := <-store.User().GetProfiles(u1.TeamId); r1.Err != nil { + if r1 := <-store.User().GetProfileByIds([]string{u1.Id, u2.Id}); r1.Err != nil { t.Fatal(r1.Err) } else { users := r1.Data.(map[string]*model.User) @@ -268,6 +350,19 @@ func TestUserStoreGetProfiles(t *testing.T) { } } + if r1 := <-store.User().GetProfileByIds([]string{u1.Id}); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.(map[string]*model.User) + if len(users) != 1 { + t.Fatal("invalid returned users") + } + + if users[u1.Id].Id != u1.Id { + t.Fatal("invalid returned user") + } + } + if r2 := <-store.User().GetProfiles("123"); r2.Err != nil { t.Fatal(r2.Err) } else { @@ -280,15 +375,18 @@ func TestUserStoreGetProfiles(t *testing.T) { func TestUserStoreGetSystemAdminProfiles(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + u1.Roles = model.ROLE_SYSTEM_ADMIN + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) - u2 := model.User{} - u2.TeamId = u1.TeamId + u2 := &model.User{} u2.Email = model.NewId() - Must(store.User().Save(&u2)) + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) if r1 := <-store.User().GetSystemAdminProfiles(); r1.Err != nil { t.Fatal(r1.Err) @@ -303,16 +401,18 @@ func TestUserStoreGetSystemAdminProfiles(t *testing.T) { func TestUserStoreGetByEmail(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamid := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamid, UserId: u1.Id})) - if err := (<-store.User().GetByEmail(u1.TeamId, u1.Email)).Err; err != nil { + if err := (<-store.User().GetByEmail(u1.Email)).Err; err != nil { t.Fatal(err) } - if err := (<-store.User().GetByEmail("", "")).Err; err == nil { + if err := (<-store.User().GetByEmail("")).Err; err == nil { t.Fatal("Should have failed because of missing email") } } @@ -320,18 +420,20 @@ func TestUserStoreGetByEmail(t *testing.T) { func TestUserStoreGetByAuthData(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - u1.AuthData = "123" + u1.AuthData = "123" + model.NewId() u1.AuthService = "service" - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) - if err := (<-store.User().GetByAuth(u1.TeamId, u1.AuthData, u1.AuthService)).Err; err != nil { + if err := (<-store.User().GetByAuth(u1.AuthData, u1.AuthService)).Err; err != nil { t.Fatal(err) } - if err := (<-store.User().GetByAuth("", "", "")).Err; err == nil { + if err := (<-store.User().GetByAuth("", "")).Err; err == nil { t.Fatal("Should have failed because of missing auth data") } } @@ -339,17 +441,19 @@ func TestUserStoreGetByAuthData(t *testing.T) { func TestUserStoreGetByUsername(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() u1.Username = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) - if err := (<-store.User().GetByUsername(u1.TeamId, u1.Username)).Err; err != nil { + if err := (<-store.User().GetByUsername(u1.Username)).Err; err != nil { t.Fatal(err) } - if err := (<-store.User().GetByUsername("", "")).Err; err == nil { + if err := (<-store.User().GetByUsername("")).Err; err == nil { t.Fatal("Should have failed because of missing username") } } @@ -357,10 +461,12 @@ func TestUserStoreGetByUsername(t *testing.T) { func TestUserStoreUpdatePassword(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) hashedPassword := model.HashPassword("newpwd") @@ -368,7 +474,7 @@ func TestUserStoreUpdatePassword(t *testing.T) { t.Fatal(err) } - if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil { + if r1 := <-store.User().GetByEmail(u1.Email); r1.Err != nil { t.Fatal(r1.Err) } else { user := r1.Data.(*model.User) @@ -381,10 +487,10 @@ func TestUserStoreUpdatePassword(t *testing.T) { func TestUserStoreDelete(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id})) if err := (<-store.User().PermanentDelete(u1.Id)).Err; err != nil { t.Fatal(err) @@ -394,10 +500,12 @@ func TestUserStoreDelete(t *testing.T) { func TestUserStoreUpdateAuthData(t *testing.T) { Setup() - u1 := model.User{} - u1.TeamId = model.NewId() + teamId := model.NewId() + + u1 := &model.User{} u1.Email = model.NewId() - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) service := "someservice" authData := "1" @@ -406,7 +514,7 @@ func TestUserStoreUpdateAuthData(t *testing.T) { t.Fatal(err) } - if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil { + if r1 := <-store.User().GetByEmail(u1.Email); r1.Err != nil { t.Fatal(r1.Err) } else { user := r1.Data.(*model.User) @@ -430,26 +538,26 @@ func TestUserUnreadCount(t *testing.T) { c1 := model.Channel{} c1.TeamId = teamId c1.DisplayName = "Unread Messages" - c1.Name = "unread-messages" + c1.Name = "unread-messages-" + model.NewId() c1.Type = model.CHANNEL_OPEN c2 := model.Channel{} c2.TeamId = teamId c2.DisplayName = "Unread Direct" - c2.Name = "unread-direct" + c2.Name = "unread-direct-" + model.NewId() c2.Type = model.CHANNEL_DIRECT - u1 := model.User{} + u1 := &model.User{} + u1.Username = "user1" + model.NewId() u1.Email = model.NewId() - u1.Username = "user1" - u1.TeamId = teamId - Must(store.User().Save(&u1)) + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) - u2 := model.User{} + u2 := &model.User{} u2.Email = model.NewId() - u2.Username = "user2" - u2.TeamId = teamId - Must(store.User().Save(&u2)) + u2.Username = "user2" + model.NewId() + Must(store.User().Save(u2)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) if err := (<-store.Channel().Save(&c1)).Err; err != nil { t.Fatal("couldn't save item", err) @@ -507,7 +615,6 @@ func TestUserStoreUpdateMfaSecret(t *testing.T) { Setup() u1 := model.User{} - u1.TeamId = model.NewId() u1.Email = model.NewId() Must(store.User().Save(&u1)) @@ -527,7 +634,6 @@ func TestUserStoreUpdateMfaActive(t *testing.T) { Setup() u1 := model.User{} - u1.TeamId = model.NewId() u1.Email = model.NewId() Must(store.User().Save(&u1)) diff --git a/store/store.go b/store/store.go index 4a4fa1481..f5c440721 100644 --- a/store/store.go +++ b/store/store.go @@ -41,8 +41,10 @@ type Store interface { Command() CommandStore Preference() PreferenceStore License() LicenseStore + PasswordRecovery() PasswordRecoveryStore MarkSystemRanUnitTests() Close() + DropAllTables() } type TeamStore interface { @@ -51,12 +53,19 @@ type TeamStore interface { UpdateDisplayName(name string, teamId string) StoreChannel Get(id string) StoreChannel GetByName(name string) StoreChannel - GetTeamsForEmail(domain string) StoreChannel GetAll() StoreChannel GetAllTeamListing() StoreChannel + GetTeamsByUserId(userId string) StoreChannel GetByInviteId(inviteId string) StoreChannel PermanentDelete(teamId string) StoreChannel AnalyticsTeamCount() StoreChannel + SaveMember(member *model.TeamMember) StoreChannel + UpdateMember(member *model.TeamMember) StoreChannel + GetMembers(teamId string) StoreChannel + GetTeamsForUser(userId string) StoreChannel + RemoveMember(teamId string, userId string) StoreChannel + RemoveAllMembersByTeam(teamId string) StoreChannel + RemoveAllMembersByUser(userId string) StoreChannel } type ChannelStore interface { @@ -82,6 +91,7 @@ type ChannelStore interface { PermanentDeleteMembersByUser(userId string) StoreChannel GetExtraMembers(channelId string, limit int) StoreChannel CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel + CheckPermissionsToNoTeam(channelId string, userId string) StoreChannel CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel UpdateLastViewedAt(channelId string, userId string) StoreChannel @@ -120,12 +130,16 @@ type UserStore interface { UpdateMfaSecret(userId, secret string) StoreChannel UpdateMfaActive(userId string, active bool) StoreChannel Get(id string) StoreChannel + GetAll() StoreChannel GetProfiles(teamId string) StoreChannel - GetByEmail(teamId string, email string) StoreChannel - GetByAuth(teamId string, authData string, authService string) StoreChannel - GetByUsername(teamId string, username string) StoreChannel + GetDirectProfiles(userId string) StoreChannel + GetProfileByIds(userId []string) StoreChannel + GetByEmail(email string) StoreChannel + GetByAuth(authData string, authService string) StoreChannel + GetByUsername(username string) StoreChannel VerifyEmail(userId string) StoreChannel GetEtagForProfiles(teamId string) StoreChannel + GetEtagForDirectProfiles(userId string) StoreChannel UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel GetForExport(teamId string) StoreChannel GetTotalUsersCount() StoreChannel @@ -133,7 +147,6 @@ type UserStore interface { GetSystemAdminProfiles() StoreChannel PermanentDelete(userId string) StoreChannel AnalyticsUniqueUserCount(teamId string) StoreChannel - GetUnreadCount(userId string) StoreChannel } @@ -142,12 +155,12 @@ type SessionStore interface { Get(sessionIdOrToken string) StoreChannel GetSessions(userId string) StoreChannel Remove(sessionIdOrToken string) StoreChannel - RemoveAllSessionsForTeam(teamId string) StoreChannel + RemoveAllSessions() StoreChannel PermanentDeleteSessionsByUser(teamId string) StoreChannel UpdateLastActivityAt(sessionId string, time int64) StoreChannel UpdateRoles(userId string, roles string) StoreChannel UpdateDeviceId(id string, deviceId string) StoreChannel - AnalyticsSessionCount(teamId string) StoreChannel + AnalyticsSessionCount() StoreChannel } type AuditStore interface { @@ -228,3 +241,10 @@ type LicenseStore interface { Save(license *model.LicenseRecord) StoreChannel Get(id string) StoreChannel } + +type PasswordRecoveryStore interface { + SaveOrUpdate(recovery *model.PasswordRecovery) StoreChannel + Delete(userId string) StoreChannel + Get(userId string) StoreChannel + GetByCode(code string) StoreChannel +} diff --git a/utils/config.go b/utils/config.go index 8ff328008..1ae658b16 100644 --- a/utils/config.go +++ b/utils/config.go @@ -57,6 +57,14 @@ func FindDir(dir string) string { return fileName + "/" } +func DisableDebugLogForTest() { + l4g.Global["stdout"].Level = l4g.WARNING +} + +func EnableDebugLogForTest() { + l4g.Global["stdout"].Level = l4g.DEBUG +} + func ConfigureCmdLineLog() { ls := model.LogSettings{} ls.EnableConsole = true @@ -199,11 +207,10 @@ func getClientConfig(c *model.Config) map[string]string { props["SiteName"] = c.TeamSettings.SiteName props["EnableTeamCreation"] = strconv.FormatBool(c.TeamSettings.EnableTeamCreation) props["EnableUserCreation"] = strconv.FormatBool(c.TeamSettings.EnableUserCreation) + props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer) props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames) - props["EnableTeamListing"] = strconv.FormatBool(*c.TeamSettings.EnableTeamListing) props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) - props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks) @@ -242,7 +249,7 @@ func getClientConfig(c *model.Config) map[string]string { props["AllowCorsFrom"] = *c.ServiceSettings.AllowCorsFrom - if License.Features != nil { + if IsLicensed { if *License.Features.CustomBrand { props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) props["CustomBrandText"] = *c.TeamSettings.CustomBrandText diff --git a/utils/license.go b/utils/license.go index ba323d9b4..d3654932f 100644 --- a/utils/license.go +++ b/utils/license.go @@ -21,7 +21,9 @@ import ( ) var IsLicensed bool = false -var License *model.License = &model.License{} +var License *model.License = &model.License{ + Features: new(model.Features), +} var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- diff --git a/utils/textgeneration.go b/utils/textgeneration.go index 31b6517b8..96c43f402 100644 --- a/utils/textgeneration.go +++ b/utils/textgeneration.go @@ -473,16 +473,16 @@ func RandString(l int, charset string) string { return string(ret) } -func RandomEmail(length Range, charset string) string { - emaillen := RandIntFromRange(length) - username := RandString(emaillen, charset) - domain := "simulator.amazonses.com" - return "success+" + username + "@" + domain -} - -func FuzzEmail() string { - return FUZZY_STRINGS_EMAILS[RandIntFromRange(Range{0, len(FUZZY_STRINGS_EMAILS) - 1})] -} +// func RandomEmail(length Range, charset string) string { +// emaillen := RandIntFromRange(length) +// username := RandString(emaillen, charset) +// domain := "simulator.amazonses.com" +// return "success+" + username + "@" + domain +// } + +// func FuzzEmail() string { +// return FUZZY_STRINGS_EMAILS[RandIntFromRange(Range{0, len(FUZZY_STRINGS_EMAILS) - 1})] +// } func RandomName(length Range, charset string) string { namelen := RandIntFromRange(length) diff --git a/web/web.go b/web/web.go index ff5040a4b..8e96edd69 100644 --- a/web/web.go +++ b/web/web.go @@ -62,5 +62,11 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { return } + + if api.IsApiCall(r) { + api.Handle404(w, r) + return + } + http.ServeFile(w, r, utils.FindDir(CLIENT_DIR)+"root.html") } diff --git a/web/web_test.go b/web/web_test.go index 8dde5d747..9d7a84c5c 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -30,6 +30,8 @@ func Setup() { ApiClient = model.NewClient(URL) api.Srv.Store.MarkSystemRanUnitTests() + + *utils.Cfg.TeamSettings.EnableOpenServer = true } } @@ -62,8 +64,9 @@ func TestGetAccessToken(t *testing.T) { team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := ApiClient.CreateTeam(&team) - user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Password: "pwd"} + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Password: "pwd"} ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User) + api.JoinUserToTeam(rteam.Data.(*model.Team), ruser) store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id)) app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -77,6 +80,7 @@ func TestGetAccessToken(t *testing.T) { } else { ApiClient.Must(ApiClient.LoginById(ruser.Id, "pwd")) + ApiClient.SetTeamId(rteam.Data.(*model.Team).Id) app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp) redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"] @@ -191,15 +195,17 @@ func TestIncomingWebhook(t *testing.T) { team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team) - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) + api.JoinUserToTeam(team, user) c := &api.Context{} c.RequestId = model.NewId() c.IpAddress = "cmd_line" api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) ApiClient.LoginByEmail(team.Name, user.Email, "pwd") + ApiClient.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel) diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index 13c9ee97f..3eb02cc40 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -19,6 +19,13 @@ "jquery": true, "es6": true }, + "globals": { + "jest": true, + "describe": true, + "it": true, + "expect": true, + "before": true + }, "rules": { "comma-dangle": [2, "never"], "array-callback-return": 2, diff --git a/webapp/Makefile b/webapp/Makefile index 6ec75d1df..b0c2c831a 100644 --- a/webapp/Makefile +++ b/webapp/Makefile @@ -1,6 +1,6 @@ .PHONY: build test run clean stop -test: +test: .npminstall @echo Checking for style guide compliance npm run check diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx index fd447ec93..5641246f9 100644 --- a/webapp/action_creators/global_actions.jsx +++ b/webapp/action_creators/global_actions.jsx @@ -4,11 +4,16 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; +import ErrorStore from 'stores/error_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import PreferenceStore from 'stores/preference_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 Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import * as Websockets from './websocket_actions.jsx'; import * as I18n from 'i18n/i18n.jsx'; @@ -21,7 +26,6 @@ export function emitChannelClickEvent(channel) { function userVisitedFakeChannel(chan, success, fail) { const otherUserId = Utils.getUserIdFromChannelName(chan); Client.createDirectChannel( - chan, otherUserId, (data) => { success(data); @@ -61,6 +65,69 @@ export function emitChannelClickEvent(channel) { } } +export function emitInitialLoad(callback) { + Client.getInitialLoad( + (data) => { + global.window.mm_config = data.client_cfg; + global.window.mm_license = data.license_cfg; + + UserStore.setNoAccounts(data.no_accounts); + + if (data.user && data.user.id) { + global.window.mm_user = data.user; + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_ME, + me: data.user + }); + } + + if (data.preferences) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PREFERENCES, + preferences: data.preferences + }); + } + + if (data.teams) { + var teams = {}; + data.teams.forEach((team) => { + teams[team.id] = team; + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_ALL_TEAMS, + teams + }); + } + + if (data.team_members) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_TEAM_MEMBERS, + team_members: data.team_members + }); + } + + if (data.direct_profiles) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_DIRECT_PROFILES, + profiles: data.direct_profiles + }); + } + + if (callback) { + callback(); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getInitialLoad'); + + if (callback) { + callback(); + } + } + ); +} + export function emitPostFocusEvent(postId) { AsyncClient.getChannels(true); Client.getPostById( @@ -80,6 +147,18 @@ export function emitPostFocusEvent(postId) { ); } +export function emitCloseRightHandSide() { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: null + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: null + }); +} + export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { Client.getPost( post.channel_id, @@ -133,21 +212,21 @@ export function emitLoadMorePostsFocusedBottomEvent() { AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); } -export function emitPostRecievedEvent(post, websocketMessageProps) { +export function emitPostRecievedEvent(post, msg) { if (ChannelStore.getCurrentId() === post.channel_id) { if (window.isActive) { AsyncClient.updateLastViewedAt(); } else { AsyncClient.getChannel(post.channel_id); } - } else { + } else if (msg && TeamStore.getCurrentId() === msg.team_id) { AsyncClient.getChannel(post.channel_id); } AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST, post, - websocketMessageProps + websocketMessageProps: msg.props }); } @@ -271,10 +350,6 @@ export function sendEphemeralPost(message, channelId) { emitPostRecievedEvent(post); } -export function loadTeamRequiredPage() { - AsyncClient.getAllTeams(); -} - export function newLocalizationSelected(locale) { if (locale === 'en') { AppDispatcher.handleServerAction({ @@ -311,8 +386,6 @@ export function loadBrowserLocale() { 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(); @@ -335,3 +408,21 @@ export function emitRemoteUserTypingEvent(channelId, userId, postParentId) { postParentId }); } + +export function emitUserLoggedOutEvent(redirectTo) { + const rURL = (redirectTo && typeof redirectTo === 'string') ? redirectTo : '/'; + Client.logout( + () => { + BrowserStore.signalLogout(); + BrowserStore.clear(); + ErrorStore.clearLastError(); + PreferenceStore.clear(); + UserStore.clear(); + TeamStore.clear(); + browserHistory.push(rURL); + }, + () => { + browserHistory.push(rURL); + } + ); +} diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/action_creators/websocket_actions.jsx index a66d79d18..c4e9c63c2 100644 --- a/webapp/action_creators/websocket_actions.jsx +++ b/webapp/action_creators/websocket_actions.jsx @@ -3,12 +3,14 @@ import $ from 'jquery'; import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-line no-unused-vars +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; @@ -31,7 +33,7 @@ export function initialize() { protocol = 'wss://'; } - const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; + const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket'; if (connectFailCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console @@ -145,6 +147,11 @@ function handleMessage(msg) { export function sendMessage(msg) { if (conn && conn.readyState === WebSocket.OPEN) { + var teamId = TeamStore.getCurrentId(); + if (teamId && teamId.length > 0) { + msg.team_id = teamId; + } + conn.send(JSON.stringify(msg)); } else if (!conn || conn.readyState === WebSocket.Closed) { conn = null; @@ -161,7 +168,7 @@ export function close() { function handleNewPostEvent(msg) { const post = JSON.parse(msg.props.post); - GlobalActions.emitPostRecievedEvent(post, msg.props); + GlobalActions.emitPostRecievedEvent(post, msg); } function handlePostEditEvent(msg) { @@ -193,7 +200,7 @@ function handleUserAddedEvent(msg) { AsyncClient.getChannelExtraInfo(); } - if (UserStore.getCurrentId() === msg.user_id) { + if (TeamStore.getCurrentId() === msg.team_id && UserStore.getCurrentId() === msg.user_id) { AsyncClient.getChannel(msg.channel_id); } } @@ -219,7 +226,7 @@ function handleUserRemovedEvent(msg) { function handleChannelViewedEvent(msg) { // Useful for when multiple devices have the app open to different channels - if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { + if (TeamStore.getCurrentId() === msg.team_id && ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { AsyncClient.getChannel(msg.channel_id); } } @@ -230,5 +237,7 @@ function handlePreferenceChangedEvent(msg) { } function handleUserTypingEvent(msg) { - GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id); + if (TeamStore.getCurrentId() === msg.team_id) { + GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id); + } } diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx new file mode 100644 index 000000000..98e660227 --- /dev/null +++ b/webapp/client/client.jsx @@ -0,0 +1,1463 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import request from 'superagent'; + +const HEADER_X_VERSION_ID = 'x-version-id'; +const HEADER_TOKEN = 'token'; +const HEADER_BEARER = 'BEARER'; +const HEADER_AUTH = 'Authorization'; + +export default class Client { + constructor() { + this.teamId = ''; + this.serverVersion = ''; + this.logToConsole = false; + this.useToken = false; + this.token = ''; + this.url = ''; + this.urlVersion = '/api/v3'; + this.defaultHeaders = { + 'X-Requested-With': 'XMLHttpRequest' + }; + + this.translations = { + connectionError: 'There appears to be a problem with your internet connection.', + unknownError: 'We received an unexpected status code from the server.' + }; + } + + setUrl = (url) => { + this.url = url; + } + + setTeamId = (id) => { + this.teamId = id; + } + + getTeamId = () => { + if (this.teamId === '') { + console.error('You are trying to use a route that requires a team_id, but you have not called setTeamId() in client.jsx'); // eslint-disable-line no-console + } + + return this.teamId; + } + + getServerVersion = () => { + return this.serverVersion; + } + + getBaseRoute() { + return `${this.url}${this.urlVersion}`; + } + + getAdminRoute() { + return `${this.url}${this.urlVersion}/admin`; + } + + getLicenseRoute() { + return `${this.url}${this.urlVersion}/license`; + } + + getTeamsRoute() { + return `${this.url}${this.urlVersion}/teams`; + } + + getTeamNeededRoute() { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}`; + } + + getChannelsRoute() { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels`; + } + + getChannelNeededRoute(channelId) { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/${channelId}`; + } + + getCommandsRoute() { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/commands`; + } + + getHooksRoute() { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/hooks`; + } + + getPostsRoute(channelId) { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/${channelId}/posts`; + } + + getUsersRoute() { + return `${this.url}${this.urlVersion}/users`; + } + + getFilesRoute() { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/files`; + } + + getOAuthRoute() { + return `${this.url}${this.urlVersion}/oauth`; + } + + getUserNeededRoute(userId) { + return `${this.url}${this.urlVersion}/users/${userId}`; + } + + setTranslations = (messages) => { + this.translations = messages; + } + + enableLogErrorsToConsole = (enabled) => { + this.logToConsole = enabled; + } + + useHeaderToken = () => { + this.useToken = true; + if (this.token !== '') { + this.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${this.token}`; + } + } + + track = (category, action, label, property, value) => { // eslint-disable-line no-unused-vars + // NO-OP for inherited classes to override + } + + trackPage = () => { + // NO-OP for inherited classes to override + } + + handleError = (err, res) => { // eslint-disable-line no-unused-vars + // NO-OP for inherited classes to override + } + + handleResponse = (methodName, successCallback, errorCallback, err, res) => { + if (res && res.header) { + this.serverVersion = res.header[HEADER_X_VERSION_ID]; + if (res.header[HEADER_X_VERSION_ID]) { + this.serverVersion = res.header[HEADER_X_VERSION_ID]; + } + } + + if (err) { + // test to make sure it looks like a server JSON error response + var e = null; + if (res && res.body && res.body.id) { + e = res.body; + } + + var msg = ''; + + if (e) { + msg = 'method=' + methodName + ' msg=' + e.message + ' detail=' + e.detailed_error + ' rid=' + e.request_id; + } else { + msg = 'method=' + methodName + ' status=' + err.status + ' statusCode=' + err.statusCode + ' err=' + err; + + if (err.status === 0 || !err.status) { + e = {message: this.translations.connectionError}; + } else { + e = {message: this.translations.unknownError + ' (' + err.status + ')'}; + } + } + + if (this.logToConsole) { + console.error(msg); // eslint-disable-line no-console + console.error(e); // eslint-disable-line no-console + } + + this.track('api', 'api_weberror', methodName, 'message', msg); + + this.handleError(err, res); + + if (errorCallback) { + errorCallback(e, err, res); + return; + } + } + + if (successCallback) { + successCallback(res.body, res); + } + } + + // General / Admin / Licensing Routes Section + + getTranslations = (url, success, error) => { + return request. + get(url). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTranslations', success, error)); + } + + getClientConfig = (success, error) => { + return request. + get(`${this.getAdminRoute()}/client_props`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getClientConfig', success, error)); + } + + getComplianceReports = (success, error) => { + return request. + get(`${this.getAdminRoute()}/compliance_reports`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getComplianceReports', success, error)); + } + + uploadBrandImage = (image, success, error) => { + request. + post(`${this.getAdminRoute()}/upload_brand_image`). + set(this.defaultHeaders). + accept('application/json'). + attach('image', image, image.name). + end(this.handleResponse.bind(this, 'uploadBrandImage', success, error)); + } + + saveComplianceReports = (job, success, error) => { + return request. + post(`${this.getAdminRoute()}/save_compliance_report`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(job). + end(this.handleResponse.bind(this, 'saveComplianceReports', success, error)); + } + + getLogs = (success, error) => { + return request. + get(`${this.getAdminRoute()}/logs`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getLogs', success, error)); + } + + getServerAudits = (success, error) => { + return request. + get(`${this.getAdminRoute()}/audits`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getServerAudits', success, error)); + } + + getConfig = (success, error) => { + return request. + get(`${this.getAdminRoute()}/config`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getConfig', success, error)); + } + + getAnalytics = (name, teamId, success, error) => { + let url = `${this.getAdminRoute()}/analytics/`; + if (teamId == null) { + url += name; + } else { + url += teamId + '/' + name; + } + + return request. + get(url). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getAnalytics', success, error)); + } + + getTeamAnalytics = (teamId, name, success, error) => { + return request. + get(`${this.getAdminRoute()}/analytics/${teamId}/${name}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamAnalytics', success, error)); + } + + saveConfig = (config, success, error) => { + request. + post(`${this.getAdminRoute()}/save_config`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(config). + end(this.handleResponse.bind(this, 'saveConfig', success, error)); + } + + testEmail = (config, success, error) => { + request. + post(`${this.getAdminRoute()}/test_email`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(config). + end(this.handleResponse.bind(this, 'testEmail', success, error)); + } + + logClientError = (msg) => { + var l = {}; + l.level = 'ERROR'; + l.message = msg; + + request. + post(`${this.getAdminRoute()}/log_client`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(l). + end(this.handleResponse.bind(this, 'logClientError', null, null)); + } + + getClientLicenceConfig = (success, error) => { + request. + get(`${this.getLicenseRoute()}/client_config`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getClientLicenceConfig', success, error)); + } + + removeLicenseFile = (success, error) => { + request. + post(`${this.getLicenseRoute()}/remove`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'removeLicenseFile', success, error)); + } + + uploadLicenseFile = (license, success, error) => { + request. + post(`${this.getLicenseRoute()}/add`). + set(this.defaultHeaders). + accept('application/json'). + attach('license', license, license.name). + end(this.handleResponse.bind(this, 'uploadLicenseFile', success, error)); + + this.track('api', 'api_license_upload'); + } + + importSlack = (fileData, success, error) => { + request. + post(`${this.getTeamsRoute()}/import_team`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(fileData). + end(this.handleResponse.bind(this, 'importSlack', success, error)); + } + + exportTeam = (success, error) => { + request. + get(`${this.getTeamsRoute()}/export_team`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'exportTeam', success, error)); + } + + signupTeam = (email, success, error) => { + request. + post(`${this.getTeamsRoute()}/signup`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({email}). + end(this.handleResponse.bind(this, 'signupTeam', success, error)); + + this.track('api', 'api_teams_signup'); + } + + adminResetMfa = (userId, success, error) => { + const data = {}; + data.user_id = userId; + + request. + post(`${this.getAdminRoute()}/reset_mfa`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'adminResetMfa', success, error)); + } + + adminResetPassword = (userId, newPassword, success, error) => { + var data = {}; + data.new_password = newPassword; + data.user_id = userId; + + request. + post(`${this.getAdminRoute()}/reset_password`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'adminResetPassword', success, error)); + + this.track('api', 'api_admin_reset_password'); + } + + // Team Routes Section + + createTeamFromSignup = (teamSignup, success, error) => { + request. + post(`${this.getTeamsRoute()}/create_from_signup`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(teamSignup). + end(this.handleResponse.bind(this, 'createTeamFromSignup', success, error)); + } + + findTeamByName = (teamName, success, error) => { + request. + post(`${this.getTeamsRoute()}/find_team_by_name`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({name: teamName}). + end(this.handleResponse.bind(this, 'findTeamByName', success, error)); + } + + createTeam = (team, success, error) => { + request. + post(`${this.getTeamsRoute()}/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(team). + end(this.handleResponse.bind(this, 'createTeam', success, error)); + + this.track('api', 'api_users_create', '', 'email', team.name); + } + + updateTeam = (team, success, error) => { + request. + post(`${this.getTeamNeededRoute()}/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(team). + end(this.handleResponse.bind(this, 'updateTeam', success, error)); + + this.track('api', 'api_teams_update_name'); + } + + getAllTeams = (success, error) => { + request. + get(`${this.getTeamsRoute()}/all`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getAllTeams', success, error)); + } + + getAllTeamListings = (success, error) => { + request. + get(`${this.getTeamsRoute()}/all_team_listings`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getAllTeamListings', success, error)); + } + + getMyTeam = (success, error) => { + request. + get(`${this.getTeamNeededRoute()}/me`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getMyTeam', success, error)); + } + + getTeamMembers = (teamId, success, error) => { + request. + get(`${this.getTeamsRoute()}/members/${teamId}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamMembers', success, error)); + } + + inviteMembers = (data, success, error) => { + request. + post(`${this.getTeamNeededRoute()}/invite_members`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'inviteMembers', success, error)); + + this.track('api', 'api_teams_invite_members'); + } + + addUserToTeam = (userId, success, error) => { + request. + post(`${this.getTeamNeededRoute()}/add_user_to_team`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({user_id: userId}). + end(this.handleResponse.bind(this, 'addUserToTeam', success, error)); + + this.track('api', 'api_teams_invite_members'); + } + + addUserToTeamFromInvite = (data, hash, inviteId, success, error) => { + request. + post(`${this.getTeamsRoute()}/add_user_to_team_from_invite`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({hash, data, invite_id: inviteId}). + end(this.handleResponse.bind(this, 'addUserToTeam', success, error)); + + this.track('api', 'api_teams_invite_members'); + } + + getInviteInfo = (inviteId, success, error) => { + request. + post(`${this.getTeamsRoute()}/get_invite_info`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({invite_id: inviteId}). + end(this.handleResponse.bind(this, 'getInviteInfo', success, error)); + } + + // User Routes Setions + + createUser = (user, success, error) => { + this.createUserWithInvite(user, null, null, null, success, error); + } + + createUserWithInvite = (user, data, emailHash, inviteId, success, error) => { + var url = `${this.getUsersRoute()}/create`; + + if (data || emailHash || inviteId) { + url += '?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash) + '&iid=' + encodeURIComponent(inviteId); + } + + request. + post(url). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(user). + end(this.handleResponse.bind(this, 'createUser', success, error)); + + this.track('api', 'api_users_create', '', 'email', user.email); + } + + updateUser = (user, success, error) => { + request. + post(`${this.getUsersRoute()}/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(user). + end(this.handleResponse.bind(this, 'updateUser', success, error)); + + this.track('api', 'api_users_update'); + } + + updatePassword = (userId, currentPassword, newPassword, success, error) => { + var data = {}; + data.user_id = userId; + data.current_password = currentPassword; + data.new_password = newPassword; + + request. + post(`${this.getUsersRoute()}/newpassword`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updatePassword', success, error)); + + this.track('api', 'api_users_newpassword'); + } + + updateUserNotifyProps = (notifyProps, success, error) => { + request. + post(`${this.getUsersRoute()}/update_notify`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(notifyProps). + end(this.handleResponse.bind(this, 'updateUserNotifyProps', success, error)); + } + + updateRoles = (userId, newRoles, success, error) => { + var data = { + user_id: userId, + new_roles: newRoles + }; + + request. + post(`${this.getUsersRoute()}/update_roles`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateRoles', success, error)); + + this.track('api', 'api_users_update_roles'); + } + + updateActive = (userId, active, success, error) => { + var data = {}; + data.user_id = userId; + data.active = '' + active; + + request. + post(`${this.getUsersRoute()}/update_active`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateActive', success, error)); + + this.track('api', 'api_users_update_roles'); + } + + sendPasswordReset = (email, success, error) => { + var data = {}; + data.email = email; + + request. + post(`${this.getUsersRoute()}/send_password_reset`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'sendPasswordReset', success, error)); + + this.track('api', 'api_users_send_password_reset'); + } + + resetPassword = (code, newPassword, success, error) => { + var data = {}; + data.new_password = newPassword; + data.code = code; + + request. + post(`${this.getUsersRoute()}/reset_password`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'resetPassword', success, error)); + + this.track('api', 'api_users_reset_password'); + } + + emailToOAuth = (email, password, service, success, error) => { + var data = {}; + data.password = password; + data.email = email; + data.service = service; + + request. + post(`${this.getUsersRoute()}/claim/email_to_oauth`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'emailToOAuth', success, error)); + + this.track('api', 'api_users_email_to_oauth'); + } + + oauthToEmail = (email, password, success, error) => { + var data = {}; + data.password = password; + data.email = email; + + request. + post(`${this.getUsersRoute()}/claim/oauth_to_email`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'oauthToEmail', success, error)); + + this.track('api', 'api_users_oauth_to_email'); + } + + emailToLdap = (email, password, ldapId, ldapPassword, success, error) => { + var data = {}; + data.email_password = password; + data.email = email; + data.ldap_id = ldapId; + data.ldap_password = ldapPassword; + + request. + post(`${this.getUsersRoute()}/claim/email_to_ldap`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'emailToLdap', success, error)); + + this.track('api', 'api_users_email_to_ldap'); + } + + ldapToEmail = (email, emailPassword, ldapPassword, success, error) => { + var data = {}; + data.email = email; + data.ldap_password = ldapPassword; + data.email_password = emailPassword; + + request. + post(`${this.getUsersRoute()}/claim/ldap_to_email`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'ldapToEmail', success, error)); + + this.track('api', 'api_users_oauth_to_email'); + } + + getInitialLoad = (success, error) => { + request. + get(`${this.getUsersRoute()}/initial_load`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getInitialLoad', success, error)); + } + + getMe = (success, error) => { + request. + get(`${this.getUsersRoute()}/me`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getMe', success, error)); + } + + login = (email, username, password, mfaToken, success, error) => { + var outer = this; // eslint-disable-line consistent-this + + request. + post(`${this.getUsersRoute()}/login`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({email, password, username, token: mfaToken}). + end(this.handleResponse.bind( + this, + 'login', + (data, res) => { + if (res && res.header) { + outer.token = res.header[HEADER_TOKEN]; + + if (outer.useToken) { + outer.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${outer.token}`; + } + } + + if (success) { + success(data, res); + } + }, + error + )); + + this.track('api', 'api_users_login', '', 'email', email); + } + + loginByLdap = (ldapId, password, mfaToken, success, error) => { + var outer = this; // eslint-disable-line consistent-this + + request. + post(`${this.getUsersRoute()}/login_ldap`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: ldapId, password, token: mfaToken}). + end(this.handleResponse.bind( + this, + 'loginByLdap', + (data, res) => { + if (res && res.header) { + outer.token = res.header[HEADER_TOKEN]; + + if (outer.useToken) { + outer.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${outer.token}`; + } + } + + if (success) { + success(data, res); + } + }, + error + )); + + this.track('api', 'api_users_loginLdap', '', 'email', ldapId); + } + + logout = (success, error) => { + request. + post(`${this.getUsersRoute()}/logout`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'logout', success, error)); + + this.track('api', 'api_users_logout'); + } + + checkMfa = (method, loginId, success, error) => { + var data = {}; + data.method = method; + data.login_id = loginId; + + request. + post(`${this.getUsersRoute()}/mfa`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'checkMfa', success, error)); + + this.track('api', 'api_users_oauth_to_email'); + } + + revokeSession = (altId, success, error) => { + request. + post(`${this.getUsersRoute()}/revoke_session`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: altId}). + end(this.handleResponse.bind(this, 'revokeSession', success, error)); + } + + getSessions = (userId, success, error) => { + request. + get(`${this.getUserNeededRoute(userId)}/sessions`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getSessions', success, error)); + } + + getAudits = (userId, success, error) => { + request. + get(`${this.getUserNeededRoute(userId)}/audits`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getAudits', success, error)); + } + + getDirectProfiles = (success, error) => { + request. + get(`${this.getUsersRoute()}/direct_profiles`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getDirectProfiles', success, error)); + } + + getProfiles = (success, error) => { + request. + get(`${this.getUsersRoute()}/profiles/${this.getTeamId()}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfiles', success, error)); + } + + getProfilesForTeam = (teamId, success, error) => { + request. + get(`${this.getUsersRoute()}/profiles/${teamId}?skip_direct=true`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfilesForTeam', success, error)); + } + + getStatuses = (ids, success, error) => { + request. + post(`${this.getUsersRoute()}/status`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(ids). + end(this.handleResponse.bind(this, 'getStatuses', success, error)); + } + + verifyEmail = (uid, hid, success, error) => { + request. + post(`${this.getUsersRoute()}/verify_email`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({uid, hid}). + end(this.handleResponse.bind(this, 'verifyEmail', success, error)); + } + + resendVerification = (email, success, error) => { + request. + post(`${this.getUsersRoute()}/resend_verification`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({email}). + end(this.handleResponse.bind(this, 'resendVerification', success, error)); + } + + updateMfa = (token, activate, success, error) => { + const data = {}; + data.activate = activate; + data.token = token; + + request. + post(`${this.getUsersRoute()}/update_mfa`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateMfa', success, error)); + } + + uploadProfileImage = (image, success, error) => { + request. + post(`${this.getUsersRoute()}/newimage`). + set(this.defaultHeaders). + attach('image', image, image.name). + accept('application/json'). + end(this.handleResponse.bind(this, 'uploadProfileImage', success, error)); + } + + // Channel Routes Section + + createChannel = (channel, success, error) => { + request. + post(`${this.getChannelsRoute()}/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(channel). + end(this.handleResponse.bind(this, 'createChannel', success, error)); + + this.track('api', 'api_channels_create', channel.type, 'name', channel.name); + } + + createDirectChannel = (userId, success, error) => { + request. + post(`${this.getChannelsRoute()}/create_direct`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({user_id: userId}). + end(this.handleResponse.bind(this, 'createDirectChannel', success, error)); + } + + updateChannel = (channel, success, error) => { + request. + post(`${this.getChannelsRoute()}/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(channel). + end(this.handleResponse.bind(this, 'updateChannel', success, error)); + + this.track('api', 'api_channels_update'); + } + + updateChannelHeader = (channelId, header, success, error) => { + const data = { + channel_id: channelId, + channel_header: header + }; + + request. + post(`${this.getChannelsRoute()}/update_header`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateChannel', success, error)); + + this.track('api', 'api_channels_header'); + } + + updateChannelPurpose = (channelId, purpose, success, error) => { + const data = { + channel_id: channelId, + channel_purpose: purpose + }; + + request. + post(`${this.getChannelsRoute()}/update_purpose`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateChannelPurpose', success, error)); + + this.track('api', 'api_channels_purpose'); + } + + updateChannelNotifyProps = (data, success, error) => { + request. + post(`${this.getChannelsRoute()}/update_notify_props`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'updateChannelNotifyProps', success, error)); + } + + leaveChannel = (channelId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/leave`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'leaveChannel', success, error)); + + this.track('api', 'api_channels_leave'); + } + + joinChannel = (channelId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/join`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'joinChannel', success, error)); + + this.track('api', 'api_channels_join'); + } + + deleteChannel = (channelId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'deleteChannel', success, error)); + + this.track('api', 'api_channels_delete'); + } + + updateLastViewedAt = (channelId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/update_last_viewed_at`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'updateLastViewedAt', success, error)); + } + + getChannels = (success, error) => { + request. + get(`${this.getChannelsRoute()}/`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getChannels', success, error)); + } + + getChannel = (channelId, success, error) => { + request. + get(`${this.getChannelNeededRoute(channelId)}/`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getChannel', success, error)); + + this.track('api', 'api_channel_get'); + } + + getMoreChannels = (success, error) => { + request. + get(`${this.getChannelsRoute()}/more`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getMoreChannels', success, error)); + } + + getChannelCounts = (success, error) => { + request. + get(`${this.getChannelsRoute()}/counts`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getChannelCounts', success, error)); + } + + getChannelExtraInfo = (channelId, memberLimit, success, error) => { + var url = `${this.getChannelNeededRoute(channelId)}/extra_info`; + if (memberLimit) { + url += '/' + memberLimit; + } + + request. + get(url). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getChannelExtraInfo', success, error)); + } + + addChannelMember = (channelId, userId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/add`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({user_id: userId}). + end(this.handleResponse.bind(this, 'addChannelMember', success, error)); + + this.track('api', 'api_channels_add_member'); + } + + removeChannelMember = (channelId, userId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/remove`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({user_id: userId}). + end(this.handleResponse.bind(this, 'removeChannelMember', success, error)); + + this.track('api', 'api_channels_remove_member'); + } + + // Routes for Commands + + listCommands = (success, error) => { + request. + get(`${this.getCommandsRoute()}/list`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'listCommands', success, error)); + } + + executeCommand = (channelId, command, suggest, success, error) => { + request. + post(`${this.getCommandsRoute()}/execute`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({channelId, command, suggest: '' + suggest}). + end(this.handleResponse.bind(this, 'executeCommand', success, error)); + } + + addCommand = (command, success, error) => { + request. + post(`${this.getCommandsRoute()}/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(command). + end(this.handleResponse.bind(this, 'addCommand', success, error)); + } + + deleteCommand = (commandId, success, error) => { + request. + post(`${this.getCommandsRoute()}/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: commandId}). + end(this.handleResponse.bind(this, 'deleteCommand', success, error)); + } + + listTeamCommands = (success, error) => { + request. + get(`${this.getCommandsRoute()}/list_team_commands`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'listTeamCommands', success, error)); + } + + regenCommandToken = (commandId, suggest, success, error) => { + request. + post(`${this.getCommandsRoute()}/regen_token`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: commandId}). + end(this.handleResponse.bind(this, 'regenCommandToken', success, error)); + } + + // Routes for Posts + + createPost = (post, success, error) => { + request. + post(`${this.getPostsRoute(post.channel_id)}/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(post). + end(this.handleResponse.bind(this, 'createPost', success, error)); + + this.track('api', 'api_posts_create', post.channel_id, 'length', post.message.length); + } + + getPostById = (postId, success, error) => { + request. + get(`${this.getTeamNeededRoute()}/posts/${postId}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPostById', success, error)); + } + + getPost = (channelId, postId, success, error) => { + request. + get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPost', success, error)); + } + + updatePost = (post, success, error) => { + request. + post(`${this.getPostsRoute(post.channel_id)}/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(post). + end(this.handleResponse.bind(this, 'updatePost', success, error)); + + this.track('api', 'api_posts_update'); + } + + deletePost = (channelId, postId, success, error) => { + request. + post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'deletePost', success, error)); + + this.track('api', 'api_posts_delete'); + } + + search = (terms, success, error) => { + request. + get(`${this.getTeamNeededRoute()}/posts/search`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + query({terms}). + end(this.handleResponse.bind(this, 'search', success, error)); + + this.track('api', 'api_posts_search'); + } + + getPostsPage = (channelId, offset, limit, success, error) => { + request. + get(`${this.getPostsRoute(channelId)}/page/${offset}/${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPostsPage', success, error)); + } + + getPosts = (channelId, since, success, error) => { + request. + get(`${this.getPostsRoute(channelId)}/since/${since}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPosts', success, error)); + } + + getPostsBefore = (channelId, postId, offset, numPost, success, error) => { + request. + get(`${this.getPostsRoute(channelId)}/${postId}/before/${offset}/${numPost}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPostsBefore', success, error)); + } + + getPostsAfter = (channelId, postId, offset, numPost, success, error) => { + request. + get(`${this.getPostsRoute(channelId)}/${postId}/after/${offset}/${numPost}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPostsAfter', success, error)); + } + + // Routes for Files + + getFileInfo = (filename, success, error) => { + request. + get(`${this.getFilesRoute()}/get_info${filename}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getFileInfo', success, error)); + } + + getPublicLink = (data, success, error) => { + request. + post(`${this.getFilesRoute()}/get_public_link`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(data). + end(this.handleResponse.bind(this, 'getPublicLink', success, error)); + } + + uploadFile = (file, filename, channelId, clientId, success, error) => { + return request. + post(`${this.getFilesRoute()}/upload`). + set(this.defaultHeaders). + attach('files', file, filename). + field('channel_id', channelId). + field('client_ids', clientId). + accept('application/json'). + end(this.handleResponse.bind(this, 'uploadFile', success, error)); + } + + // Routes for OAuth + + registerOAuthApp = (app, success, error) => { + request. + post(`${this.getOAuthRoute()}/register`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(app). + end(this.handleResponse.bind(this, 'registerOAuthApp', success, error)); + + this.track('api', 'api_apps_register'); + } + + allowOAuth2 = (responseType, clientId, redirectUri, state, scope, success, error) => { + request. + get(`${this.getOAuthRoute()}/allow`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + query({response_type: responseType}). + query({client_id: clientId}). + query({redirect_uri: redirectUri}). + query({scope}). + query({state}). + end(this.handleResponse.bind(this, 'allowOAuth2', success, error)); + } + + // Routes for Hooks + + addIncomingHook = (hook, success, error) => { + request. + post(`${this.getHooksRoute()}/incoming/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(hook). + end(this.handleResponse.bind(this, 'addIncomingHook', success, error)); + } + + deleteIncomingHook = (hookId, success, error) => { + request. + post(`${this.getHooksRoute()}/incoming/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: hookId}). + end(this.handleResponse.bind(this, 'deleteIncomingHook', success, error)); + } + + listIncomingHooks = (success, error) => { + request. + get(`${this.getHooksRoute()}/incoming/list`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'listIncomingHooks', success, error)); + } + + addOutgoingHook = (hook, success, error) => { + request. + post(`${this.getHooksRoute()}/outgoing/create`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(hook). + end(this.handleResponse.bind(this, 'addOutgoingHook', success, error)); + } + + deleteOutgoingHook = (hookId, success, error) => { + request. + post(`${this.getHooksRoute()}/outgoing/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: hookId}). + end(this.handleResponse.bind(this, 'deleteOutgoingHook', success, error)); + } + + listOutgoingHooks = (success, error) => { + request. + get(`${this.getHooksRoute()}/outgoing/list`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'listOutgoingHooks', success, error)); + } + + regenOutgoingHookToken = (hookId, success, error) => { + request. + post(`${this.getHooksRoute()}/outgoing/regen_token`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id: hookId}). + end(this.handleResponse.bind(this, 'regenOutgoingHookToken', success, error)); + } + + //Routes for Prefrecnes + + getAllPreferences = (success, error) => { + request. + get(`${this.getBaseRoute()}/preferences/`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getAllPreferences', success, error)); + } + + savePreferences = (preferences, success, error) => { + request. + post(`${this.getBaseRoute()}/preferences/save`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(preferences). + end(this.handleResponse.bind(this, 'savePreferences', success, error)); + } + + getPreferenceCategory = (category, success, error) => { + request. + get(`${this.getBaseRoute()}/preferences/${category}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPreferenceCategory', success, error)); + } +} diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx index f1dd4a26a..d3e5ce66d 100644 --- a/webapp/components/activity_log_modal.jsx +++ b/webapp/components/activity_log_modal.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import UserStore from 'stores/user_store.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import {Modal} from 'react-bootstrap'; import LoadingScreen from './loading_screen.jsx'; diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx index 729d4b14d..dd56411f4 100644 --- a/webapp/components/admin_console/admin_navbar_dropdown.jsx +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -3,27 +3,20 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; -import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; import {Link} from 'react-router'; -function getStateFromStores() { - return {currentTeam: TeamStore.getCurrent()}; -} - import React from 'react'; export default class AdminNavbarDropdown extends React.Component { constructor(props) { super(props); this.blockToggle = false; - - this.state = getStateFromStores(); } componentDidMount() { @@ -64,24 +57,27 @@ export default class AdminNavbarDropdown extends React.Component { >
  • - + - +
  • diff --git a/webapp/components/admin_console/admin_sidebar_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx index 2e6252075..400730030 100644 --- a/webapp/components/admin_console/admin_sidebar_header.jsx +++ b/webapp/components/admin_console/admin_sidebar_header.jsx @@ -4,6 +4,7 @@ import $ from 'jquery'; import AdminNavbarDropdown from './admin_navbar_dropdown.jsx'; import UserStore from 'stores/user_store.jsx'; +import Client from 'utils/web_client.jsx'; import {FormattedMessage} from 'react-intl'; @@ -41,7 +42,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( + +
    + +
    + +

    + +

    +
    +
    +
    -
    - -
    - - -

    - -

    -
    -
    - {brand}
    diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx index 8b37bd237..2b0e6af0a 100644 --- a/webapp/components/admin_console/team_users.jsx +++ b/webapp/components/admin_console/team_users.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import LoadingScreen from '../loading_screen.jsx'; import UserItem from './user_item.jsx'; import ResetPasswordModal from './reset_password_modal.jsx'; diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx index 5bd05d063..b8f21d77e 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/user_item.jsx @@ -1,13 +1,13 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import UserStore from 'stores/user_store.jsx'; import ConfirmModal from '../confirm_modal.jsx'; import TeamStore from 'stores/team_store.jsx'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; import React from 'react'; import {browserHistory} from 'react-router'; @@ -22,6 +22,7 @@ export default class UserItem extends React.Component { this.handleMakeAdmin = this.handleMakeAdmin.bind(this); this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this); this.handleResetPassword = this.handleResetPassword.bind(this); + this.handleResetMfa = this.handleResetMfa.bind(this); this.handleDemote = this.handleDemote.bind(this); this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); this.handleDemoteCancel = this.handleDemoteCancel.bind(this); @@ -40,12 +41,9 @@ export default class UserItem extends React.Component { if (this.props.user.id === me.id) { this.handleDemote(this.props.user, ''); } else { - const data = { - user_id: this.props.user.id, - new_roles: '' - }; - - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + '', () => { this.props.refreshProfiles(); }, @@ -86,12 +84,9 @@ export default class UserItem extends React.Component { if (this.props.user.id === me.id) { this.handleDemote(this.props.user, 'admin'); } else { - const data = { - user_id: this.props.user.id, - new_roles: 'admin' - }; - - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + 'admin', () => { this.props.refreshProfiles(); }, @@ -104,12 +99,10 @@ export default class UserItem extends React.Component { handleMakeSystemAdmin(e) { e.preventDefault(); - const data = { - user_id: this.props.user.id, - new_roles: 'system_admin' - }; - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + 'system_admin', () => { this.props.refreshProfiles(); }, @@ -124,6 +117,19 @@ export default class UserItem extends React.Component { this.props.doPasswordReset(this.props.user); } + handleResetMfa(e) { + e.preventDefault(); + + Client.adminResetMfa(this.props.user.id, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + handleDemote(user, role) { this.setState({ serverError: this.state.serverError, @@ -143,12 +149,9 @@ export default class UserItem extends React.Component { } handleDemoteSubmit() { - const data = { - user_id: this.props.user.id, - new_roles: this.state.role - }; - - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + this.state.role, () => { this.setState({ serverError: null, @@ -211,10 +214,15 @@ export default class UserItem extends React.Component { const email = user.email; let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin'; - let showMakeAdmin = user.roles === '' || user.roles === 'system_admin'; + + //let showMakeAdmin = user.roles === '' || user.roles === 'system_admin'; + let showMakeAdmin = false; + let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin'; let showMakeActive = false; let showMakeNotActive = user.roles !== 'system_admin'; + let mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true'; + let showMfaReset = mfaEnabled && user.mfa_active; if (user.delete_at > 0) { currentRoles = ( @@ -319,6 +327,64 @@ export default class UserItem extends React.Component { ); } + + let mfaReset = null; + if (showMfaReset) { + mfaReset = ( +
  • + + + +
  • + ); + } + + let mfaActiveText; + if (mfaEnabled) { + if (user.mfa_active) { + mfaActiveText = ( + + ); + } else { + mfaActiveText = ( + + ); + } + } + + let authServiceText; + if (user.auth_service) { + authServiceText = ( + + ); + } else { + authServiceText = ( + + ); + } + const me = UserStore.getCurrentUser(); let makeDemoteModal = null; if (this.props.user.id === me.id) { @@ -368,13 +434,23 @@ export default class UserItem extends React.Component {
    {Utils.getDisplayName(user)}
    -
    {email}
    +
    + + {authServiceText} + {mfaActiveText} +
    @@ -397,6 +473,7 @@ export default class UserItem extends React.Component { {makeActive} {makeNotActive} {makeSystemAdmin} + {mfaReset}
  • ) }); + + return; } if (!command.url) { @@ -101,12 +103,14 @@ export default class AddCommand extends React.Component { /> ) }); + + return; } AsyncClient.addCommand( command, () => { - browserHistory.push('/settings/integrations/commands'); + browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'); }, (err) => { this.setState({ @@ -251,7 +255,7 @@ export default class AddCommand extends React.Component { return (
    - + { - browserHistory.push('/settings/integrations/incoming_webhooks'); + browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'); }, (err) => { this.setState({ @@ -102,7 +103,7 @@ export default class AddIncomingWebhook extends React.Component { return (
    - + { - browserHistory.push('/settings/integrations/outgoing_webhooks'); + browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'); }, (err) => { this.setState({ @@ -154,7 +155,7 @@ export default class AddOutgoingWebhook extends React.Component { return (
    - + diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx index eb84709a3..6f8e0b86a 100644 --- a/webapp/components/backstage/backstage_sidebar.jsx +++ b/webapp/components/backstage/backstage_sidebar.jsx @@ -3,6 +3,7 @@ import React from 'react'; +import * as Utils from 'utils/utils.jsx'; import BackstageCategory from './backstage_category.jsx'; import BackstageSection from './backstage_section.jsx'; import {FormattedMessage} from 'react-intl'; @@ -14,7 +15,7 @@ export default class BackstageSidebar extends React.Component {
      } - addLink='/settings/integrations/commands/add' + addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands/add'} > {commands} diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx index 58d318310..afa6e9958 100644 --- a/webapp/components/backstage/installed_incoming_webhook.jsx +++ b/webapp/components/backstage/installed_incoming_webhook.jsx @@ -90,6 +90,17 @@ export default class InstalledIncomingWebhook extends React.Component {
    {description} +
    + + + +
    } - addLink='/settings/integrations/incoming_webhooks/add' + addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks/add'} > {incomingWebhooks} diff --git a/webapp/components/backstage/installed_outgoing_webhooks.jsx b/webapp/components/backstage/installed_outgoing_webhooks.jsx index 15d927a41..98992b081 100644 --- a/webapp/components/backstage/installed_outgoing_webhooks.jsx +++ b/webapp/components/backstage/installed_outgoing_webhooks.jsx @@ -5,6 +5,7 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; +import * as Utils from 'utils/utils.jsx'; import {FormattedMessage} from 'react-intl'; import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; @@ -82,7 +83,7 @@ export default class InstalledOutgoingWebhooks extends React.Component { defaultMessage='Add Outgoing Webhook' /> } - addLink='/settings/integrations/outgoing_webhooks/add' + addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks/add'} > {outgoingWebhooks} diff --git a/webapp/components/backstage/integrations.jsx b/webapp/components/backstage/integrations.jsx index 71232ea45..fdd75026a 100644 --- a/webapp/components/backstage/integrations.jsx +++ b/webapp/components/backstage/integrations.jsx @@ -5,6 +5,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import IntegrationOption from './integration_option.jsx'; +import * as Utils from 'utils/utils.jsx'; import WebhookIcon from 'images/webhook_icon.jpg'; @@ -29,7 +30,7 @@ export default class Integrations extends React.Component { defaultMessage='Incoming webhooks allow external integrations to send messages' /> } - link={'/settings/integrations/incoming_webhooks'} + link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} /> ); } @@ -51,7 +52,7 @@ export default class Integrations extends React.Component { defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages' /> } - link={'/settings/integrations/outgoing_webhooks'} + link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} /> ); } @@ -73,7 +74,7 @@ export default class Integrations extends React.Component { defaultMessage='Slash commands send events to an external integration' /> } - link={'/settings/integrations/commands'} + link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} /> ); } diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 16d9ea536..c82f59299 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -25,7 +25,7 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Utils from 'utils/utils.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import Constants from 'utils/constants.jsx'; import {FormattedMessage} from 'react-intl'; diff --git a/webapp/components/channel_info_modal.jsx b/webapp/components/channel_info_modal.jsx index c7f9f9f79..4c16dda90 100644 --- a/webapp/components/channel_info_modal.jsx +++ b/webapp/components/channel_info_modal.jsx @@ -3,33 +3,26 @@ import * as Utils from 'utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - +import {FormattedMessage} from 'react-intl'; import {Modal} from 'react-bootstrap'; -const holders = defineMessages({ - notFound: { - id: 'channel_info.notFound', - defaultMessage: 'No Channel Found' - } -}); - import React from 'react'; -class ChannelInfoModal extends React.Component { +export default class ChannelInfoModal extends React.Component { render() { - const {formatMessage} = this.props.intl; let channel = this.props.channel; if (!channel) { + const notFound = Utils.localizeMessage('channel_info.notFound', 'No Channel Found'); + channel = { - display_name: formatMessage(holders.notFound), - name: formatMessage(holders.notFound), - purpose: formatMessage(holders.notFound), - id: formatMessage(holders.notFound) + display_name: notFound, + name: notFound, + purpose: notFound, + id: notFound }; } - const channelURL = Utils.getShortenedTeamURL() + channel.name; + const channelURL = Utils.getTeamURLFromAddressBar() + '/channels/' + channel.name; return ( { this.setState({ addingUser: false diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx index 67be2ef50..ecea891bb 100644 --- a/webapp/components/channel_members_modal.jsx +++ b/webapp/components/channel_members_modal.jsx @@ -9,7 +9,7 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import {FormattedMessage} from 'react-intl'; @@ -97,12 +97,9 @@ export default class ChannelMembersModal extends React.Component { handleRemove(user) { const userId = user.id; - const data = {}; - data.user_id = userId; - Client.removeChannelMember( ChannelStore.getCurrentId(), - data, + userId, () => { const memberList = this.state.memberList.slice(); for (let i = 0; i < memberList.length; i++) { diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx index 564776876..112c07ad0 100644 --- a/webapp/components/channel_notifications_modal.jsx +++ b/webapp/components/channel_notifications_modal.jsx @@ -6,7 +6,7 @@ import {Modal} from 'react-bootstrap'; import SettingItemMin from './setting_item_min.jsx'; import SettingItemMax from './setting_item_max.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import {FormattedMessage} from 'react-intl'; @@ -60,7 +60,7 @@ export default class ChannelNotificationsModal extends React.Component { data.desktop = notifyLevel; //TODO: This should be moved to event_helpers - Client.updateNotifyProps(data, + Client.updateChannelNotifyProps(data, () => { // YUCK var member = ChannelStore.getMember(channelId); @@ -252,7 +252,7 @@ export default class ChannelNotificationsModal extends React.Component { }; //TODO: This should be fixed, moved to event_helpers - Client.updateNotifyProps(data, + Client.updateChannelNotifyProps(data, () => { // Yuck... var member = ChannelStore.getMember(channelId); diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx index 8622d1f57..238cfa1ae 100644 --- a/webapp/components/channel_select.jsx +++ b/webapp/components/channel_select.jsx @@ -54,7 +54,7 @@ export default class ChannelSelect extends React.Component { ]; this.state.channels.forEach((channel) => { - if (channel.type !== Constants.DM_CHANNEL) { + if (channel.type === Constants.OPEN_CHANNEL) { options.push(
    {React.cloneElement(this.props.children, { - teamName: this.state.teamName, - teamDisplayName: this.state.teamDisplayName, currentType: this.state.oldType, newType: this.state.newType, email: this.state.email @@ -83,7 +54,6 @@ export default class Claim extends React.Component { Claim.defaultProps = { }; Claim.propTypes = { - params: React.PropTypes.object.isRequired, location: React.PropTypes.object.isRequired, children: React.PropTypes.node }; diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx index 1ceb42a27..fbf26cade 100644 --- a/webapp/components/claim/components/email_to_ldap.jsx +++ b/webapp/components/claim/components/email_to_ldap.jsx @@ -2,12 +2,11 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; export default class EmailToLDAP extends React.Component { constructor(props) { @@ -45,17 +44,14 @@ export default class EmailToLDAP extends React.Component { state.error = null; this.setState(state); - var postData = {}; - postData.email_password = password; - postData.ldap_id = ldapId; - postData.ldap_password = ldapPassword; - postData.email = this.props.email; - postData.team_name = this.props.teamName; - - Client.emailToLDAP(postData, + Client.emailToLdap( + this.props.email, + password, + ldapId, + ldapPassword, (data) => { if (data.follow_link) { - browserHistory.push(data.follow_link); + window.location.href = data.follow_link; } }, (error) => { @@ -74,6 +70,20 @@ export default class EmailToLDAP extends React.Component { formClass += ' has-error'; } + let loginPlaceholder; + if (global.window.mm_config.LdapLoginFieldName) { + loginPlaceholder = global.window.mm_config.LdapLoginFieldName; + } else { + loginPlaceholder = Utils.localizeMessage('claim.email_to_ldap.ldapId', 'LDAP ID'); + } + + let passwordPlaceholder; + if (global.window.mm_config.LdapPasswordFieldName) { + passwordPlaceholder = global.window.mm_config.LdapPasswordFieldName; + } else { + passwordPlaceholder = Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'LDAP Password'); + } + return (

    @@ -98,9 +108,8 @@ export default class EmailToLDAP extends React.Component {

    @@ -125,10 +134,6 @@ export default class EmailToLDAP extends React.Component {

    @@ -138,7 +143,7 @@ export default class EmailToLDAP extends React.Component { name='ldapId' ref='ldapid' autoComplete='off' - placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapId', 'LDAP ID')} + placeholder={loginPlaceholder} spellCheck='false' />
    @@ -149,7 +154,7 @@ export default class EmailToLDAP extends React.Component { name='ldapPassword' ref='ldappassword' autoComplete='off' - placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'LDAP Password')} + placeholder={passwordPlaceholder} spellCheck='false' />

    @@ -172,7 +177,5 @@ export default class EmailToLDAP extends React.Component { EmailToLDAP.defaultProps = { }; EmailToLDAP.propTypes = { - email: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + email: React.PropTypes.string }; diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx index f4376067a..1fd284bed 100644 --- a/webapp/components/claim/components/email_to_oauth.jsx +++ b/webapp/components/claim/components/email_to_oauth.jsx @@ -2,12 +2,11 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; export default class EmailToOAuth extends React.Component { constructor(props) { @@ -31,16 +30,13 @@ export default class EmailToOAuth extends React.Component { state.error = null; this.setState(state); - var postData = {}; - postData.password = password; - postData.email = this.props.email; - postData.team_name = this.props.teamName; - postData.service = this.props.newType; - - Client.emailToOAuth(postData, + Client.emailToOAuth( + this.props.email, + password, + this.props.newType, (data) => { if (data.follow_link) { - browserHistory.push(data.follow_link); + window.location.href = data.follow_link; } }, (error) => { @@ -94,9 +90,8 @@ export default class EmailToOAuth extends React.Component {

    @@ -134,7 +129,5 @@ EmailToOAuth.defaultProps = { }; EmailToOAuth.propTypes = { newType: React.PropTypes.string, - email: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + email: React.PropTypes.string }; diff --git a/webapp/components/claim/components/ldap_to_email.jsx b/webapp/components/claim/components/ldap_to_email.jsx index ed8a314bd..a10cefd6f 100644 --- a/webapp/components/claim/components/ldap_to_email.jsx +++ b/webapp/components/claim/components/ldap_to_email.jsx @@ -2,12 +2,11 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; export default class LDAPToEmail extends React.Component { constructor(props) { @@ -45,16 +44,13 @@ export default class LDAPToEmail extends React.Component { state.error = null; this.setState(state); - var postData = {}; - postData.email_password = password; - postData.ldap_password = ldapPassword; - postData.email = this.props.email; - postData.team_name = this.props.teamName; - - Client.ldapToEmail(postData, + Client.ldapToEmail( + this.props.email, + password, + ldapPassword, (data) => { if (data.follow_link) { - browserHistory.push(data.follow_link); + window.location.href = data.follow_link; } }, (error) => { @@ -73,6 +69,13 @@ export default class LDAPToEmail extends React.Component { formClass += ' has-error'; } + let passwordPlaceholder; + if (global.window.mm_config.LdapPasswordFieldName) { + passwordPlaceholder = global.window.mm_config.LdapPasswordFieldName; + } else { + passwordPlaceholder = Utils.localizeMessage('claim.ldap_to_email.ldapPwd', 'LDAP Password'); + } + return (

    @@ -100,9 +103,9 @@ export default class LDAPToEmail extends React.Component {

    @@ -113,7 +116,7 @@ export default class LDAPToEmail extends React.Component { className='form-control' name='ldapPassword' ref='ldappassword' - placeholder={Utils.localizeMessage('claim.ldap_to_email.ldapPwd', 'LDAP Password')} + placeholder={passwordPlaceholder} spellCheck='false' />

    @@ -162,7 +165,5 @@ export default class LDAPToEmail extends React.Component { LDAPToEmail.defaultProps = { }; LDAPToEmail.propTypes = { - email: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + email: React.PropTypes.string }; diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx index 72e0500a9..7fd18aaa6 100644 --- a/webapp/components/claim/components/oauth_to_email.jsx +++ b/webapp/components/claim/components/oauth_to_email.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -38,12 +38,9 @@ export default class OAuthToEmail extends React.Component { state.error = null; this.setState(state); - var postData = {}; - postData.password = password; - postData.email = this.props.email; - postData.team_name = this.props.teamName; - - Client.oauthToEmail(postData, + Client.oauthToEmail( + this.props.email, + password, (data) => { if (data.follow_link) { browserHistory.push(data.follow_link); @@ -87,10 +84,9 @@ export default class OAuthToEmail extends React.Component {

    @@ -137,8 +133,6 @@ export default class OAuthToEmail extends React.Component { OAuthToEmail.defaultProps = { }; OAuthToEmail.propTypes = { - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, currentType: React.PropTypes.string, email: React.PropTypes.string }; diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index a91c03d58..e8fa59165 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -4,7 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -148,7 +148,6 @@ class CreateComment extends React.Component { Client.createPost( post, - ChannelStore.getCurrent(), (data) => { AsyncClient.getPosts(this.props.channelId); diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 232537d8b..9bbc44f38 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -11,7 +11,7 @@ import TutorialTip from './tutorial/tutorial_tip.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -172,7 +172,7 @@ class CreatePost extends React.Component { GlobalActions.emitUserPostedEvent(post); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - Client.createPost(post, channel, + Client.createPost(post, (data) => { AsyncClient.getPosts(); diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx new file mode 100644 index 000000000..e33eee1bc --- /dev/null +++ b/webapp/components/create_team/components/display_name.jsx @@ -0,0 +1,136 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import * as utils from 'utils/utils.jsx'; +import Client from 'utils/web_client.jsx'; +import {Link} from 'react-router'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +import logoImage from 'images/logo.png'; + +const holders = defineMessages({ + required: { + id: 'create_team_display_name.required', + defaultMessage: 'This field is required' + }, + charLength: { + id: 'create_team_display_name.charLength', + defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' + } +}); + +import React from 'react'; + +class TeamSignupDisplayNamePage extends React.Component { + constructor(props) { + super(props); + + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + + submitNext(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim(); + if (!displayName) { + this.setState({nameError: formatMessage(holders.required)}); + return; + } else if (displayName.length < 4 || displayName.length > 15) { + this.setState({nameError: formatMessage(holders.charLength)}); + return; + } + + this.props.state.wizard = 'team_url'; + this.props.state.team.display_name = displayName; + this.props.state.team.name = utils.cleanUpUrlable(displayName); + this.props.updateParent(this.props.state); + } + + handleFocus(e) { + e.preventDefault(); + e.currentTarget.select(); + } + + render() { + Client.track('signup', 'signup_team_02_name'); + + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = ; + nameDivClass += ' has-error'; + } + + return ( +

    +
    + +

    + +

    +
    +
    +
    + +
    +
    + {nameError} +
    +
    + +
    + +
    + + + +
    +
    +
    + ); + } +} + +TeamSignupDisplayNamePage.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamSignupDisplayNamePage); diff --git a/webapp/components/create_team/components/team_url.jsx b/webapp/components/create_team/components/team_url.jsx new file mode 100644 index 000000000..025103141 --- /dev/null +++ b/webapp/components/create_team/components/team_url.jsx @@ -0,0 +1,229 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import Client from 'utils/web_client.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import Constants from 'utils/constants.jsx'; +import {browserHistory} from 'react-router'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +import logoImage from 'images/logo.png'; + +const holders = defineMessages({ + required: { + id: 'create_team_url.required', + defaultMessage: 'This field is required' + }, + regex: { + id: 'create_team_url.regex', + defaultMessage: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash." + }, + charLength: { + id: 'create_team_url.charLength', + defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' + }, + taken: { + id: 'create_team_url.taken', + defaultMessage: 'URL is taken or contains a reserved word' + }, + unavailable: { + id: 'create_team_url.unavailable', + defaultMessage: 'This URL is unavailable. Please try another.' + } +}); + +import React from 'react'; + +class TeamUrl extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + this.handleFocus = this.handleFocus.bind(this); + + this.state = {nameError: ''}; + } + submitBack(e) { + e.preventDefault(); + this.props.state.wizard = 'display_name'; + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + const name = ReactDOM.findDOMNode(this.refs.name).value.trim(); + if (!name) { + this.setState({nameError: formatMessage(holders.required)}); + return; + } + + const cleanedName = Utils.cleanUpUrlable(name); + + const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; + if (cleanedName !== name || !urlRegex.test(name)) { + this.setState({nameError: formatMessage(holders.regex)}); + return; + } else if (cleanedName.length < 4 || cleanedName.length > 15) { + this.setState({nameError: formatMessage(holders.charLength)}); + return; + } + + if (global.window.mm_config.RestrictTeamNames === 'true') { + for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { + if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { + this.setState({nameError: formatMessage(holders.taken)}); + return; + } + } + } + + $('#finish-button').button('loading'); + var teamSignup = JSON.parse(JSON.stringify(this.props.state)); + teamSignup.team.type = 'O'; + teamSignup.team.name = name; + + Client.findTeamByName(name, + (findTeam) => { + if (findTeam) { + this.setState({nameError: formatMessage(holders.unavailable)}); + $('#finish-button').button('reset'); + } else { + Client.createTeam(teamSignup.team, + (team) => { + Client.track('signup', 'signup_team_08_complete'); + $('#sign-up-button').button('reset'); + TeamStore.saveTeam(team); + TeamStore.appendTeamMember({team_id: team.id, user_id: UserStore.getCurrentId(), roles: 'admin'}); + TeamStore.emitChange(); + browserHistory.push('/' + team.name + '/channels/town-square'); + }, + (err) => { + this.setState({nameError: err.message}); + $('#finish-button').button('reset'); + } + ); + + $('#finish-button').button('reset'); + } + }, + (err) => { + this.setState({nameError: err.message}); + $('#finish-button').button('reset'); + } + ); + } + handleFocus(e) { + e.preventDefault(); + + e.currentTarget.select(); + } + render() { + $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); + + Client.track('signup', 'signup_team_03_url'); + + let nameError = null; + let nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = ; + nameDivClass += ' has-error'; + } + + const title = `${Utils.getWindowLocationOrigin()}/`; + + return ( +
    + ); + } +} + +TeamUrl.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamUrl); diff --git a/webapp/components/create_team/create_team.jsx b/webapp/components/create_team/create_team.jsx new file mode 100644 index 000000000..8a119a122 --- /dev/null +++ b/webapp/components/create_team/create_team.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ErrorBar from 'components/error_bar.jsx'; + +import {FormattedMessage} from 'react-intl'; +import {browserHistory, Link} from 'react-router'; + +import React from 'react'; + +export default class CreateTeam extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + this.updateParent = this.updateParent.bind(this); + + const state = {}; + state.team = {}; + state.wizard = 'display_name'; + this.state = state; + } + + submit() { + // todo fill in + } + + componentDidMount() { + browserHistory.push('/create_team/display_name'); + } + + updateParent(state) { + this.setState(state); + browserHistory.push('/create_team/' + state.wizard); + } + + render() { + return ( +
    + +
    + + + + +
    +
    +
    +

    {global.window.mm_config.SiteName}

    +

    + +

    +
    + {React.cloneElement(this.props.children, { + state: this.state, + updateParent: this.updateParent + })} +
    +
    +
    +
    + ); + } +} + +CreateTeam.propTypes = { + children: React.PropTypes.node +}; diff --git a/webapp/components/delete_channel_modal.jsx b/webapp/components/delete_channel_modal.jsx index 244472a56..222b72c41 100644 --- a/webapp/components/delete_channel_modal.jsx +++ b/webapp/components/delete_channel_modal.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import {Modal} from 'react-bootstrap'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; diff --git a/webapp/components/delete_post_modal.jsx b/webapp/components/delete_post_modal.jsx index 0dbdc2b43..b2aea6590 100644 --- a/webapp/components/delete_post_modal.jsx +++ b/webapp/components/delete_post_modal.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import PostStore from 'stores/post_store.jsx'; import ModalStore from 'stores/modal_store.jsx'; import {Modal} from 'react-bootstrap'; diff --git a/webapp/components/do_verify_email.jsx b/webapp/components/do_verify_email.jsx index c3be111ed..fe4d61a72 100644 --- a/webapp/components/do_verify_email.jsx +++ b/webapp/components/do_verify_email.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import {FormattedMessage} from 'react-intl'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import LoadingScreen from './loading_screen.jsx'; import {browserHistory, Link} from 'react-router'; @@ -25,14 +25,14 @@ export default class DoVerifyEmail extends React.Component { const email = this.props.location.query.email; Client.verifyEmail( + uid, + hid, () => { browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email); }, (err) => { this.setState({verifyStatus: 'failure', serverError: err.message}); - }, - uid, - hid + } ); } render() { diff --git a/webapp/components/edit_channel_header_modal.jsx b/webapp/components/edit_channel_header_modal.jsx index 35a5fb9dc..6a7cccebb 100644 --- a/webapp/components/edit_channel_header_modal.jsx +++ b/webapp/components/edit_channel_header_modal.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; diff --git a/webapp/components/edit_channel_purpose_modal.jsx b/webapp/components/edit_channel_purpose_modal.jsx index 31cbdd240..a4779d022 100644 --- a/webapp/components/edit_channel_purpose_modal.jsx +++ b/webapp/components/edit_channel_purpose_modal.jsx @@ -4,7 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import Constants from 'utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -49,12 +49,9 @@ export default class EditChannelPurposeModal extends React.Component { return; } - const data = { - channel_id: this.props.channel.id, - channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim() - }; - - Client.updateChannelPurpose(data, + Client.updateChannelPurpose( + this.props.channel.id, + ReactDOM.findDOMNode(this.refs.purpose).value.trim(), () => { AsyncClient.getChannel(this.props.channel.id); diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx index caf9a0ee5..bc67a34f9 100644 --- a/webapp/components/edit_post_modal.jsx +++ b/webapp/components/edit_post_modal.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import Textbox from './textbox.jsx'; @@ -33,20 +33,25 @@ class EditPostModal extends React.Component { this.handleEdit = this.handleEdit.bind(this); this.handleEditInput = this.handleEditInput.bind(this); this.handleEditKeyPress = this.handleEditKeyPress.bind(this); - this.handleUserInput = this.handleUserInput.bind(this); this.handleEditPostEvent = this.handleEditPostEvent.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.onPreferenceChange = this.onPreferenceChange.bind(this); - this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''}; + this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''}; } handleEdit() { var updatedPost = {}; updatedPost.message = this.state.editText.trim(); + if (updatedPost.message === this.state.originalText.trim()) { + // no changes so just close the modal + $('#edit_post').modal('hide'); + return; + } + if (updatedPost.message.length === 0) { var tempState = this.state; - delete tempState.editText; + Reflect.deleteProperty(tempState, 'editText'); BrowserStore.setItem('edit_state_transfer', tempState); $('#edit_post').modal('hide'); GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); @@ -79,12 +84,10 @@ class EditPostModal extends React.Component { this.handleEdit(e); } } - handleUserInput(e) { - this.setState({editText: e.target.value}); - } handleEditPostEvent(options) { this.setState({ editText: options.message || '', + originalText: options.message || '', title: options.title || '', post_id: options.postId || '', channel_id: options.channelId || '', @@ -108,7 +111,7 @@ class EditPostModal extends React.Component { var self = this; $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => { - self.setState({editText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''}); + self.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''}); }); $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { @@ -116,7 +119,15 @@ class EditPostModal extends React.Component { if (!button) { return; } - self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refocusid')}); + self.setState({ + editText: $(button).attr('data-message'), + originalText: $(button).attr('data-message'), + title: $(button).attr('data-title'), + channel_id: $(button).attr('data-channelid'), + post_id: $(button).attr('data-postid'), + comments: $(button).attr('data-comments'), + refocusId: $(button).attr('data-refocusid') + }); }); $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => { @@ -163,17 +174,17 @@ class EditPostModal extends React.Component { aria-label='Close' onClick={this.handleEditClose} > - + -

    - -

    +

    + +

    Constants.MAX_FILE_SIZE) { @@ -81,18 +81,15 @@ class FileUpload extends React.Component { // generate a unique id that can be used by other components to refer back to this upload const clientId = Utils.generateId(); - // prepare data to be uploaded - var formData = new FormData(); - formData.append('channel_id', channelId); - formData.append('files', files[i], files[i].name); - formData.append('client_ids', clientId); - - var request = Client.uploadFile(formData, + const request = Client.uploadFile(files[i], + files[i].name, + channelId, + clientId, this.fileUploadSuccess.bind(this, channelId), this.fileUploadFail.bind(this, clientId) ); - var requests = this.state.requests; + const requests = this.state.requests; requests[clientId] = request; this.setState({requests}); @@ -231,8 +228,6 @@ class FileUpload extends React.Component { // generate a unique id that can be used by other components to refer back to this file upload var clientId = Utils.generateId(); - var formData = new FormData(); - formData.append('channel_id', channelId); var d = new Date(); var hour; if (d.getHours() < 10) { @@ -247,16 +242,17 @@ class FileUpload extends React.Component { min = String(d.getMinutes()); } - var name = formatMessage(holders.pasted) + d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext; - formData.append('files', file, name); - formData.append('client_ids', clientId); + const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext; - var request = Client.uploadFile(formData, + const request = Client.uploadFile(file, + name, + channelId, + clientId, self.fileUploadSuccess.bind(self, channelId), self.fileUploadFail.bind(self, clientId) ); - var requests = self.state.requests; + const requests = self.state.requests; requests[clientId] = request; self.setState({requests}); @@ -280,13 +276,13 @@ class FileUpload extends React.Component { } cancelUpload(clientId) { - var requests = this.state.requests; - var request = requests[clientId]; + const requests = JSON.parse(JSON.stringify(this.state.requests)); + const request = requests[clientId]; if (request) { request.abort(); - delete requests[clientId]; + Reflect.deleteProperty(requests, clientId); this.setState({requests}); } } diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx index bd6c49714..5aa0bd96c 100644 --- a/webapp/components/filtered_user_list.jsx +++ b/webapp/components/filtered_user_list.jsx @@ -113,6 +113,7 @@ class FilteredUserList extends React.Component { > @@ -124,6 +125,7 @@ class FilteredUserList extends React.Component { FilteredUserList.defaultProps = { users: [], + teamMembers: [], actions: [], actionProps: {} }; @@ -131,6 +133,7 @@ FilteredUserList.defaultProps = { FilteredUserList.propTypes = { intl: intlShape.isRequired, users: React.PropTypes.arrayOf(React.PropTypes.object), + teamMembers: React.PropTypes.arrayOf(React.PropTypes.object), actions: React.PropTypes.arrayOf(React.PropTypes.func), actionProps: React.PropTypes.object, style: React.PropTypes.object diff --git a/webapp/components/header_footer_template.jsx b/webapp/components/header_footer_template.jsx new file mode 100644 index 000000000..ce2f59541 --- /dev/null +++ b/webapp/components/header_footer_template.jsx @@ -0,0 +1,74 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; +import {Link} from 'react-router'; + +export default class NotLoggedIn extends React.Component { + componentDidMount() { + $('body').addClass('sticky'); + $('#root').addClass('container-fluid'); + } + componentWillUnmount() { + $('body').removeClass('sticky'); + $('#root').removeClass('container-fluid'); + } + render() { + return ( +
    +
    + {this.props.children} +
    +
    +
    +
    +
    + {global.window.mm_config.SiteName} +
    +
    + {'© 2015 Mattermost, Inc.'} + + + + + + + + + + + + +
    +
    +
    +
    + ); + } +} + +NotLoggedIn.defaultProps = { +}; + +NotLoggedIn.propTypes = { + children: React.PropTypes.object +}; diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index 17cec7aad..4ac620f08 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'; 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 Client from 'utils/web_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import ModalStore from 'stores/modal_store.jsx'; import UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 1dcb6b0aa..3941dd12c 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -2,39 +2,17 @@ // See License.txt for license information. import $ from 'jquery'; +import LoadingScreen from 'components/loading_screen.jsx'; 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 ChannelStore from 'stores/channel_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; -const TutorialSteps = Constants.TutorialSteps; -const Preferences = Constants.Preferences; -import ErrorBar from 'components/error_bar.jsx'; import * as Websockets from 'action_creators/websocket_actions.jsx'; -import LoadingScreen from 'components/loading_screen.jsx'; +import Constants from 'utils/constants.jsx'; import {browserHistory} from 'react-router'; -import SidebarRight from 'components/sidebar_right.jsx'; -import SidebarRightMenu from 'components/sidebar_right_menu.jsx'; -import Navbar from 'components/navbar.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; @@ -45,19 +23,22 @@ export default class LoggedIn extends React.Component { super(params); this.onUserChanged = this.onUserChanged.bind(this); + this.setupUser = this.setupUser.bind(this); this.state = { - user: null, - profiles: null + user: UserStore.getCurrentUser() }; + + if (this.state.user) { + this.setupUser(this.state.user); + } } + isValidState() { - return this.state.user != null && this.state.profiles != null; + return this.state.user != null; } - onUserChanged() { - // Grab the current user - const user = UserStore.getCurrentUser(); + setupUser(user) { // Update segment indentify if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') { global.window.analytics.identify(user.id, { @@ -65,7 +46,6 @@ export default class LoggedIn extends React.Component { email: user.email, createdAt: user.create_at, username: user.username, - team_id: user.team_id, id: user.id }); } @@ -78,25 +58,19 @@ export default class LoggedIn extends React.Component { Utils.applyTheme(Constants.THEMES.default); } } + } - // Go to tutorial if we are first arrivign - const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); - if (tutorialStep <= TutorialSteps.INTRO_SCREENS) { - browserHistory.push(Utils.getTeamURLFromAddressBar() + '/tutorial'); - } - - // Get profiles - const profiles = UserStore.getProfiles(); + onUserChanged() { + // Grab the current user + const user = UserStore.getCurrentUser(); + this.setupUser(user); this.setState({ - user, - profiles + user }); } - componentWillMount() { - // Emit view action - GlobalActions.viewLoggedIn(); + componentWillMount() { // Listen for user UserStore.addChangeListener(this.onUserChanged); @@ -116,7 +90,7 @@ export default class LoggedIn extends React.Component { } console.log('detected logout from a different tab'); //eslint-disable-line no-console - browserHistory.push('/' + this.props.params.team); + browserHistory.push('/'); } if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { @@ -170,18 +144,6 @@ export default class LoggedIn extends React.Component { $('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); @@ -193,13 +155,11 @@ export default class LoggedIn extends React.Component { } }); } + componentWillUnmount() { $('#root').attr('class', ''); clearInterval(this.intervalId); - $(window).off('focus'); - $(window).off('blur'); - Websockets.close(); UserStore.removeChangeListener(this.onUserChanged); @@ -211,75 +171,18 @@ export default class LoggedIn extends React.Component { $(window).off('keydown.preventBackspace'); } + render() { if (!this.isValidState()) { return ; } - let content = []; - if (this.props.children) { - content = this.props.children; - } else { - content.push( - this.props.navbar - ); - content.push( - this.props.sidebar - ); - content.push( -
    -
    - -
    -
    - {React.cloneElement(this.props.center, { - user: this.state.user, - profiles: this.state.profiles - })} -
    -
    - ); - } - return ( -
    - -
    - - - {content} - - - - - - - - - - - - -
    -
    - ); + return React.cloneElement(this.props.children, { + user: this.state.user + }); } } -LoggedIn.defaultProps = { -}; - LoggedIn.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]), - navbar: React.PropTypes.element, - sidebar: React.PropTypes.element, - center: React.PropTypes.element, - params: React.PropTypes.object + children: React.PropTypes.object }; diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login.jsx index a3dadbf36..c5955a1f4 100644 --- a/webapp/components/login/login.jsx +++ b/webapp/components/login/login.jsx @@ -5,12 +5,14 @@ import LoginEmail from './components/login_email.jsx'; import LoginUsername from './components/login_username.jsx'; import LoginLdap from './components/login_ldap.jsx'; import LoginMfa from './components/login_mfa.jsx'; +import ErrorBar from 'components/error_bar.jsx'; -import TeamStore from 'stores/team_store.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import UserStore from 'stores/user_store.jsx'; +import Client from 'utils/web_client.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import * as Client from 'utils/client.jsx'; + import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -18,40 +20,24 @@ import {FormattedMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router'; import React from 'react'; +import logoImage from 'images/logo.png'; export default class Login extends React.Component { constructor(props) { super(props); - this.getStateFromStores = this.getStateFromStores.bind(this); - this.onTeamChange = this.onTeamChange.bind(this); this.preSubmit = this.preSubmit.bind(this); this.submit = this.submit.bind(this); + this.finishSignin = this.finishSignin.bind(this); - const state = this.getStateFromStores(); - state.doneCheckLogin = false; + const state = {}; + state.showMfa = false; this.state = state; } componentDidMount() { - TeamStore.addChangeListener(this.onTeamChange); - Client.getMeLoggedIn((data) => { - if (data && data.logged_in !== 'false') { - browserHistory.push('/' + this.props.params.team + '/channels/town-square'); - } else { - this.setState({doneCheckLogin: true}); //eslint-disable-line react/no-did-mount-set-state - } - }); - } - componentWillUnmount() { - TeamStore.removeChangeListener(this.onTeamChange); - } - getStateFromStores() { - return { - currentTeam: TeamStore.getByName(this.props.params.team) - }; - } - onTeamChange() { - this.setState(this.getStateFromStores()); + if (UserStore.getCurrentUser()) { + browserHistory.push('/select_team'); + } } preSubmit(method, loginId, password) { if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') { @@ -59,7 +45,7 @@ export default class Login extends React.Component { return; } - Client.checkMfa(method, this.state.currentTeam.name, loginId, + Client.checkMfa(method, loginId, (data) => { if (data.mfa_required === 'true') { this.setState({showMfa: true, method, loginId, password}); @@ -78,27 +64,41 @@ export default class Login extends React.Component { } ); } + finishSignin() { + GlobalActions.emitInitialLoad( + () => { + browserHistory.push('/select_team'); + } + ); + } + submit(method, loginId, password, token) { this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null}); - const team = this.state.currentTeam.name; - if (method === Constants.EMAIL_SERVICE) { - Client.loginByEmail(team, loginId, password, token, + Client.webLogin( + loginId, + null, + password, + token, () => { UserStore.setLastEmail(loginId); - browserHistory.push('/' + team + '/channels/town-square'); + this.finishSignin(); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { - browserHistory.push('/should_verify_email?teamname=' + encodeURIComponent(team) + '&email=' + encodeURIComponent(loginId)); + browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId)); return; } this.setState({serverEmailError: err.message}); } ); } else if (method === Constants.USERNAME_SERVICE) { - Client.loginByUsername(team, loginId, password, token, + Client.webLogin( + null, + loginId, + password, + token, () => { UserStore.setLastUsername(loginId); @@ -106,7 +106,7 @@ export default class Login extends React.Component { if (redirect) { browserHistory.push(decodeURIComponent(redirect)); } else { - browserHistory.push('/' + team + '/channels/town-square'); + this.finishSignin(); } }, (err) => { @@ -120,13 +120,16 @@ export default class Login extends React.Component { } ); } else if (method === Constants.LDAP_SERVICE) { - Client.loginByLdap(team, loginId, password, token, + Client.loginByLdap( + loginId, + password, + token, () => { const redirect = Utils.getUrlParameter('redirect'); if (redirect) { browserHistory.push(decodeURIComponent(redirect)); } else { - browserHistory.push('/' + team + '/channels/town-square'); + this.finishSignin(); } }, (err) => { @@ -144,7 +147,7 @@ export default class Login extends React.Component { return (

    @@ -153,7 +156,7 @@ export default class Login extends React.Component { return null; } - createLoginOptions(currentTeam) { + createLoginOptions() { const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { @@ -187,10 +190,19 @@ export default class Login extends React.Component { />
    ); + } else if (extraParam === Constants.PASSWORD_CHANGE) { + extraBox = ( +
    + + +
    + ); } } - const teamName = currentTeam.name; const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true'; const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true'; @@ -200,10 +212,10 @@ export default class Login extends React.Component { const oauthLogins = []; if (gitlabSigninEnabled) { oauthLogins.push( - @@ -212,7 +224,7 @@ export default class Login extends React.Component { defaultMessage='with GitLab' /> - + ); } @@ -221,7 +233,7 @@ export default class Login extends React.Component { @@ -238,7 +250,6 @@ export default class Login extends React.Component { if (emailSigninEnabled) { emailLogin = ( @@ -263,7 +274,6 @@ export default class Login extends React.Component { if (usernameSigninEnabled) { usernameLogin = ( @@ -288,7 +298,6 @@ export default class Login extends React.Component { if (ldapEnabled) { ldapLogin = ( @@ -309,34 +318,31 @@ export default class Login extends React.Component { } } - let userSignUp; - if (currentTeam.allow_open_invite) { - userSignUp = ( -
    - + const userSignUp = ( +
    + + + - - - - -
    - ); - } + +
    +
    + ); let forgotPassword; if (usernameSigninEnabled || emailSigninEnabled) { forgotPassword = (
    - + - - - -
    - ); - } - return (
    {extraBox} @@ -372,16 +361,10 @@ export default class Login extends React.Component { {ldapLogin} {userSignUp} {forgotPassword} - {teamSignUp}
    ); } render() { - const currentTeam = this.state.currentTeam; - if (currentTeam == null || !this.state.doneCheckLogin) { - return
    ; - } - let content; let customContent; let customClass; @@ -395,7 +378,7 @@ export default class Login extends React.Component { /> ); } else { - content = this.createLoginOptions(currentTeam); + content = this.createLoginOptions(); customContent = this.createCustomLogin(); if (customContent) { customClass = 'branded'; @@ -404,36 +387,23 @@ export default class Login extends React.Component { return (
    -
    - - - - -
    +
    {customContent}
    + +

    {global.window.mm_config.SiteName}

    +

    + +

    -
    - -
    -

    {currentTeam.display_name}

    -

    - -

    {content}
    diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx index bb5eee496..d0714e942 100644 --- a/webapp/components/member_list_team.jsx +++ b/webapp/components/member_list_team.jsx @@ -4,6 +4,8 @@ import FilteredUserList from './filtered_user_list.jsx'; import TeamMembersDropdown from './team_members_dropdown.jsx'; import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import React from 'react'; @@ -13,18 +15,23 @@ export default class MemberListTeam extends React.Component { this.getUsers = this.getUsers.bind(this); this.onChange = this.onChange.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); this.state = { - users: this.getUsers() + users: this.getUsers(), + teamMembers: TeamStore.getMembersForTeam() }; } componentDidMount() { UserStore.addChangeListener(this.onChange); + TeamStore.addChangeListener(this.onTeamChange); + AsyncClient.getTeamMembers(TeamStore.getCurrentId()); } componentWillUnmount() { UserStore.removeChangeListener(this.onChange); + TeamStore.removeChangeListener(this.onTeamChange); } getUsers() { @@ -46,11 +53,18 @@ export default class MemberListTeam extends React.Component { }); } + onTeamChange() { + this.setState({ + teamMembers: TeamStore.getMembersForTeam() + }); + } + render() { return ( ); diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index 5ccf9c11e..3ab05341b 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -4,7 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import * as client from 'utils/client.jsx'; +import client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import LoadingScreen from './loading_screen.jsx'; diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index e4e64c12e..919a72d0a 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -18,7 +18,7 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index da1ae237e..19b99a14d 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; +import TeamStore from 'stores/team_store.jsx'; import AboutBuildModal from './about_build_modal.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; @@ -25,10 +26,13 @@ export default class NavbarDropdown extends React.Component { this.handleAboutModal = this.handleAboutModal.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); this.state = { showUserSettingsModal: false, - showAboutModal: false + showAboutModal: false, + teams: TeamStore.getAll(), + teamMembers: TeamStore.getTeamMembers() }; } handleAboutModal() { @@ -37,6 +41,7 @@ export default class NavbarDropdown extends React.Component { aboutModalDismissed() { this.setState({showAboutModal: false}); } + componentDidMount() { $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { $('.sidebar--left .dropdown-menu').scrollTop(0); @@ -45,10 +50,22 @@ export default class NavbarDropdown extends React.Component { this.blockToggle = false; }, 100); }); + + TeamStore.addChangeListener(this.onTeamChange); + } + + onTeamChange() { + this.setState({ + teams: TeamStore.getAll(), + teamMembers: TeamStore.getTeamMembers() + }); } + componentWillUnmount() { $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); + TeamStore.removeChangeListener(this.onTeamChange); } + render() { var teamLink = ''; var inviteLink = ''; @@ -130,7 +147,7 @@ export default class NavbarDropdown extends React.Component { if (isAdmin || window.EnableAdminOnlyIntegrations !== 'true') { integrationsLink = (
  • - + 0) { + teams.push( +
  • + ); + + for (var index in this.state.teamMembers) { + if (this.state.teamMembers.hasOwnProperty(index)) { + var teamMember = this.state.teamMembers[index]; + var team = this.state.teams[teamMember.team_id]; + + if (team.name !== this.props.teamName) { + teams.push( +
  • + + {team.display_name} + +
  • + ); + } + } + } + } + let helpLink = null; if (global.window.mm_config.HelpLink) { helpLink = ( @@ -245,12 +289,15 @@ export default class NavbarDropdown extends React.Component { {inviteLink} {teamLink}
  • - + - +
  • {adminDivider} {teamSettings} diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index f624b1ebc..59797f086 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -1,22 +1,156 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import React from 'react'; + +import $ from 'jquery'; + +import {browserHistory} from 'react-router'; +import * as Utils from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; +import Constants from 'utils/constants.jsx'; +const TutorialSteps = Constants.TutorialSteps; +const Preferences = Constants.Preferences; -import React from 'react'; +import ErrorBar from 'components/error_bar.jsx'; +import SidebarRight from 'components/sidebar_right.jsx'; +import SidebarRightMenu from 'components/sidebar_right_menu.jsx'; +import Navbar from 'components/navbar.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'; export default class NeedsTeam extends React.Component { + constructor(params) { + super(params); + + this.onChanged = this.onChanged.bind(this); + + this.state = { + profiles: UserStore.getProfiles(), + team: TeamStore.getCurrent() + }; + } + + onChanged() { + this.setState({ + profiles: UserStore.getProfiles(), + team: TeamStore.getCurrent() + }); + } + componentWillMount() { - GlobalActions.loadTeamRequiredPage(); + UserStore.addChangeListener(this.onChanged); + TeamStore.addChangeListener(this.onChanged); + + // Emit view action + GlobalActions.viewLoggedIn(); + + // Go to tutorial if we are first arrivign + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + if (tutorialStep <= TutorialSteps.INTRO_SCREENS) { + browserHistory.push(Utils.getTeamURLFromAddressBar() + '/tutorial'); + } + + // 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; + }); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChanged); + TeamStore.addChangeListener(this.onChanged); + $(window).off('focus'); + $(window).off('blur'); } + render() { - return this.props.children; + let content = []; + if (this.props.children) { + content = this.props.children; + } else { + content.push( + this.props.navbar + ); + content.push( + this.props.sidebar + ); + content.push( +
    +
    + +
    +
    + {React.cloneElement(this.props.center, { + user: this.props.user, + profiles: this.state.profiles, + team: this.state.team + })} +
    +
    + ); + } + return ( +
    + +
    + + + {content} + + + + + + + + + + + + +
    +
    + ); } } -NeedsTeam.defaultProps = { -}; - NeedsTeam.propTypes = { - children: React.PropTypes.object + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]), + navbar: React.PropTypes.element, + sidebar: React.PropTypes.element, + center: React.PropTypes.element, + params: React.PropTypes.object, + user: React.PropTypes.object }; diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx index 82494dac0..7019da4aa 100644 --- a/webapp/components/new_channel_flow.jsx +++ b/webapp/components/new_channel_flow.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import UserStore from 'stores/user_store.jsx'; import NewChannelModal from './new_channel_modal.jsx'; @@ -106,11 +106,7 @@ class NewChannelFlow extends React.Component { (data) => { Client.getChannel( data.id, - (data2, textStatus, xhr) => { - if (xhr.status === 304 || !data2) { - return; - } - + (data2) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNEL, channel: data2.channel, diff --git a/webapp/components/not_logged_in.jsx b/webapp/components/not_logged_in.jsx deleted file mode 100644 index 4beee6259..000000000 --- a/webapp/components/not_logged_in.jsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; -import {Link} from 'react-router'; - -export default class NotLoggedIn extends React.Component { - componentDidMount() { - $('body').attr('class', 'sticky'); - $('#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/webapp/components/notify_counts.jsx b/webapp/components/notify_counts.jsx index acc64dfb0..9238c8736 100644 --- a/webapp/components/notify_counts.jsx +++ b/webapp/components/notify_counts.jsx @@ -32,17 +32,22 @@ export default class NotifyCounts extends React.Component { this.onListenerChange = this.onListenerChange.bind(this); this.state = getCountsStateFromStores(); + this.mounted = false; } componentDidMount() { + this.mounted = true; ChannelStore.addChangeListener(this.onListenerChange); } componentWillUnmount() { + this.mounted = false; ChannelStore.removeChangeListener(this.onListenerChange); } onListenerChange() { - var newState = getCountsStateFromStores(); - if (!utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); + if (this.mounted) { + var newState = getCountsStateFromStores(); + if (!utils.areObjectsEqual(newState, this.state)) { + this.setState(newState); + } } } render() { diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx index 863420902..23b8952cc 100644 --- a/webapp/components/password_reset_form.jsx +++ b/webapp/components/password_reset_form.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import ReactDOM from 'react-dom'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -40,16 +40,12 @@ class PasswordResetForm extends React.Component { error: null }); - const data = {}; - data.new_password = password; - data.hash = this.props.location.query.h; - data.data = this.props.location.query.d; - data.name = this.props.params.team; - - Client.resetPassword(data, + Client.resetPassword( + this.props.location.query.code, + password, () => { this.setState({error: null}); - browserHistory.push('/' + this.props.params.team + '/login'); + browserHistory.push('/login?extra=' + Constants.PASSWORD_CHANGE); }, (err) => { this.setState({error: err.message}); diff --git a/webapp/components/password_reset_send_link.jsx b/webapp/components/password_reset_send_link.jsx index e3ab8949e..65d9439bd 100644 --- a/webapp/components/password_reset_send_link.jsx +++ b/webapp/components/password_reset_send_link.jsx @@ -4,7 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import * as client from 'utils/client.jsx'; +import client from 'utils/web_client.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; @@ -43,10 +43,8 @@ class PasswordResetSendLink extends React.Component { error: '' }); - var data = {}; - data.email = email; - data.name = this.props.params.team; - client.sendPasswordReset(data, + client.sendPasswordReset( + email, () => { this.setState({ error: null, diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index 8c3c381af..387c37ab5 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -7,6 +7,7 @@ import UserStore from 'stores/user_store.jsx'; import {Popover, Overlay} from 'react-bootstrap'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; +import Client from 'utils/web_client.jsx'; import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router'; @@ -97,7 +98,7 @@ export default class PopoverListMembers extends React.Component { className='more-modal__image' width='26px' height='26px' - src={`/api/v1/users/${m.id}/image?time=${m.update_at}`} + src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`} />
    { AsyncClient.getPosts(); diff --git a/webapp/components/register_app_modal.jsx b/webapp/components/register_app_modal.jsx index c632233cf..82a095c75 100644 --- a/webapp/components/register_app_modal.jsx +++ b/webapp/components/register_app_modal.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import ModalStore from 'stores/modal_store.jsx'; import {Modal} from 'react-bootstrap'; diff --git a/webapp/components/rename_channel_modal.jsx b/webapp/components/rename_channel_modal.jsx index 3e47847e7..ed045da91 100644 --- a/webapp/components/rename_channel_modal.jsx +++ b/webapp/components/rename_channel_modal.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import Constants from 'utils/constants.jsx'; diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 53170ee15..709865dc1 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -9,7 +9,7 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from 'utils/text_formatting.jsx'; @@ -43,7 +43,7 @@ class RhsComment extends React.Component { e.preventDefault(); var post = this.props.post; - Client.createPost(post, post.channel_id, + Client.createPost(post, (data) => { AsyncClient.getPosts(post.channel_id); @@ -261,7 +261,7 @@ class RhsComment extends React.Component {
    diff --git a/webapp/components/rhs_header_post.jsx b/webapp/components/rhs_header_post.jsx index 189ee0acb..493040800 100644 --- a/webapp/components/rhs_header_post.jsx +++ b/webapp/components/rhs_header_post.jsx @@ -3,6 +3,7 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; @@ -21,16 +22,7 @@ export default class RhsHeaderPost extends React.Component { } handleClose(e) { e.preventDefault(); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: null - }); + GlobalActions.emitCloseRightHandSide(); } handleBack(e) { e.preventDefault(); diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx index 59364d085..0adbc7f04 100644 --- a/webapp/components/root.jsx +++ b/webapp/components/root.jsx @@ -1,10 +1,10 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; +//import $ from 'jquery'; +//import Client from 'utils/web_client.jsx'; + import * as GlobalActions from 'action_creators/global_actions.jsx'; -import * as Client from 'utils/client.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; import LocalizationStore from 'stores/localization_store.jsx'; import {IntlProvider} from 'react-intl'; @@ -14,6 +14,7 @@ import React from 'react'; import FastClick from 'fastclick'; import {browserHistory} from 'react-router'; +import UserStore from 'stores/user_store.jsx'; export default class Root extends React.Component { constructor(props) { @@ -29,15 +30,16 @@ export default class Root extends React.Component { localizationChanged() { this.setState({locale: LocalizationStore.getLocale(), translations: LocalizationStore.getTranslations()}); } + redirectIfNecessary(props) { if (props.location.pathname === '/') { - Client.getMeLoggedIn((data) => { - if (!data || data.logged_in === 'false') { - browserHistory.push('/signup_team'); - } else { - browserHistory.push('/' + data.team_name + '/channels/town-square'); - } - }); + if (UserStore.getNoAccounts()) { + browserHistory.push('/signup_user_complete'); + } else if (UserStore.getCurrentUser()) { + browserHistory.push('/select_team'); + } else { + browserHistory.push('/login'); + } } } componentWillReceiveProps(newProps) { @@ -47,28 +49,6 @@ export default class Root extends React.Component { // Setup localization listener LocalizationStore.addChangeListener(this.localizationChanged); - // Browser store check version - BrowserStore.checkVersion(); - - window.onerror = (msg, url, line, column, stack) => { - 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 !== "") { @@ -76,11 +56,7 @@ export default class Root extends React.Component { analytics.load(window.mm_config.SegmentDeveloperKey); analytics.page(); }}(); - } else { - global.window.analytics = {}; - global.window.analytics.page = function(){}; - global.window.analytics.track = function(){}; - } + } /*eslint-enable */ // Fastclick diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx index caaf0f844..1156ac0f1 100644 --- a/webapp/components/search_bar.jsx +++ b/webapp/components/search_bar.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; -import * as client from 'utils/client.jsx'; +import client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import SearchStore from 'stores/search_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx new file mode 100644 index 000000000..5804a641e --- /dev/null +++ b/webapp/components/select_team/select_team.jsx @@ -0,0 +1,257 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as Utils from 'utils/utils.jsx'; +import ErrorBar from 'components/error_bar.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; +import Client from 'utils/web_client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; + +import * as TextFormatting from 'utils/text_formatting.jsx'; + +import {Link} from 'react-router'; + +import {FormattedMessage} from 'react-intl'; + +//import {browserHistory, Link} from 'react-router'; + +import React from 'react'; +import logoImage from 'images/logo.png'; + +export default class Login extends React.Component { + + constructor(props) { + super(props); + this.onTeamChange = this.onTeamChange.bind(this); + + const state = this.getStateFromStores(false); + this.state = state; + } + + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + AsyncClient.getAllTeamListings(); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + + onTeamChange() { + this.setState(this.getStateFromStores(true)); + } + + getStateFromStores(loaded) { + return { + teams: TeamStore.getAll(), + teamMembers: TeamStore.getTeamMembers(), + teamListings: TeamStore.getTeamListings(), + loaded + }; + } + + createCustomLogin() { + if (global.window.mm_license.IsLicensed === 'true' && + global.window.mm_license.CustomBrand === 'true' && + global.window.mm_config.EnableCustomBrand === 'true') { + const text = global.window.mm_config.CustomBrandText || ''; + + return ( +
    + +

    +

    + ); + } + + return null; + } + + render() { + var content; + + let customClass; + const customContent = this.createCustomLogin(); + if (customContent) { + customClass = 'branded'; + } + + var teamContents = []; + var isAlreadyMember = new Map(); + + for (var index in this.state.teamMembers) { + if (this.state.teamMembers.hasOwnProperty(index)) { + var teamMember = this.state.teamMembers[index]; + var team = this.state.teams[teamMember.team_id]; + isAlreadyMember[teamMember.team_id] = true; + teamContents.push( +
    + + {team.display_name} +
    + ); + } + } + + if (!teamContents || teamContents.length === 0) { + teamContents = ( +
    +
    + +
    +
    + ); + } + + content = ( +
    +

    + +

    +
    + {teamContents} +
    +
    + ); + + var openTeamContents = []; + + for (var id in this.state.teamListings) { + if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) { + var openTeam = this.state.teamListings[id]; + openTeamContents.push( +
    + + {openTeam.display_name} +
    + ); + } + } + + var openContent; + if (openTeamContents.length > 0) { + openContent = ( +
    +

    + +

    +
    + {openTeamContents} +
    +
    + ); + } + + if (!this.state.loaded) { + openContent = ; + } + + var isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles); + + let teamSignUp; + if (isSystemAdmin || (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp())) { + teamSignUp = ( +
    + + + +
    + ); + } + + let adminConsoleLink; + if (isSystemAdmin) { + adminConsoleLink = ( +
    + + + +
    + ); + } + + return ( +
    + + +
    +
    +
    + {customContent} +
    + +

    {global.window.mm_config.SiteName}

    +

    + +

    + {content} + {openContent} + {teamSignUp} + {adminConsoleLink} +
    +
    +
    + ); + } +} diff --git a/webapp/components/should_verify_email.jsx b/webapp/components/should_verify_email.jsx index 5103452b0..a95101ba1 100644 --- a/webapp/components/should_verify_email.jsx +++ b/webapp/components/should_verify_email.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import {FormattedMessage} from 'react-intl'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import {Link} from 'react-router'; @@ -18,19 +18,19 @@ export default class ShouldVerifyEmail extends React.Component { }; } 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); + Client.resendVerification( + email, + () => { + this.setState({resendStatus: 'success'}); + }, + () => { + this.setState({resendStatus: 'failure'}); + } + ); } render() { let resendConfirm = ''; diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx index ec3a03d17..143a3458a 100644 --- a/webapp/components/sidebar_header.jsx +++ b/webapp/components/sidebar_header.jsx @@ -7,6 +7,7 @@ import NavbarDropdown from './navbar_dropdown.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import Client from 'utils/web_client.jsx'; import Constants from 'utils/constants.jsx'; const Preferences = Constants.Preferences; @@ -61,7 +62,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( + - +
  • {helpLink} diff --git a/webapp/components/signup_team.jsx b/webapp/components/signup_team.jsx deleted file mode 100644 index 634aa6e68..000000000 --- a/webapp/components/signup_team.jsx +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChoosePage from './team_signup_choose_auth.jsx'; -import EmailSignUpPage from './team_signup_with_email.jsx'; -import SSOSignupPage from './team_signup_with_sso.jsx'; -import LdapSignUpPage from './team_signup_with_ldap.jsx'; -import Constants from 'utils/constants.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; - -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; -import {Link} from 'react-router'; - -import logoImage from 'images/logo.png'; - -export default class TeamSignUp extends React.Component { - constructor(props) { - super(props); - - this.updatePage = this.updatePage.bind(this); - this.onTeamUpdate = this.onTeamUpdate.bind(this); - - var count = 0; - - if (global.window.mm_config.EnableSignUpWithEmail === 'true') { - count = count + 1; - } - - if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { - count = count + 1; - } - - if (global.window.mm_config.EnableLdap === 'true') { - count = count + 1; - } - - if (count > 1) { - this.state = {page: 'choose'}; - } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') { - this.state = {page: 'email'}; - } else if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { - this.state = {page: 'gitlab'}; - } else if (global.window.mm_config.EnableLdap === 'true') { - this.state = {page: 'ldap'}; - } else { - this.state = {page: 'none'}; - } - } - - updatePage(page) { - this.setState({page}); - } - - componentWillMount() { - if (global.window.mm_config.EnableTeamListing === 'true') { - AsyncClient.getAllTeams(); - this.onTeamUpdate(); - } - } - - componentDidMount() { - TeamStore.addChangeListener(this.onTeamUpdate); - } - - componentWillUnmount() { - TeamStore.removeChangeListener(this.onTeamUpdate); - } - - onTeamUpdate() { - this.setState({ - teams: TeamStore.getAll() - }); - } - - render() { - let teamListing = null; - - if (global.window.mm_config.EnableTeamListing === 'true') { - if (this.state.teams == null) { - teamListing = (
    ); - } else if (this.state.teams.length === 0) { - if (global.window.mm_config.EnableTeamCreation !== 'true') { - teamListing = ( -
    - -
    - ); - } - } else { - teamListing = ( -
    -

    - -

    -
    - { - Object.values(this.state.teams).map((team) => { - if (team.allow_team_listing) { - return ( -
    - - {team.display_name} -
    - ); - } - return null; - }) - } -
    -

    - -

    -
    - ); - } - } - - let signupMethod = null; - let goBack = ( - - ); - - if (global.window.mm_config.EnableTeamCreation !== 'true') { - if (teamListing == null) { - signupMethod = ( - - ); - } - } else if (this.state.page === 'choose') { - signupMethod = ( - - ); - goBack = null; - } else if (this.state.page === 'email') { - signupMethod = ( -
    - -
    - ); - } else if (this.state.page === 'ldap') { - return ( -
    - {teamListing} - -
    - ); - } else if (this.state.page === 'gitlab') { - signupMethod = ( - - ); - } else if (this.state.page === 'google') { - signupMethod = ( - - ); - } else if (this.state.page === 'none') { - signupMethod = ( - - ); - goBack = null; - } - - return ( -
    - {goBack} -
    -
    - -

    {global.window.mm_config.SiteName}

    -

    - -

    -
    - {teamListing} - {signupMethod} -
    -
    -
    -
    - ); - } -} - -TeamSignUp.propTypes = { -}; - diff --git a/webapp/components/signup_team_complete/components/signup_team_complete.jsx b/webapp/components/signup_team_complete/components/signup_team_complete.jsx index 95b41dbde..00fdafe5f 100644 --- a/webapp/components/signup_team_complete/components/signup_team_complete.jsx +++ b/webapp/components/signup_team_complete/components/signup_team_complete.jsx @@ -74,6 +74,7 @@ export default class SignupTeamComplete extends React.Component { SignupTeamComplete.defaultProps = { }; + SignupTeamComplete.propTypes = { location: React.PropTypes.object, children: React.PropTypes.node diff --git a/webapp/components/signup_team_complete/components/team_signup_display_name_page.jsx b/webapp/components/signup_team_complete/components/team_signup_display_name_page.jsx deleted file mode 100644 index 111fc6835..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_display_name_page.jsx +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as utils from 'utils/utils.jsx'; -import * as client from 'utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; - -import logoImage from 'images/logo.png'; - -const holders = defineMessages({ - required: { - id: 'team_signup_display_name.required', - defaultMessage: 'This field is required' - }, - charLength: { - id: 'team_signup_display_name.charLength', - defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' - } -}); - -import React from 'react'; - -class TeamSignupDisplayNamePage extends React.Component { - constructor(props) { - super(props); - - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - - this.state = {}; - } - submitBack(e) { - e.preventDefault(); - this.props.state.wizard = 'welcome'; - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - const {formatMessage} = this.props.intl; - var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim(); - if (!displayName) { - this.setState({nameError: formatMessage(holders.required)}); - return; - } else if (displayName.length < 4 || displayName.length > 15) { - this.setState({nameError: formatMessage(holders.charLength)}); - return; - } - - this.props.state.wizard = 'team_url'; - this.props.state.team.display_name = displayName; - this.props.state.team.name = utils.cleanUpUrlable(displayName); - this.props.updateParent(this.props.state); - } - handleFocus(e) { - e.preventDefault(); - e.currentTarget.select(); - } - render() { - client.track('signup', 'signup_team_02_name'); - - var nameError = null; - var nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = ; - nameDivClass += ' has-error'; - } - - return ( -
    -
    - -

    - -

    -
    -
    -
    - -
    -
    - {nameError} -
    -
    - -
    - -
    - - - -
    -
    -
    - ); - } -} - -TeamSignupDisplayNamePage.propTypes = { - intl: intlShape.isRequired, - state: React.PropTypes.object, - updateParent: React.PropTypes.func -}; - -export default injectIntl(TeamSignupDisplayNamePage); diff --git a/webapp/components/signup_team_complete/components/team_signup_email_item.jsx b/webapp/components/signup_team_complete/components/team_signup_email_item.jsx deleted file mode 100644 index c3903ca85..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_email_item.jsx +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; - -import {intlShape, injectIntl, defineMessages} from 'react-intl'; - -const holders = defineMessages({ - validEmail: { - id: 'team_signup_email.validEmail', - defaultMessage: 'Please enter a valid email address' - }, - different: { - id: 'team_signup_email.different', - defaultMessage: 'Please use a different email than the one used at signup' - }, - address: { - id: 'team_signup_email.address', - defaultMessage: 'Email Address' - } -}); - -import React from 'react'; - -class TeamSignupEmailItem extends React.Component { - constructor(props) { - super(props); - - this.getValue = this.getValue.bind(this); - this.validate = this.validate.bind(this); - - this.state = {}; - } - getValue() { - return ReactDOM.findDOMNode(this.refs.email).value.trim(); - } - validate(teamEmail) { - const {formatMessage} = this.props.intl; - const email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - - if (!email) { - return true; - } - - if (!Utils.isEmail(email)) { - this.setState({emailError: formatMessage(holders.validEmail)}); - return false; - } else if (email === teamEmail) { - this.setState({emailError: formatMessage(holders.different)}); - return false; - } - - this.setState({emailError: ''}); - return true; - } - render() { - let emailError = null; - let emailDivClass = 'form-group'; - if (this.state.emailError) { - emailError = ; - emailDivClass += ' has-error'; - } - - return ( -
    - - {emailError} -
    - ); - } -} - -TeamSignupEmailItem.propTypes = { - intl: intlShape.isRequired, - focus: React.PropTypes.bool, - email: React.PropTypes.string -}; - -export default injectIntl(TeamSignupEmailItem, {withRef: true}); diff --git a/webapp/components/signup_team_complete/components/team_signup_finished.jsx b/webapp/components/signup_team_complete/components/team_signup_finished.jsx deleted file mode 100644 index 9fbb8473a..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_finished.jsx +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; - -export default class FinishedPage extends React.Component { - render() { - return ( - - ); - } -} diff --git a/webapp/components/signup_team_complete/components/team_signup_password_page.jsx b/webapp/components/signup_team_complete/components/team_signup_password_page.jsx deleted file mode 100644 index 7b8b49e0c..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_password_page.jsx +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import * as Client from 'utils/client.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; - -import logoImage from 'images/logo.png'; - -const holders = defineMessages({ - passwordError: { - id: 'team_signup_password.passwordError', - defaultMessage: 'Please enter at least {chars} characters' - }, - creating: { - id: 'team_signup_password.creating', - defaultMessage: 'Creating team...' - } -}); - -import React from 'react'; - -class TeamSignupPasswordPage extends React.Component { - constructor(props) { - super(props); - - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - - this.state = {}; - } - submitBack(e) { - e.preventDefault(); - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); - if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { - this.setState({passwordError: this.props.intl.formatMessage(holders.passwordError, {chars: Constants.MIN_PASSWORD_LENGTH})}); - return; - } - - this.setState({passwordError: null, serverError: null}); - $('#finish-button').button('loading'); - var teamSignup = JSON.parse(JSON.stringify(this.props.state)); - teamSignup.user.password = password; - teamSignup.user.allow_marketing = true; - delete teamSignup.wizard; - - Client.createTeamFromSignup(teamSignup, - () => { - Client.track('signup', 'signup_team_08_complete'); - - var props = this.props; - - Client.loginByEmail( - teamSignup.team.name, - teamSignup.team.email, - teamSignup.user.password, - '', // No MFA Token - () => { - UserStore.setLastEmail(teamSignup.team.email); - if (this.props.hash > 0) { - BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); - } - - $('#sign-up-button').button('reset'); - props.state.wizard = 'finished'; - props.updateParent(props.state, true); - - browserHistory.push('/' + teamSignup.team.name + '/channels/town-square'); - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - browserHistory.push('/should_verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name)); - } else { - this.setState({serverError: err.message}); - $('#finish-button').button('reset'); - } - } - ); - }, - (err) => { - this.setState({serverError: err.message}); - $('#finish-button').button('reset'); - } - ); - } - render() { - Client.track('signup', 'signup_team_07_password'); - - var passwordError = null; - var passwordDivStyle = 'form-group'; - if (this.state.passwordError) { - passwordError =
    ; - passwordDivStyle = ' has-error'; - } - - var serverError = null; - if (this.state.serverError) { - serverError =
    ; - } - - return ( -
    -
    - -

    - -

    -
    - -
    -
    -
    - -
    -
    {this.props.state.team.email}
    -
    -
    -
    -
    - -
    - - - - -
    -
    - {passwordError} - {serverError} -
    -
    -
    - -
    -

    - -

    -
    - - - -
    -
    -
    - ); - } -} - -TeamSignupPasswordPage.defaultProps = { - state: {}, - hash: '' -}; -TeamSignupPasswordPage.propTypes = { - intl: intlShape.isRequired, - state: React.PropTypes.object, - hash: React.PropTypes.string, - updateParent: React.PropTypes.func -}; - -export default injectIntl(TeamSignupPasswordPage); diff --git a/webapp/components/signup_team_complete/components/team_signup_send_invites_page.jsx b/webapp/components/signup_team_complete/components/team_signup_send_invites_page.jsx deleted file mode 100644 index db060b6b9..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_send_invites_page.jsx +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import EmailItem from './team_signup_email_item.jsx'; -import * as Client from 'utils/client.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import logoImage from 'images/logo.png'; - -import React from 'react'; - -export default class TeamSignupSendInvitesPage extends React.Component { - constructor(props) { - super(props); - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - this.submitAddInvite = this.submitAddInvite.bind(this); - this.submitSkip = this.submitSkip.bind(this); - this.keySubmit = this.keySubmit.bind(this); - this.state = { - emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' - }; - } - submitBack(e) { - e.preventDefault(); - this.props.state.wizard = 'team_url'; - - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - var valid = true; - - if (this.state.emailEnabled) { - var emails = []; - - for (var i = 0; i < this.props.state.invites.length; i++) { - if (this.refs['email_' + i].getWrappedInstance().validate(this.props.state.team.email)) { - emails.push(this.refs['email_' + i].getWrappedInstance().getValue()); - } else { - valid = false; - } - } - - if (valid) { - this.props.state.invites = emails; - } - } - - if (valid) { - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); - } - } - submitAddInvite(e) { - e.preventDefault(); - this.props.state.wizard = 'send_invites'; - if (!this.props.state.invites) { - this.props.state.invites = []; - } - this.props.state.invites.push(''); - this.props.updateParent(this.props.state); - } - submitSkip(e) { - e.preventDefault(); - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); - } - keySubmit(e) { - if (e && e.keyCode === 13) { - this.submitNext(e); - } - } - componentDidMount() { - if (!this.state.emailEnabled) { - // Must use keypress not keyup due to event chain of pressing enter - $('body').keypress(this.keySubmit); - } - } - componentWillUnmount() { - if (!this.state.emailEnabled) { - $('body').off('keypress', this.keySubmit); - } - } - render() { - Client.track('signup', 'signup_team_05_send_invites'); - - var content = null; - var bottomContent = null; - - if (this.state.emailEnabled) { - var emails = []; - - for (var i = 0; i < this.props.state.invites.length; i++) { - if (i === 0) { - emails.push( - - ); - } else { - emails.push( - - ); - } - } - - content = ( -
    - {emails} -
    - - - -
    -
    - ); - - bottomContent = ( -

    - - - - - -

    - ); - } else { - content = ( -
    - -
    - ); - } - - return ( -
    -
    - -

    - -

    - {content} -
    - -
    - - {bottomContent} -
    - - - -
    -
    - ); - } -} - -TeamSignupSendInvitesPage.propTypes = { - state: React.PropTypes.object.isRequired, - updateParent: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/signup_team_complete/components/team_signup_url_page.jsx b/webapp/components/signup_team_complete/components/team_signup_url_page.jsx deleted file mode 100644 index b2ab57285..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_url_page.jsx +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; -import Constants from 'utils/constants.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import logoImage from 'images/logo.png'; - -const holders = defineMessages({ - required: { - id: 'team_signup_url.required', - defaultMessage: 'This field is required' - }, - regex: { - id: 'team_signup_url.regex', - defaultMessage: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash." - }, - charLength: { - id: 'team_signup_url.charLength', - defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' - }, - taken: { - id: 'team_signup_url.taken', - defaultMessage: 'URL is taken or contains a reserved word' - }, - unavailable: { - id: 'team_signup_url.unavailable', - defaultMessage: 'This URL is unavailable. Please try another.' - } -}); - -import React from 'react'; - -class TeamSignupUrlPage extends React.Component { - constructor(props) { - super(props); - - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - this.handleFocus = this.handleFocus.bind(this); - - this.state = {nameError: ''}; - } - submitBack(e) { - e.preventDefault(); - this.props.state.wizard = 'team_display_name'; - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - const {formatMessage} = this.props.intl; - const name = ReactDOM.findDOMNode(this.refs.name).value.trim(); - if (!name) { - this.setState({nameError: formatMessage(holders.required)}); - return; - } - - const cleanedName = Utils.cleanUpUrlable(name); - - const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; - if (cleanedName !== name || !urlRegex.test(name)) { - this.setState({nameError: formatMessage(holders.regex)}); - return; - } else if (cleanedName.length < 4 || cleanedName.length > 15) { - this.setState({nameError: formatMessage(holders.charLength)}); - return; - } - - if (global.window.mm_config.RestrictTeamNames === 'true') { - for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { - if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { - this.setState({nameError: formatMessage(holders.taken)}); - return; - } - } - } - - Client.findTeamByName(name, - (data) => { - if (data) { - this.setState({nameError: formatMessage(holders.unavailable)}); - } else { - if (global.window.mm_config.SendEmailNotifications === 'true') { - this.props.state.wizard = 'send_invites'; - } else { - this.props.state.wizard = 'username'; - } - this.props.state.team.type = 'O'; - - this.props.state.team.name = name; - this.props.updateParent(this.props.state); - } - }, - (err) => { - this.setState({nameError: err.message}); - } - ); - } - handleFocus(e) { - e.preventDefault(); - - e.currentTarget.select(); - } - render() { - $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); - - Client.track('signup', 'signup_team_03_url'); - - let nameError = null; - let nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = ; - nameDivClass += ' has-error'; - } - - const title = `${Utils.getWindowLocationOrigin()}/`; - - return ( -
    -
    - -

    - -

    -
    -
    -
    -
    - - {title} - - -
    -
    -
    - {nameError} -
    -

    - -

    -
      - -
    - -
    - - - -
    -
    -
    - ); - } -} - -TeamSignupUrlPage.propTypes = { - intl: intlShape.isRequired, - state: React.PropTypes.object, - updateParent: React.PropTypes.func -}; - -export default injectIntl(TeamSignupUrlPage); diff --git a/webapp/components/signup_team_complete/components/team_signup_username_page.jsx b/webapp/components/signup_team_complete/components/team_signup_username_page.jsx deleted file mode 100644 index b79c1179f..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_username_page.jsx +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -import logoImage from 'images/logo.png'; - -const holders = defineMessages({ - reserved: { - id: 'team_signup_username.reserved', - defaultMessage: 'This username is reserved, please choose a new one.' - }, - invalid: { - id: 'team_signup_username.invalid', - defaultMessage: 'Username must begin with a letter, and contain between {min} to {max} characters in total, which may be numbers, lowercase letters, or any of the symbols \'.\', \'-\', or \'_\'' - } -}); - -import React from 'react'; - -class TeamSignupUsernamePage extends React.Component { - constructor(props) { - super(props); - - this.submitBack = this.submitBack.bind(this); - this.submitNext = this.submitNext.bind(this); - - this.state = {}; - } - submitBack(e) { - e.preventDefault(); - if (global.window.mm_config.SendEmailNotifications === 'true') { - this.props.state.wizard = 'send_invites'; - } else { - this.props.state.wizard = 'team_url'; - } - - this.props.updateParent(this.props.state); - } - submitNext(e) { - e.preventDefault(); - - const {formatMessage} = this.props.intl; - var name = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase(); - - var usernameError = Utils.isValidUsername(name); - if (usernameError === 'Cannot use a reserved word as a username.') { //this should be change to some kind of ID - this.setState({nameError: formatMessage(holders.reserved)}); - return; - } else if (usernameError) { - this.setState({nameError: formatMessage(holders.invalid, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH})}); - return; - } - - this.props.state.wizard = 'password'; - this.props.state.user.username = name; - this.props.updateParent(this.props.state); - } - render() { - Client.track('signup', 'signup_team_06_username'); - - var nameError = null; - var nameHelpText = ( - - - - ); - var nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = ; - nameHelpText = ''; - nameDivClass += ' has-error'; - } - - return ( -
    -
    - -

    - -

    -
    - -
    -
    -
    -
    -
    -
    - -
    - - {nameHelpText} -
    -
    - {nameError} -
    -
    - -
    - - - -
    -
    -
    - ); - } -} - -TeamSignupUsernamePage.defaultProps = { - state: null -}; -TeamSignupUsernamePage.propTypes = { - intl: intlShape.isRequired, - state: React.PropTypes.object, - updateParent: React.PropTypes.func -}; - -export default injectIntl(TeamSignupUsernamePage); diff --git a/webapp/components/signup_team_complete/components/team_signup_welcome_page.jsx b/webapp/components/signup_team_complete/components/team_signup_welcome_page.jsx deleted file mode 100644 index 15b708128..000000000 --- a/webapp/components/signup_team_complete/components/team_signup_welcome_page.jsx +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import {browserHistory} from 'react-router'; - -import logoImage from 'images/logo.png'; - -const holders = defineMessages({ - storageError: { - id: 'team_signup_welcome.storageError', - defaultMessage: 'This service requires local storage to be enabled. Please enable it or exit private browsing.' - }, - validEmailError: { - id: 'team_signup_welcome.validEmailError', - defaultMessage: 'Please enter a valid email address' - }, - address: { - id: 'team_signup_welcome.address', - defaultMessage: 'Email Address' - } -}); - -import React from 'react'; - -class TeamSignupWelcomePage extends React.Component { - constructor(props) { - super(props); - - this.submitNext = this.submitNext.bind(this); - this.handleDiffEmail = this.handleDiffEmail.bind(this); - this.handleDiffSubmit = this.handleDiffSubmit.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - - this.state = {useDiff: false}; - - document.addEventListener('keyup', this.handleKeyPress, false); - } - submitNext(e) { - if (!BrowserStore.isLocalStorageSupported()) { - this.setState({storageError: this.props.intl.formatMessage(holders.storageError)}); - return; - } - e.preventDefault(); - this.props.state.wizard = 'team_display_name'; - this.props.updateParent(this.props.state); - } - handleDiffEmail(e) { - e.preventDefault(); - this.setState({useDiff: true}); - } - handleDiffSubmit(e) { - e.preventDefault(); - - const {formatMessage} = this.props.intl; - var state = {useDiff: true, serverError: ''}; - - var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!email || !Utils.isEmail(email)) { - state.emailError = formatMessage(holders.validEmailError); - this.setState(state); - return; - } else if (!BrowserStore.isLocalStorageSupported()) { - state.emailError = formatMessage(holders.storageError); - this.setState(state); - return; - } - state.emailError = ''; - - Client.signupTeam(email, - function success(data) { - if (data.follow_link) { - browserHistory.push(data.follow_link); - } else { - this.props.state.wizard = 'finished'; - this.props.updateParent(this.props.state); - browserHistory.push('/signup_team_confirm/?email=' + encodeURIComponent(email)); - } - }.bind(this), - function error(err) { - let errorMsg = err.message; - - if (err.detailed_error.indexOf('Invalid RCPT TO address provided') >= 0) { - errorMsg = formatMessage(holders.validEmailError); - } - - this.setState({emailError: '', serverError: errorMsg}); - }.bind(this) - ); - } - handleKeyPress(event) { - if (event.keyCode === 13) { - if (this.state.useDiff) { - this.handleDiffSubmit(event); - } else { - this.submitNext(event); - } - } - } - componentWillUnmount() { - document.removeEventListener('keyup', this.handleKeyPress, false); - } - render() { - Client.track('signup', 'signup_team_01_welcome'); - - var storageError = null; - if (this.state.storageError) { - storageError = ; - } - - var emailError = null; - var emailDivClass = 'form-group'; - if (this.state.emailError) { - emailError = ; - emailDivClass += ' has-error'; - } - - var serverError = null; - if (this.state.serverError) { - serverError = ( -
    - -
    - ); - } - - var differentEmailLinkClass = ''; - var emailDivContainerClass = 'hidden'; - if (this.state.useDiff) { - differentEmailLinkClass = 'hidden'; - emailDivContainerClass = ''; - } - - return ( -
    - -

    - -

    -

    {global.window.mm_config.SiteName}

    -

    - -

    -
    - -
    -
    -
    {this.props.state.team.email}
    -
    -
    -

    - -

    -
    - - {storageError} -
    -
    -
    -
    -
    -
    - -
    -
    - {emailError} -
    - {serverError} - -
    - - - -
    - ); - } -} - -TeamSignupWelcomePage.defaultProps = { - state: {} -}; -TeamSignupWelcomePage.propTypes = { - intl: intlShape.isRequired, - updateParent: React.PropTypes.func.isRequired, - state: React.PropTypes.object -}; - -export default injectIntl(TeamSignupWelcomePage); diff --git a/webapp/components/signup_team_confirm.jsx b/webapp/components/signup_team_confirm.jsx deleted file mode 100644 index 117a0b068..000000000 --- a/webapp/components/signup_team_confirm.jsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {Link} from 'react-router'; - -import React from 'react'; - -export default class SignupTeamConfirm extends React.Component { - render() { - return ( -
    -
    - - - - -
    -
    -
    -

    - -

    -

    - -

    -
    -
    -
    - ); - } -} - -SignupTeamConfirm.defaultProps = { -}; -SignupTeamConfirm.propTypes = { - location: React.PropTypes.object -}; diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx index 9e821289b..666e72e13 100644 --- a/webapp/components/signup_user_complete.jsx +++ b/webapp/components/signup_user_complete.jsx @@ -3,12 +3,13 @@ import LoadingScreen from 'components/loading_screen.jsx'; import LoginLdap from 'components/login/components/login_ldap.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import Constants from 'utils/constants.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; @@ -24,7 +25,6 @@ class SignupUserComplete extends React.Component { super(props); this.handleSubmit = this.handleSubmit.bind(this); - this.inviteInfoRecieved = this.inviteInfoRecieved.bind(this); this.handleLdapSignup = this.handleLdapSignup.bind(this); this.state = { @@ -34,7 +34,10 @@ class SignupUserComplete extends React.Component { email: '', teamDisplayName: '', teamName: '', - teamId: '' + teamId: '', + openServer: false, + loading: true, + inviteId: '' }; } componentWillMount() { @@ -46,19 +49,91 @@ class SignupUserComplete extends React.Component { let teamDisplayName = ''; let teamName = ''; let teamId = ''; + let openServer = false; + let loading = true; + + if ((inviteId && inviteId.length > 0) || (hash && hash.length > 0)) { + // if we are already logged in then attempt to just join the team + if (UserStore.getCurrentUser()) { + loading = true; + Client.addUserToTeamFromInvite( + data, + hash, + inviteId, + () => { + GlobalActions.emitInitialLoad( + () => { + browserHistory.push('/select_team'); + } + ); + }, + (err) => { + this.setState({ + noOpenServerError: true, + serverError: err.message, + loading: false + }); + } + ); + } else if (hash) { + // If we have a hash in the url then we are attempting to access a private team + const parsedData = JSON.parse(data); + usedBefore = BrowserStore.getGlobalItem(hash); + email = parsedData.email; + teamDisplayName = parsedData.display_name; + teamName = parsedData.name; + teamId = parsedData.id; + loading = false; + } else { + loading = true; + Client.getInviteInfo( + inviteId, + (inviteData) => { + if (!inviteData) { + return; + } - // If we have a hash in the url then we are attempting to access a private team - if (hash) { - const parsedData = JSON.parse(data); - usedBefore = BrowserStore.getGlobalItem(hash); - email = parsedData.email; - teamDisplayName = parsedData.display_name; - teamName = parsedData.name; - teamId = parsedData.id; + this.setState({ + serverError: null, + teamDisplayName: inviteData.display_name, + teamName: inviteData.name, + teamId: inviteData.id, + loading: false + }); + }, + () => { + this.setState({ + noOpenServerError: true, + loading: false, + serverError: + + }); + } + ); + + data = ''; + hash = ''; + } + } else if (global.window.mm_config.EnableOpenServer === 'true' || UserStore.getNoAccounts()) { + // If this is the first account then let them create an account anyway. + // The server will verify it's the first account before allowing creation. + // Of if the server is open then we don't care. + openServer = true; + loading = false; } else { - Client.getInviteInfo(this.inviteInfoRecieved, null, inviteId); - data = ''; - hash = ''; + loading = false; + this.setState({ + noOpenServerError: true, + serverError: + , + loading: false + }); } this.setState({ @@ -68,29 +143,25 @@ class SignupUserComplete extends React.Component { email, teamDisplayName, teamName, - teamId - }); - } - inviteInfoRecieved(data) { - if (!data) { - return; - } - - this.setState({ - teamDisplayName: data.display_name, - teamName: data.name, - teamId: data.id + teamId, + openServer, + inviteId, + loading }); } handleLdapSignup(method, loginId, password, token) { - Client.loginByLdap(this.state.teamName, loginId, password, token, + Client.loginByLdap(loginId, password, token, () => { const redirect = Utils.getUrlParameter('redirect'); if (redirect) { browserHistory.push(decodeURIComponent(redirect)); } else { - browserHistory.push('/' + this.state.teamName + '/channels/town-square'); + GlobalActions.emitInitialLoad( + () => { + browserHistory.push('/select_team'); + } + ); } }, (err) => { @@ -187,28 +258,34 @@ class SignupUserComplete extends React.Component { }); const user = { - team_id: this.state.teamId, email: providedEmail, username: providedUsername, password: providedPassword, allow_marketing: true }; - Client.createUser(user, this.state.data, this.state.hash, + Client.createUserWithInvite(user, + this.state.data, + this.state.hash, + this.state.inviteId, () => { Client.track('signup', 'signup_user_02_complete'); - - Client.loginByEmail( - this.state.teamName, + Client.login( user.email, + null, user.password, - '', // No MFA Token + '', () => { UserStore.setLastEmail(user.email); if (this.state.hash > 0) { BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); } - browserHistory.push('/' + this.state.teamName + '/channels/town-square'); + + GlobalActions.emitInitialLoad( + () => { + browserHistory.push('/select_team'); + } + ); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { @@ -239,9 +316,7 @@ class SignupUserComplete extends React.Component { ); } - // If we haven't got a team id yet we are waiting for - // the client so just show the standard loading screen - if (this.state.teamId === '') { + if (this.state.loading) { return (); } @@ -349,7 +424,7 @@ class SignupUserComplete extends React.Component { @@ -367,7 +442,7 @@ class SignupUserComplete extends React.Component { @@ -497,12 +572,33 @@ class SignupUserComplete extends React.Component { ); } + let terms = ( +

    + +

    + ); + + if (this.state.noOpenServerError) { + signupMessage = null; + ldapSignup = null; + emailSignup = null; + terms = null; + } + return (
    - - + +
    @@ -511,22 +607,12 @@ class SignupUserComplete extends React.Component { className='signup-team-logo' src={logoImage} /> -
    - -
    -

    {this.state.teamName}

    -

    +

    {global.window.mm_config.SiteName}

    +

    -

    +

    diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx index 90ec6e660..79ac8aaf1 100644 --- a/webapp/components/suggestion/at_mention_provider.jsx +++ b/webapp/components/suggestion/at_mention_provider.jsx @@ -4,6 +4,7 @@ import SuggestionStore from 'stores/suggestion_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import Client from 'utils/web_client.jsx'; import {FormattedMessage} from 'react-intl'; @@ -42,7 +43,7 @@ class AtMentionSuggestion extends React.Component { icon = (
    {item.username}
    diff --git a/webapp/components/team_export_tab.jsx b/webapp/components/team_export_tab.jsx index 9bd5785a0..37f886aab 100644 --- a/webapp/components/team_export_tab.jsx +++ b/webapp/components/team_export_tab.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import {FormattedMessage} from 'react-intl'; diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx index c27e8ca59..1f783fe9f 100644 --- a/webapp/components/team_general_tab.jsx +++ b/webapp/components/team_general_tab.jsx @@ -5,7 +5,7 @@ import $ from 'jquery'; import SettingItemMin from './setting_item_min.jsx'; import SettingItemMax from './setting_item_max.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -42,7 +42,7 @@ const holders = defineMessages({ }, openInviteTitle: { id: 'general_tab.openInviteTitle', - defaultMessage: 'Allow anyone to sign-up from login page' + defaultMessage: 'Allow anyone to join this team' }, codeTitle: { id: 'general_tab.codeTitle', @@ -68,7 +68,6 @@ class GeneralTab extends React.Component { this.handleNameSubmit = this.handleNameSubmit.bind(this); this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this); this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this); - this.handleTeamListingSubmit = this.handleTeamListingSubmit.bind(this); this.handleClose = this.handleClose.bind(this); this.onUpdateNameSection = this.onUpdateNameSection.bind(this); this.updateName = this.updateName.bind(this); @@ -76,8 +75,6 @@ class GeneralTab extends React.Component { this.updateInviteId = this.updateInviteId.bind(this); this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this); this.handleOpenInviteRadio = this.handleOpenInviteRadio.bind(this); - this.onUpdateTeamListingSection = this.onUpdateTeamListingSection.bind(this); - this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this); this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this); this.state = this.setupInitialState(props); @@ -96,12 +93,19 @@ class GeneralTab extends React.Component { name: team.display_name, invite_id: team.invite_id, allow_open_invite: team.allow_open_invite, - allow_team_listing: team.allow_team_listing, serverError: '', clientError: '' }; } + componentWillReceiveProps(nextProps) { + this.setState({ + name: nextProps.team.display_name, + invite_id: nextProps.team.invite_id, + allow_open_invite: nextProps.team.allow_open_invite + }); + } + handleGenerateInviteId(e) { e.preventDefault(); @@ -117,14 +121,6 @@ class GeneralTab extends React.Component { this.setState({allow_open_invite: openInvite}); } - handleTeamListingRadio(listing) { - if (global.window.mm_config.EnableTeamListing !== 'true' && listing) { - this.setState({clientError: this.props.intl.formatMessage(holders.dirDisabled)}); - } else { - this.setState({allow_team_listing: listing}); - } - } - handleOpenInviteSubmit(e) { e.preventDefault(); @@ -145,26 +141,6 @@ class GeneralTab extends React.Component { ); } - handleTeamListingSubmit(e) { - e.preventDefault(); - - var state = {serverError: '', clientError: ''}; - - var data = this.props.team; - data.allow_team_listing = this.state.allow_team_listing; - Client.updateTeam(data, - (team) => { - TeamStore.saveTeam(team); - TeamStore.emitChange(); - this.updateSection(''); - }, - (err) => { - state.serverError = err.message; - this.setState(state); - } - ); - } - handleNameSubmit(e) { e.preventDefault(); @@ -239,12 +215,6 @@ class GeneralTab extends React.Component { ); } - componentWillReceiveProps(newProps) { - if (newProps.team && newProps.teamDisplayName) { - this.setState({name: newProps.teamDisplayName}); - } - } - handleClose() { this.updateSection(''); } @@ -284,15 +254,6 @@ class GeneralTab extends React.Component { } } - onUpdateTeamListingSection(e) { - e.preventDefault(); - if (this.props.activeSection === 'team_listing') { - this.updateSection(''); - } else { - this.updateSection('team_listing'); - } - } - updateName(e) { e.preventDefault(); this.setState({name: e.target.value}); @@ -313,105 +274,8 @@ class GeneralTab extends React.Component { serverError = this.state.serverError; } - const enableTeamListing = global.window.mm_config.EnableTeamListing === 'true'; const {formatMessage} = this.props.intl; - let teamListingSection; - if (this.props.activeSection === 'team_listing') { - const inputs = []; - let submitHandle = null; - - if (enableTeamListing) { - submitHandle = this.handleTeamListingSubmit; - - inputs.push( -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - ); - } else { - inputs.push( -
    -
    -
    - -
    -
    - ); - } - - teamListingSection = ( - - ); - } else { - let describe = ''; - - if (enableTeamListing) { - if (this.state.allow_team_listing === true) { - describe = formatMessage(holders.yes); - } else { - describe = formatMessage(holders.no); - } - } else { - describe = formatMessage(holders.dirOff); - } - - teamListingSection = ( - - ); - } - let openInviteSection; if (this.props.activeSection === 'open_invite') { const inputs = [ @@ -450,7 +314,7 @@ class GeneralTab extends React.Component {
    @@ -639,8 +503,6 @@ class GeneralTab extends React.Component {
    {openInviteSection}
    - {teamListingSection} -
    {inviteSection}
    diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx index ad82a2280..251c2ce3b 100644 --- a/webapp/components/team_members_dropdown.jsx +++ b/webapp/components/team_members_dropdown.jsx @@ -3,7 +3,7 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; import ConfirmModal from './confirm_modal.jsx'; @@ -38,11 +38,9 @@ export default class TeamMembersDropdown extends React.Component { if (this.props.user.id === me.id) { this.handleDemote(this.props.user, ''); } else { - const data = { - user_id: this.props.user.id, - new_roles: '' - }; - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + '', () => { AsyncClient.getProfiles(); }, @@ -79,12 +77,9 @@ export default class TeamMembersDropdown extends React.Component { if (this.props.user.id === me.id) { this.handleDemote(this.props.user, 'admin'); } else { - const data = { - user_id: this.props.user.id, - new_roles: 'admin' - }; - - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + 'admin', () => { AsyncClient.getProfiles(); }, @@ -94,12 +89,13 @@ export default class TeamMembersDropdown extends React.Component { ); } } - handleDemote(user, role) { + handleDemote(user, role, newRole) { this.setState({ serverError: this.state.serverError, showDemoteModal: true, user, - role + role, + newRole }); } handleDemoteCancel() { @@ -107,16 +103,14 @@ export default class TeamMembersDropdown extends React.Component { serverError: null, showDemoteModal: false, user: null, - role: null + role: null, + newRole: null }); } handleDemoteSubmit() { - const data = { - user_id: this.props.user.id, - new_roles: this.state.role - }; - - Client.updateRoles(data, + Client.updateRoles( + this.props.user.id, + this.state.newRole, () => { const teamUrl = TeamStore.getCurrentTeamUrl(); if (teamUrl) { @@ -140,6 +134,7 @@ export default class TeamMembersDropdown extends React.Component { ); } + const teamMember = this.props.teamMember; const user = this.props.user; let currentRoles = ( - - - - -
    - ); - } - - if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { - buttons.push( - { - e.preventDefault(); - this.props.updatePage('google'); - } - } - > - - - - - - ); - } - - if (global.window.mm_config.EnableLdap === 'true') { - buttons.push( - { - e.preventDefault(); - this.props.updatePage('ldap'); - } - } - > - - - - - - ); - } - - if (global.window.mm_config.EnableSignUpWithEmail === 'true') { - buttons.push( - - - - - - - ); - } - - if (buttons.length === 0) { - buttons = ( - - - - ); - } - - return ( -
    - {buttons} -
    - ); - } -} - -ChooseAuthPage.propTypes = { - updatePage: React.PropTypes.func -}; diff --git a/webapp/components/team_signup_with_email.jsx b/webapp/components/team_signup_with_email.jsx deleted file mode 100644 index 90a6e9773..000000000 --- a/webapp/components/team_signup_with_email.jsx +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; - -const holders = defineMessages({ - emailError: { - id: 'email_signup.emailError', - defaultMessage: 'Please enter a valid email address' - }, - address: { - id: 'email_signup.address', - defaultMessage: 'Email Address' - } -}); - -import React from 'react'; - -class EmailSignUpPage extends React.Component { - constructor() { - super(); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.state = {}; - } - handleSubmit(e) { - e.preventDefault(); - const team = {}; - const state = {serverError: null}; - let isValid = true; - - team.email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!team.email || !Utils.isEmail(team.email)) { - state.emailError = this.props.intl.formatMessage(holders.emailError); - isValid = false; - } else { - state.emailError = null; - } - - if (!isValid) { - this.setState(state); - return; - } - - Client.signupTeam(team.email, - (data) => { - if (data.follow_link) { - browserHistory.push(data.follow_link); - } else { - browserHistory.push(`/signup_team_confirm/?email=${encodeURIComponent(team.email)}`); - } - }, - (err) => { - state.serverError = err.message; - this.setState(state); - } - ); - } - render() { - let serverError = null; - if (this.state.serverError) { - serverError =
    ; - } - - let emailError = null; - if (this.state.emailError) { - emailError =
    ; - } - - return ( -
    -
    - - {emailError} -
    -
    - - {serverError} -
    -
    - ); - } -} - -EmailSignUpPage.defaultProps = { -}; -EmailSignUpPage.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(EmailSignUpPage); diff --git a/webapp/components/team_signup_with_ldap.jsx b/webapp/components/team_signup_with_ldap.jsx deleted file mode 100644 index 9d812e8ee..000000000 --- a/webapp/components/team_signup_with_ldap.jsx +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import * as utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; - -const holders = defineMessages({ - team_error: { - id: 'ldap_signup.team_error', - defaultMessage: 'Please enter a team name' - }, - length_error: { - id: 'ldap_signup.length_error', - defaultMessage: 'Name must be 3 or more characters up to a maximum of 15' - }, - teamName: { - id: 'ldap_signup.teamName', - defaultMessage: 'Enter name of new team' - }, - badTeam: { - id: 'login_ldap.badTeam', - defaultMessage: 'Bad team name' - }, - idReq: { - id: 'login_ldap.idlReq', - defaultMessage: 'An LDAP ID is required' - }, - pwdReq: { - id: 'login_ldap.pwdReq', - defaultMessage: 'An LDAP password is required' - }, - username: { - id: 'login_ldap.username', - defaultMessage: 'LDAP Username' - }, - pwd: { - id: 'login_ldap.pwd', - defaultMessage: 'LDAP Password' - } -}); - -import React from 'react'; - -class LdapSignUpPage extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - this.valueChange = this.valueChange.bind(this); - - this.state = {name: '', id: '', password: ''}; - } - - handleSubmit(e) { - e.preventDefault(); - const {formatMessage} = this.props.intl; - var teamSignup = {}; - teamSignup.user = {}; - teamSignup.team = {}; - var state = this.state; - state.serverError = null; - - teamSignup.team.display_name = this.state.name; - - if (!teamSignup.team.display_name) { - state.serverError = formatMessage(holders.team_error); - this.setState(state); - return; - } - - if (teamSignup.team.display_name.length <= 2) { - state.serverError = formatMessage(holders.length_error); - this.setState(state); - return; - } - - const id = this.refs.id.value.trim(); - if (!id) { - state.serverError = formatMessage(holders.idReq); - this.setState(state); - return; - } - - const password = this.refs.password.value.trim(); - if (!password) { - state.serverError = formatMessage(holders.pwdReq); - this.setState(state); - return; - } - - state.serverError = ''; - this.setState(state); - - teamSignup.team.name = utils.cleanUpUrlable(teamSignup.team.display_name); - teamSignup.team.type = 'O'; - - teamSignup.user.username = ReactDOM.findDOMNode(this.refs.id).value.trim(); - teamSignup.user.password = ReactDOM.findDOMNode(this.refs.password).value.trim(); - teamSignup.user.allow_marketing = true; - teamSignup.user.ldap = true; - teamSignup.user.auth_service = 'ldap'; - - $('#ldap-button').button('loading'); - - Client.createTeamWithLdap(teamSignup, - () => { - Client.track('signup', 'signup_team_ldap_complete'); - - Client.loginByLdap(teamSignup.team.name, id, password, - () => { - browserHistory.push('/' + teamSignup.team.name + '/channels/town-square'); - }, - (err) => { - $('#ldap-button').button('reset'); - this.setState({serverError: err.message}); - } - ); - }, - (err) => { - $('#ldap-button').button('reset'); - this.setState({serverError: err.message}); - } - ); - } - - valueChange() { - this.setState({ - name: ReactDOM.findDOMNode(this.refs.teamname).value.trim(), - id: ReactDOM.findDOMNode(this.refs.id).value.trim(), - password: ReactDOM.findDOMNode(this.refs.password).value.trim() - }); - } - - render() { - const {formatMessage} = this.props.intl; - var nameError = null; - var nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = ; - nameDivClass += ' has-error'; - } - - var serverError = null; - if (this.state.serverError) { - serverError =
    ; - } - - var disabled = false; - if (this.state.name.length <= 2) { - disabled = true; - } - - if (this.state.id.length <= 1) { - disabled = true; - } - - if (this.state.password.length <= 1) { - disabled = true; - } - - return ( -
    -
    - - {nameError} -
    -
    - -
    -
    - -
    -
    - - - - - - - {serverError} -
    -
    - ); - } -} - -LdapSignUpPage.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(LdapSignUpPage); diff --git a/webapp/components/team_signup_with_sso.jsx b/webapp/components/team_signup_with_sso.jsx deleted file mode 100644 index 78396eea8..000000000 --- a/webapp/components/team_signup_with_sso.jsx +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import * as utils from 'utils/utils.jsx'; -import * as client from 'utils/client.jsx'; -import Constants from 'utils/constants.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; - -const holders = defineMessages({ - team_error: { - id: 'sso_signup.team_error', - defaultMessage: 'Please enter a team name' - }, - length_error: { - id: 'sso_signup.length_error', - defaultMessage: 'Name must be 3 or more characters up to a maximum of 15' - }, - teamName: { - id: 'sso_signup.teamName', - defaultMessage: 'Enter name of new team' - } -}); - -import React from 'react'; -import {browserHistory} from 'react-router'; - -class SSOSignUpPage extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - this.nameChange = this.nameChange.bind(this); - - this.state = {name: ''}; - } - handleSubmit(e) { - e.preventDefault(); - const {formatMessage} = this.props.intl; - var team = {}; - var state = this.state; - state.nameError = null; - state.serverError = null; - - team.display_name = this.state.name; - - if (!team.display_name) { - state.nameError = formatMessage(holders.team_error); - this.setState(state); - return; - } - - if (team.display_name.length <= 2) { - state.nameError = formatMessage(holders.length_error); - this.setState(state); - return; - } - - team.name = utils.cleanUpUrlable(team.display_name); - team.type = 'O'; - - client.createTeamWithSSO(team, - this.props.service, - (data) => { - if (data.follow_link) { - window.location.href = data.follow_link; - } else { - browserHistory.push('/' + team.name + '/channels/town-square'); - } - }, - (err) => { - state.serverError = err.message; - this.setState(state); - } - ); - } - nameChange() { - this.setState({name: ReactDOM.findDOMNode(this.refs.teamname).value.trim()}); - } - render() { - var nameError = null; - var nameDivClass = 'form-group'; - if (this.state.nameError) { - nameError = ; - nameDivClass += ' has-error'; - } - - var serverError = null; - if (this.state.serverError) { - serverError =
    ; - } - - var disabled = false; - if (this.state.name.length <= 2) { - disabled = true; - } - - var button = null; - - if (this.props.service === Constants.GITLAB_SERVICE) { - button = ( - - - - - - - ); - } else if (this.props.service === Constants.GOOGLE_SERVICE) { - button = ( - - - - - - - ); - } - - return ( -
    -
    - - {nameError} -
    -
    - {button} - {serverError} -
    -
    - ); - } -} - -SSOSignUpPage.defaultProps = { - service: '' -}; -SSOSignUpPage.propTypes = { - intl: intlShape.isRequired, - service: React.PropTypes.string -}; - -export default injectIntl(SSOSignUpPage); diff --git a/webapp/components/toggle_modal_button.jsx b/webapp/components/toggle_modal_button.jsx index de225493c..69bdbbda0 100644 --- a/webapp/components/toggle_modal_button.jsx +++ b/webapp/components/toggle_modal_button.jsx @@ -15,7 +15,8 @@ export default class ModalToggleButton extends React.Component { }; } - show() { + show(e) { + e.preventDefault(); this.setState({show: true}); } @@ -29,10 +30,10 @@ export default class ModalToggleButton extends React.Component { // allow callers to provide an onClick which will be called before the modal is shown let clickHandler = this.show; if (onClick) { - clickHandler = () => { + clickHandler = (e) => { onClick(); - this.show(); + this.show(e); }; } diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx index 3652723be..626cb3cf5 100644 --- a/webapp/components/user_list.jsx +++ b/webapp/components/user_list.jsx @@ -13,10 +13,18 @@ export default class UserList extends React.Component { let content; if (users.length > 0) { content = users.map((user) => { + var teamMember; + for (var index in this.props.teamMembers) { + if (this.props.teamMembers[index].user_id === user.id) { + teamMember = this.props.teamMembers[index]; + } + } + return ( @@ -48,12 +56,14 @@ export default class UserList extends React.Component { UserList.defaultProps = { users: [], + teamMembers: [], actions: [], actionProps: {} }; UserList.propTypes = { users: React.PropTypes.arrayOf(React.PropTypes.object), + teamMembers: React.PropTypes.arrayOf(React.PropTypes.object), actions: React.PropTypes.arrayOf(React.PropTypes.func), actionProps: React.PropTypes.object }; diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx index f6fd91688..a7838ec32 100644 --- a/webapp/components/user_list_row.jsx +++ b/webapp/components/user_list_row.jsx @@ -4,9 +4,10 @@ import Constants from 'utils/constants.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; -export default function UserListRow({user, actions, actionProps}) { +export default function UserListRow({user, teamMember, actions, actionProps}) { const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', ''); let name = user.username; @@ -21,6 +22,7 @@ export default function UserListRow({user, actions, actionProps}) { ); @@ -35,7 +37,7 @@ export default function UserListRow({user, actions, actionProps}) { className='more-modal__image' width='38' height='38' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`} />
    { this.submitActive = false; AsyncClient.getMe(); @@ -781,7 +779,7 @@ class UserSettingsGeneralTab extends React.Component { { this.props.updateSection(''); AsyncClient.getMe(); @@ -120,11 +118,9 @@ class SecurityTab extends React.Component { ); } activateMfa() { - const data = {}; - data.activate = true; - data.token = this.state.mfaToken; - - Client.updateMfa(data, + Client.updateMfa( + this.state.mfaToken, + true, () => { this.props.updateSection(''); AsyncClient.getMe(); @@ -224,7 +220,7 @@ class SecurityTab extends React.Component {
    ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + + ); } return ( diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx index 14991037d..f19538f71 100644 --- a/webapp/components/user_settings/user_settings_theme.jsx +++ b/webapp/components/user_settings/user_settings_theme.jsx @@ -11,7 +11,7 @@ import SettingItemMax from '../setting_item_max.jsx'; import UserStore from 'stores/user_store.jsx'; import AppDispatcher from '../../dispatcher/app_dispatcher.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx index 3d3107d92..bd4aeaa41 100644 --- a/webapp/components/view_image.jsx +++ b/webapp/components/view_image.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; +import Client from 'utils/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; import AudioVideoPreview from './audio_video_preview.jsx'; import Constants from 'utils/constants.jsx'; diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index da69aba74..6d4f4c287 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -271,6 +271,9 @@ "admin.ldap.lastnameAttrDesc": "The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.", "admin.ldap.lastnameAttrEx": "Ex \"sn\"", "admin.ldap.lastnameAttrTitle": "Last Name Attribute:", + "admin.ldap.nicknameAttrDesc": "(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.", + "admin.ldap.nicknameAttrEx": "Ex \"nickname\"", + "admin.ldap.nicknameAttrTitle": "Nickname Attribute:", "admin.ldap.noLicense": "

    Note:

    LDAP is an enterprise feature. Your current license does not support LDAP. Click here for information and pricing on enterprise licenses.

    ", "admin.ldap.portDesc": "The port Mattermost will use to connect to the LDAP server. Default is 389.", "admin.ldap.portEx": "Ex \"389\"", @@ -526,10 +529,18 @@ "admin.team.uploading": "Uploading..", "admin.team.userCreationDescription": "When false, the ability to create accounts is disabled. The create account button displays error when pressed.", "admin.team.userCreationTitle": "Enable User Creation: ", + "admin.team.openServerDescription": "When true, anyone can signup for a user account on this server without the need to be invited.", + "admin.team.openServerTitle": "Enable Open Server: ", "admin.team_analytics.activeUsers": "Active Users With Posts", "admin.team_analytics.totalPosts": "Total Posts", "admin.userList.title": "Users for {team}", "admin.userList.title2": "Users for {team} ({count})", + "admin.user_item.resetMfa": "Remove MFA", + "admin.user_item.mfaYes": ", MFA: Yes", + "admin.user_item.mfaNo": ", MFA: No", + "admin.user_item.authServiceNotEmail": ", Sign-in Method: {service}", + "admin.user_item.authServiceEmail": ", Sign-in Method: Email", + "admin.user_item.emailTitle": "Email: {email}", "admin.user_item.confirmDemoteDescription": "If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.", "admin.user_item.confirmDemoteRoleTitle": "Confirm demotion from System Admin role", "admin.user_item.confirmDemotion": "Confirm Demotion", @@ -721,7 +732,7 @@ "choose_auth_page.noSignup": "No sign-up methods configured, please contact your system administrator.", "claim.account.noEmail": "No email specified", "claim.email_to_ldap.enterLdapPwd": "Enter the ID and password for your LDAP account", - "claim.email_to_ldap.enterPwd": "Enter the password for your {team} {site} email account", + "claim.email_to_ldap.enterPwd": "Enter the password for your {site} email account", "claim.email_to_ldap.ldapId": "LDAP ID", "claim.email_to_ldap.ldapIdError": "Please enter your LDAP ID.", "claim.email_to_ldap.ldapPasswordError": "Please enter your LDAP password.", @@ -732,7 +743,7 @@ "claim.email_to_ldap.ssoType": "Upon claiming your account, you will only be able to login with LDAP", "claim.email_to_ldap.switchTo": "Switch account to LDAP", "claim.email_to_ldap.title": "Switch Email/Password Account to LDAP", - "claim.email_to_oauth.enterPwd": "Enter the password for your {team} {site} account", + "claim.email_to_oauth.enterPwd": "Enter the password for your {site} account", "claim.email_to_oauth.pwd": "Password", "claim.email_to_oauth.pwdError": "Please enter your password.", "claim.email_to_oauth.ssoNote": "You must already have a valid {type} account", @@ -741,7 +752,7 @@ "claim.email_to_oauth.title": "Switch Email/Password Account to {uiType}", "claim.ldap_to_email.confirm": "Confirm Password", "claim.ldap_to_email.email": "You will use the email {email} to login", - "claim.ldap_to_email.enterLdapPwd": "Enter your LDAP password for your {team} {site} email account", + "claim.ldap_to_email.enterLdapPwd": "Enter your {ldapPassword} for your {site} email account", "claim.ldap_to_email.enterPwd": "Enter a new password for your email account", "claim.ldap_to_email.ldapPasswordError": "Please enter your LDAP password.", "claim.ldap_to_email.ldapPwd": "LDAP Password", @@ -758,7 +769,7 @@ "claim.oauth_to_email.pwdNotMatch": "Password do not match.", "claim.oauth_to_email.switchTo": "Switch {type} to email and password", "claim.oauth_to_email.title": "Switch {type} Account to Email", - "claim.oauth_to_email_newPwd": "Enter a new password for your {team} {site} account", + "claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} account", "confirm_modal.cancel": "Cancel", "create_comment.addComment": "Add a comment...", "create_comment.comment": "Add Comment", @@ -841,8 +852,8 @@ "general_tab.includeDirDesc": "Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.", "general_tab.includeDirTitle": "Include this team in the Team Directory", "general_tab.no": "No", - "general_tab.openInviteDesc": "When allowed, a link to account creation will be included on the sign-in page of this team and allow any visitor to sign-up.", - "general_tab.openInviteTitle": "Allow anyone to sign-up from login page", + "general_tab.openInviteDesc": "When allowed, a link to this team will be including on the landing page allowing anyone with an account to join this team.", + "general_tab.openInviteTitle": "Allow anyone to join this team", "general_tab.regenerate": "Re-Generate", "general_tab.required": "This field is required", "general_tab.teamName": "Team Name", @@ -866,6 +877,7 @@ "installed_integrations.regenToken": "Regen Token", "installed_integrations.search": "Search Integrations", "installed_integrations.token": "Token: {token}", + "installed_integrations.url": "URL: {url}", "installed_outgoing_webhooks.add": "Add Outgoing Webhook", "installed_outgoing_webhooks.header": "Outgoing Webhooks", "integrations.command.description": "Slash commands send events to external integrations", @@ -913,6 +925,7 @@ "ldap_signup.team_error": "Please enter a team name", "loading_screen.loading": "Loading", "login.changed": " Sign-in method changed successfully", + "login.passwordChanged": " Password updated successfully", "login.create": "Create one now", "login.createTeam": "Create a new team", "login.find": "Find your other teams", @@ -1111,7 +1124,9 @@ "sidebar_right_menu.report": "Report a Problem", "sidebar_right_menu.teamLink": "Get Team Invite Link", "sidebar_right_menu.teamSettings": "Team Settings", - "signup_team.choose": "Choose a Team", + "signup_team.no_teams": "You do not appear to be a member of any team. Please ask your administrator for an invite, join an open team if one exists or possibly create a new team.", + "signup_team.choose": "Teams you are a member of: ", + "signup_team.join_open": "Open teams you can join: ", "signup_team.createTeam": "Or Create a Team", "signup_team.disabled": "Team creation has been disabled. Please contact an administrator for access.", "signup_team.noTeams": "There are no teams included in the Team Directory and team creation has been disabled.", @@ -1119,6 +1134,8 @@ "signup_team_complete.completed": "You've already completed the signup process for this invitation or this invitation has expired.", "signup_team_confirm.checkEmail": "Please check your email: {email}
    Your email contains a link to set up your team", "signup_team_confirm.title": "Sign up Complete", + "signup_user_completed.no_open_server": "This server does not allow open signups. Please speak with your Administrator to receive an invitation.", + "signup_user_completed.invalid_invite": "The invite link was invalid. Please speak with your Administrator to receive an invitation.", "signup_user_completed.choosePwd": "Choose your password", "signup_user_completed.chooseUser": "Choose your username", "signup_user_completed.create": "Create Account", @@ -1181,61 +1198,23 @@ "team_settings_modal.generalTab": "General", "team_settings_modal.importTab": "Import", "team_settings_modal.title": "Team Settings", - "team_signup_display_name.back": "Back to previous step", - "team_signup_display_name.charLength": "Name must be 4 or more characters up to a maximum of 15", - "team_signup_display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.", - "team_signup_display_name.next": "Next", - "team_signup_display_name.required": "This field is required", - "team_signup_display_name.teamName": "Team Name", - "team_signup_email.address": "Email Address", - "team_signup_email.different": "Please use a different email than the one used at signup", - "team_signup_email.validEmail": "Please enter a valid email address", - "team_signup_password.agreement": "By proceeding to create your account and use {siteName}, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {siteName}.", - "team_signup_password.back": "Back to previous step", - "team_signup_password.choosePwd": "Choose your password", - "team_signup_password.creating": "Creating team...", - "team_signup_password.email": "Email", - "team_signup_password.finish": "Finish", - "team_signup_password.hint": "Passwords must contain {min} to {max} characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.", - "team_signup_password.passwordError": "Please enter at least {chars} characters", - "team_signup_password.selectPassword": "Select a password that you'll use to login with your email address:", - "team_signup_password.yourPassword": "Your password", - "team_signup_send_invites.addInvitation": "Add Invitation", - "team_signup_send_invites.back": "Back to previous step", - "team_signup_send_invites.disabled": "Email is currently disabled for your team, and emails cannot be sent. Contact your system administrator to enable email and email invitations.", - "team_signup_send_invites.forNow": "for now.", - "team_signup_send_invites.next": "Next", - "team_signup_send_invites.prefer": "if you prefer, you can invite team members later
    and ", - "team_signup_send_invites.skip": "skip this step ", - "team_signup_send_invites.title": "Invite Team Members", - "team_signup_url.back": "Back to previous step", - "team_signup_url.charLength": "Name must be 4 or more characters up to a maximum of 15", - "team_signup_url.hint": "
  • Short and memorable is best
  • Use lowercase letters, numbers and dashes
  • Must start with a letter and can't end in a dash
  • ", - "team_signup_url.next": "Next", - "team_signup_url.regex": "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.", - "team_signup_url.required": "This field is required", - "team_signup_url.taken": "URL is taken or contains a reserved word", - "team_signup_url.teamUrl": "Team URL", - "team_signup_url.unavailable": "This URL is unavailable. Please try another.", - "team_signup_url.webAddress": "Choose the web address of your new team:", - "team_signup_username.back": "Back to previous step", - "team_signup_username.chooseUsername": "Choose your username", - "team_signup_username.hint": "Usernames must begin with a letter and contain between {min} to {max} characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'", - "team_signup_username.invalid": "Username must begin with a letter, and contain between {min} to {max} characters in total, which may be numbers, lowercase letters, or any of the symbols '.', '-', or '_'", - "team_signup_username.memorable": "Select a memorable username that makes it easy for teammates to identify you:", - "team_signup_username.next": "Next", - "team_signup_username.reserved": "This username is reserved, please choose a new one.", - "team_signup_username.username": "Your username", - "team_signup_welcome.address": "Email Address", - "team_signup_welcome.admin": "Your account will administer the new team site.
    You can add other administrators later.", - "team_signup_welcome.confirm": "Please confirm your email address:", - "team_signup_welcome.different": "Use a different email", - "team_signup_welcome.instead": "Use this instead", - "team_signup_welcome.lets": "Let's set up your new team", - "team_signup_welcome.storageError": "This service requires local storage to be enabled. Please enable it or exit private browsing.", - "team_signup_welcome.validEmailError": "Please enter a valid email address", - "team_signup_welcome.welcome": "Welcome to:", - "team_signup_welcome.yes": "Yes, this address is correct", + "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {siteName}.", + "create_team.display_name.teamName": "Team Name", + "create_team.display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.", + "create_team.display_name.next": "Next", + "create_team.display_name.back": "Back to previous step", + "create_team.display_name.required": "This field is required", + "create_team.display_name.charLength": "Name must be 4 or more characters up to a maximum of 15", + "create_team.team_url.required": "This field is required", + "create_team.team_url.regex": "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.", + "create_team.team_url.charLength": "Name must be 4 or more characters up to a maximum of 15", + "create_team.team_url.taken": "URL is taken or contains a reserved word", + "create_team.team_url.unavailable": "This URL is unavailable. Please try another.", + "create_team.team_url.teamUrl": "Team URL", + "create_team.team_url.webAddress": "Choose the web address of your new team:", + "create_team.team_url.hint": "
  • Short and memorable is best
  • Use lowercase letters, numbers and dashes
  • Must start with a letter and can't end in a dash
  • ", + "create_team.team_url.finish": "Finish", + "create_team.team_url.back": "Back to previous step", "textbox.bold": "**bold**", "textbox.edit": "Edit message", "textbox.help": "Help", @@ -1406,6 +1385,7 @@ "user.settings.security.currentPasswordError": "Please enter your current password", "user.settings.security.emailPwd": "Email and Password", "user.settings.security.gitlab": "GitLab SSO", + "user.settings.security.ldap": "LDAP", "user.settings.security.lastUpdated": "Last updated {date} at {time}", "user.settings.security.loginGitlab": "Login done through Gitlab", "user.settings.security.loginLdap": "Login done through LDAP", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 23f1d5a45..7d429b629 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -721,7 +721,7 @@ "choose_auth_page.noSignup": "No hay métodos de inicio de sesión configurad, por favor contacte al administrador de sistemasos", "claim.account.noEmail": "No se especifico un correo electrónico.", "claim.email_to_ldap.enterLdapPwd": "Ingresa el ID y la contraseña de tu cuenta LDAP", - "claim.email_to_ldap.enterPwd": "Ingresa la contraseña para tu cuenta de correo en {team} {site}", + "claim.email_to_ldap.enterPwd": "Ingresa la contraseña para tu cuenta de correo en {site}", "claim.email_to_ldap.ldapId": "LDAP ID", "claim.email_to_ldap.ldapIdError": "Por favor ingresa tu ID de LDAP.", "claim.email_to_ldap.ldapPasswordError": "Por favor ingresa tu contraseña de LDAP.", @@ -732,7 +732,7 @@ "claim.email_to_ldap.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con LDAP", "claim.email_to_ldap.switchTo": "Cambiar cuenta a LDAP", "claim.email_to_ldap.title": "Cambiar Cuenta de Correo/Contraseña a LDAP", - "claim.email_to_oauth.enterPwd": "Ingresa la contraseña para tu cuenta para {team} {site}", + "claim.email_to_oauth.enterPwd": "Ingresa la contraseña para tu cuenta para {site}", "claim.email_to_oauth.pwd": "Contraseña", "claim.email_to_oauth.pwdError": "Por favor introduce tu contraseña.", "claim.email_to_oauth.ssoNote": "Debes tener una cuenta válida con {type}", @@ -741,7 +741,6 @@ "claim.email_to_oauth.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", "claim.ldap_to_email.confirm": "Confirmar Contraseña", "claim.ldap_to_email.email": "Para iniciar sesión debes utilizar el correo electrónico {email}", - "claim.ldap_to_email.enterLdapPwd": "Ingresa tu contraseña de LDAP para tu cuenta de correo en {team} {site}", "claim.ldap_to_email.enterPwd": "Ingresa una nueva contraseña para tu cuenta de correo", "claim.ldap_to_email.ldapPasswordError": "Por favor ingresa tu contraseña LDAP.", "claim.ldap_to_email.ldapPwd": "Contraseña LDAP", @@ -758,7 +757,6 @@ "claim.oauth_to_email.pwdNotMatch": "Las contraseñas no coinciden.", "claim.oauth_to_email.switchTo": "Cambiar {type} a correo electrónico y contraseña", "claim.oauth_to_email.title": "Cambiar la cuenta de {type} a Correo Electrónico", - "claim.oauth_to_email_newPwd": "Ingresa una nueva contraseña para tu cuenta de {team} en {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Agregar un comentario...", "create_comment.comment": "Agregar Comentario", @@ -841,8 +839,6 @@ "general_tab.includeDirDesc": "Incluir este equipo mostrará el nombre del equipo en la sección de Directorio de Equipos en la página de inicio, y proveerá un enlace para la página de inicio de sesión.", "general_tab.includeDirTitle": "Incluir este Equipo en el Directorio de Equipos", "general_tab.no": "No", - "general_tab.openInviteDesc": "Cuando está permitido, un enlace para la creación de cuentas será incluido en la página de registro de este equipo y permitirá a cualquier visitante registrarse.", - "general_tab.openInviteTitle": "Permitir a cualquiera a inscribirse desde la página de inicio de sesión", "general_tab.regenerate": "Regenerar", "general_tab.required": "Este campo es obligatorio", "general_tab.teamName": "Nombre del Equipo", @@ -1111,7 +1107,6 @@ "sidebar_right_menu.report": "Reporta un Problema", "sidebar_right_menu.teamLink": "Enlace Invitación al Equipo", "sidebar_right_menu.teamSettings": "Configurar Equipo", - "signup_team.choose": "Selecciona un Equipo", "signup_team.createTeam": "O Crea un Equipo", "signup_team.disabled": "La creación de Equipos ha sido deshabilitada.", "signup_team.noTeams": "No hay equipos en el Directorio de Equipos y la creación de equipos ha sido deshabilitada.", @@ -1181,61 +1176,6 @@ "team_settings_modal.generalTab": "General", "team_settings_modal.importTab": "Importar", "team_settings_modal.title": "Configuración del Equipo", - "team_signup_display_name.back": "Volver al paso previo", - "team_signup_display_name.charLength": "El nombre debe tener 4 o más caracteres hasta un máximo de 15", - "team_signup_display_name.nameHelp": "Nombre tu equipo en cualquier idioma. El nombre de tu equipo aparecerá en menús y en inicios", - "team_signup_display_name.next": "Siguiente", - "team_signup_display_name.required": "Este campo es obligatorio", - "team_signup_display_name.teamName": "Nombre del Equipo", - "team_signup_email.address": "Dirección de correo electrónico", - "team_signup_email.different": "Please use a different email than the one used at signup", - "team_signup_email.validEmail": "Por favor ingresa una dirección de correo electrónico válida", - "team_signup_password.agreement": "Procediendo a crear tu cuenta y el uso de {siteName}, indicas que estás de acuerdo con nuestros Términos de Servicio y Políticas de Privacidad. Si no estás de acuerdo, no debes utilizar {siteName}.", - "team_signup_password.back": "Volver al paso previo", - "team_signup_password.choosePwd": "Escoge tu contraseña", - "team_signup_password.creating": "Creando equipo...", - "team_signup_password.email": "Correo electrónico", - "team_signup_password.finish": "Finalizar", - "team_signup_password.hint": "Las contraseñas deben contener de {min} a {max} caracteres. Su contraseña será más fuerte si contiene una mezcla de símbolos, números y caracteres en mayúsculas y minúsculas.", - "team_signup_password.passwordError": "Por favor ingrese al menos {chars} caracteres", - "team_signup_password.selectPassword": "Selecciona la contraseña que estás usando con tu dirección de correos:", - "team_signup_password.yourPassword": "Tu contraseña", - "team_signup_send_invites.addInvitation": "Agrega una Invitación", - "team_signup_send_invites.back": "Volver al paso previo", - "team_signup_send_invites.disabled": "Este correo electrónico está actualmente deshabilitado para tu equipo, y los correos no podrán ser enviados. Contacta a tu administrador de sistemas", - "team_signup_send_invites.forNow": "por ahora.", - "team_signup_send_invites.next": "Siguiente", - "team_signup_send_invites.prefer": "Si prefieres, puedes invitar a miembros de equipo más tarde
    y ", - "team_signup_send_invites.skip": "saltarte este paso ", - "team_signup_send_invites.title": "Invita Miembros al Equipo", - "team_signup_url.back": "Volver al paso previo", - "team_signup_url.charLength": "El nombre debe tener 4 o más caracteres hasta un máximo de 15", - "team_signup_url.hint": "
  • Corto y memorizable es mejor
  • Use letras en minúsculas, números y guiones
  • Debe empezar con una letra y no puede finalizar con un guión
  • ", - "team_signup_url.next": "Siguiente", - "team_signup_url.regex": "Sólo utiliza letras en minúsculas, numeros y guiones. Debe comenzar con una letra y no puede terminar en un guión.", - "team_signup_url.required": "Este campo es obligatorio", - "team_signup_url.taken": "Este URL ya fue asignado o contiene una palabra reservada", - "team_signup_url.teamUrl": "URL de Equipo", - "team_signup_url.unavailable": "Este URL no está disponible. Por favor intenta con otro.", - "team_signup_url.webAddress": "Escoge la dirección web de tu nuevo equipo:", - "team_signup_username.back": "Volver al paso previo", - "team_signup_username.chooseUsername": "Escoge un nombre de usuario", - "team_signup_username.hint": "El nombre de usuario debe empezar con una letra, y contener entre {min} a {max} caracteres en minúscula con números, letras, y los símbolos '.', '-' y '_'.", - "team_signup_username.invalid": "El nombre de usuario debe comenzar con una letra, y tener entre {min} y {max} de caracteres en total, los cuales pueden ser numeros, letras en minúsculas, o cualquiera de los simbolos '.', '-', o '_'", - "team_signup_username.memorable": "Selecciona un nombre de usuario sencillo de recordar y que sea fácil para a tus compañeros de equipo identificarte:", - "team_signup_username.next": "Siguiente", - "team_signup_username.reserved": "Este nombre de usuario está reservado. Por favor escoge otro.", - "team_signup_username.username": "Tu nombre de usuario", - "team_signup_welcome.address": "Dirección de correo", - "team_signup_welcome.admin": "Tu cuenta administrará un nuevo sitio del equipo.
    Puedes agregar otros administradores más adelante.", - "team_signup_welcome.confirm": "Por favor confirma tu dirección de correos:", - "team_signup_welcome.different": "Usa un correo diferente", - "team_signup_welcome.instead": "Usa este en vez de", - "team_signup_welcome.lets": "permítenos setear tu nuevo equipo", - "team_signup_welcome.storageError": "Este servicio requiere de almacenamiento local para ser habilitado. Por favor habilítalo o sale de la navegación privad.", - "team_signup_welcome.validEmailError": "Por favor ingresa una dirección de correo electrónico válida", - "team_signup_welcome.welcome": "Bienvenido a:", - "team_signup_welcome.yes": "Sí, esta dirección es correcta", "textbox.bold": "**negritas**", "textbox.edit": "Editar mensaje", "textbox.help": "Ayuda", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index 44bf36ef6..1e2ea0e9b 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -647,7 +647,7 @@ "choose_auth_page.noSignup": "Aucune méthode configurée pour s'inscrive, veuillez contacter votre administrateur système.", "claim.account.noEmail": "Aucune adresse électronique indiquée", "claim.email_to_ldap.enterLdapPwd": "Saisissez l'identifiant et le mode de passe de votre compte LDAP", - "claim.email_to_ldap.enterPwd": "Saisissez le mot de passe pour votre compte {team} {site}", + "claim.email_to_ldap.enterPwd": "Saisissez le mot de passe pour votre compte {site}", "claim.email_to_ldap.ldapId": "Identifiant LDAP", "claim.email_to_ldap.ldapIdError": "Veuillez saisir votre identifiant LDAP.", "claim.email_to_ldap.ldapPasswordError": "Veuillez saisir votre mot de passe LDAP.", @@ -658,7 +658,7 @@ "claim.email_to_ldap.ssoType": "Une fois votre compte configuré, vous ne pourrez vous connectez qu'avec LDAP", "claim.email_to_ldap.switchTo": "Basculer le compte vers LDAP", "claim.email_to_ldap.title": "Transférer le login par courriel/mot de passe en LDAP", - "claim.email_to_oauth.enterPwd": "Saisissez le mot de passe pour votre compte {team} {site}", + "claim.email_to_oauth.enterPwd": "Saisissez le mot de passe pour votre compte {site}", "claim.email_to_oauth.pwd": "Mot de passe", "claim.email_to_oauth.pwdError": "Veuillez saisir votre mot de passe.", "claim.email_to_oauth.ssoNote": "Vous devez déjà avoir un compte {type} valide", @@ -667,7 +667,6 @@ "claim.email_to_oauth.title": "Changer l'adresse électronique/mot de passe pour {uiType}", "claim.ldap_to_email.confirm": "Confirmer le mot de passe", "claim.ldap_to_email.email": "Vous devrez utiliser l'adresse électronique {email} pour vous connecter.", - "claim.ldap_to_email.enterLdapPwd": "Saisissez votre mot de passe LDAP pour votre compte {team} {site}", "claim.ldap_to_email.enterPwd": "Saisissez un nouveau mot de passe pour votre compte", "claim.ldap_to_email.ldapPasswordError": "Veuillez saisir votre mot de passe LDAP.", "claim.ldap_to_email.ldapPwd": "Mot de passe LDAP", @@ -680,7 +679,7 @@ "claim.oauth_to_email.confirm": "Confirmez le mot de passe", "claim.oauth_to_email.description": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'à l'aide de votre adresse électronique et votre mot de passe.", "claim.oauth_to_email.enterPwd": "Veuillez saisir un mot de passe.", - "claim.oauth_to_email.newPwd": "Saisissez un nouveau mot de passe pour votre compte {team} {site}", + "claim.oauth_to_email.newPwd": "Saisissez un nouveau mot de passe pour votre compte {site}", "claim.oauth_to_email.pwdNotMatch": "Le mot de passe ne correspond pas.", "claim.oauth_to_email.switchTo": "Basculer de {type} vers adresse électronique et mot de passe", "claim.oauth_to_email.title": "Basculer du compte {type} vers l'adresse électronique", @@ -765,8 +764,6 @@ "general_tab.includeDirDesc": "Inclure cette équipe affichera le nom de l'équipe dans l'annuaire sur la page d'accueil, ainsi qu'un lien pour rejoindre cette équipe.", "general_tab.includeDirTitle": "Afficher cette équipe dans l'annuaire", "general_tab.no": "Non", - "general_tab.openInviteDesc": "Si activé, un lien pour créer un compte est affiché sur la page de connexion de l'équipe, et permet à n'importe qui de rejoindre l'équipe.", - "general_tab.openInviteTitle": "Permettre à tout le monde de s'inscrire", "general_tab.regenerate": "Générer de nouveau", "general_tab.required": "Ce champ est obligatoire", "general_tab.teamName": "Nom de l'équipe", @@ -1012,7 +1009,6 @@ "sidebar_right_menu.report": "Signaler un problème", "sidebar_right_menu.teamLink": "Obtenir un lien d'invitation d'équipe", "sidebar_right_menu.teamSettings": "Configuration de l'équipe", - "signup_team.choose": "Choisir une équipe", "signup_team.createTeam": "Ou créez une équipe", "signup_team.disabled": "Aucune méthode de création d'utilisateur n'est disponible. Veuillez contacter votre administrateur système pour obtenir un accès.", "signup_team.noTeams": "Il n'y a aucune équipe dans l'annuaire, et la création d'équipe n'est pas autorisée.", @@ -1081,61 +1077,6 @@ "team_settings_modal.generalTab": "Général", "team_settings_modal.importTab": "Importer", "team_settings_modal.title": "Configuration de l'équipe", - "team_signup_display_name.back": "Revenir à l'étape précédente", - "team_signup_display_name.charLength": "Le nom doit être composé de 4 à 15 caractères", - "team_signup_display_name.nameHelp": "Donnez un nom à votre équipe. Le nom de votre équipe apparait dans les menus et les en-têtes.", - "team_signup_display_name.next": "Suivant", - "team_signup_display_name.required": "Ce champ est obligatoire", - "team_signup_display_name.teamName": "Nom de l'équipe", - "team_signup_email.address": "Adresse électronique", - "team_signup_email.different": "Veuillez utiliser une autre adresse électronique que celle utilisée pour votre inscription", - "team_signup_email.validEmail": "Veuillez saisir une adresse électronique valide", - "team_signup_password.agreement": "En créant ce compte et en utilisant {siteName}, vous consentez à nos Conditions d'utilisation et Politique de Confidentialité. Si vous n'y consentez pas, vous ne pouvez pas utiliser {siteName}.", - "team_signup_password.back": "Retour à l'étape précédente", - "team_signup_password.choosePwd": "Choisissez votre mot de passe", - "team_signup_password.creating": "Création de l'équipe...", - "team_signup_password.email": "Adresse électronique", - "team_signup_password.finish": "Terminé", - "team_signup_password.hint": "Les mots de passe doivent contenir entre {min} et {max} caractère. Votre mot de passe sera plus sécurisé s'il contient un mélange de symboles, de chiffres, et de lettres majuscules et minuscules", - "team_signup_password.passwordError": "Veuillez saisir au moins {chars} caractères", - "team_signup_password.selectPassword": "Choisissez le mot de passe que vous utiliserez pour vous connecter avec votre adresse électronique :", - "team_signup_password.yourPassword": "Votre mot de passe", - "team_signup_send_invites.addInvitation": "Ajouter une invitation", - "team_signup_send_invites.back": "Retour à l'étape précédente", - "team_signup_send_invites.disabled": "Les courriels sont désactivés pour votre équipe et ne peuvent pas être envoyés. Contactez votre administrateur système pour activer les courriels et les invitations par courriel.", - "team_signup_send_invites.forNow": "pour l'instant.", - "team_signup_send_invites.next": "Suivant", - "team_signup_send_invites.prefer": "Vous pouvez aussi inviter des membres plus tard
    et", - "team_signup_send_invites.skip": "passer cette étape", - "team_signup_send_invites.title": "Inviter des membres", - "team_signup_url.back": "Retour à l'étape précédente", - "team_signup_url.charLength": "Le nom doit contenir entre 4 et 15 caractères", - "team_signup_url.hint": "
  • Court et facile à retenir, c'est mieux !
  • Utilisez des lettres minuscules, des chiffres et des tirets
  • Doit commencer par une lettre et ne peut pas finir par un tiret.
  • ", - "team_signup_url.next": "Suivant", - "team_signup_url.regex": "Utilisez seulement des lettres minuscules, des chiffres et des tirets. Doit commencer par une lettre et ne doit pas finir par un tiret.", - "team_signup_url.required": "Champ obligatoire", - "team_signup_url.taken": "Cette URL est indisponible ou contient un mot réservé", - "team_signup_url.teamUrl": "URL de l'équipe", - "team_signup_url.unavailable": "Cette URL est indisponible. Veuillez essayer une autre URL.", - "team_signup_url.webAddress": "Choisissez l'adresse internet de votre nouvelle équipe :", - "team_signup_username.back": "Retour à l'étape précédente", - "team_signup_username.chooseUsername": "Choisissez votre nom d'utilisateur", - "team_signup_username.hint": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de lettres minuscules, de chiffres et des symboles '.', '-' et '_'.", - "team_signup_username.invalid": "Les nomes d'utilisateur doivent commencer par une lettre et contenir entre {min} et {max} caractères, qui peuvent être des chiffres, des lettres minuscules ou les symboles '.', '-' ou '_'.", - "team_signup_username.memorable": "Choisissez un nom d'utilisateur facile à retenir qui permettra aux autres membres de l'équipe de vous identifier facilement :", - "team_signup_username.next": "Suivant", - "team_signup_username.reserved": "Ce nom d'utilisateur est réservé, veuilles en choisir un autre.", - "team_signup_username.username": "Votre nom d'utilisateur", - "team_signup_welcome.address": "Adresse électronique", - "team_signup_welcome.admin": "Votre compte sera administrateur de votre nouvelle équipe.
    Vous pourrez ajouter d'autres administrateurs par la suite.", - "team_signup_welcome.confirm": "Veuillez confirmer votre adresse électronique :", - "team_signup_welcome.different": "Utiliser une autre adresse électronique", - "team_signup_welcome.instead": "Utiliser plutôt ceci", - "team_signup_welcome.lets": "Configurons ensemble votre nouvelle équipe", - "team_signup_welcome.storageError": "Ce service nécessite l'utilisation des cookies. Veuillez permettre le stockage des cookies ou quitter la navigation privée.", - "team_signup_welcome.validEmailError": "Veuillez entrer une adresse électronique valide", - "team_signup_welcome.welcome": "Bienvenue sur :", - "team_signup_welcome.yes": "Oui, cette adresse est correcte", "textbox.bold": "**gras**", "textbox.edit": "Modifier le message", "textbox.help": "Aide", diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index ea951235e..eab97673e 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -1181,61 +1181,6 @@ "team_settings_modal.generalTab": "全般", "team_settings_modal.importTab": "インポート", "team_settings_modal.title": "チームの設定", - "team_signup_display_name.back": "前のステップに戻る", - "team_signup_display_name.charLength": "名前は4文字以上の15文字以下にしてください", - "team_signup_display_name.nameHelp": "チーム名はどんな言語でも使うことができます。チーム名はメニューと画面上部に表示されます。", - "team_signup_display_name.next": "次へ", - "team_signup_display_name.required": "この項目は必須です", - "team_signup_display_name.teamName": "チーム名", - "team_signup_email.address": "電子メールアドレス", - "team_signup_email.different": "利用登録で使用した電子メールアドレスとは別の電子メールアドレスを使ってください", - "team_signup_email.validEmail": "有効な電子メールアドレスを入力してください", - "team_signup_password.agreement": "{siteName}にアカウントを作成し利用する前に使用条件プライバシーポリシーに同意してください。同意できない場合は{siteName}は使用できません。", - "team_signup_password.back": "前のステップに戻る", - "team_signup_password.choosePwd": "パスワードを入力してください", - "team_signup_password.creating": "チームを作成しています…", - "team_signup_password.email": "電子メールアドレス", - "team_signup_password.finish": "完了する", - "team_signup_password.hint": "パスワードは{min}から{max}文字にしてください。パスワードを強固にするには、記号、数字、大文字と小文字の英字が混在するようにしてください。", - "team_signup_password.passwordError": "少なくとも{chars}文字を入力してください。", - "team_signup_password.selectPassword": "電子メールアドレスでログインする場合のパスワードを入力してください:", - "team_signup_password.yourPassword": "あなたのパスワード", - "team_signup_send_invites.addInvitation": "招待する", - "team_signup_send_invites.back": "前のステップに戻る", - "team_signup_send_invites.disabled": "あなたのチームでは電子メールは有効になっていません。電子メールによる招待状は送信できません。システム管理者に電子メールと電子メールによる招待を有効にするように連絡してください。", - "team_signup_send_invites.forNow": "いますぐ。", - "team_signup_send_invites.next": "次へ", - "team_signup_send_invites.prefer": "後ほどチームメンバーを招待することもできます
    また ", - "team_signup_send_invites.skip": "このステップはスキップできます ", - "team_signup_send_invites.title": "チームメンバーを招待する", - "team_signup_url.back": "前のステップに戻る", - "team_signup_url.charLength": "名前は4文字以上の15文字以下にしてください", - "team_signup_url.hint": "
  • 短く覚えやすいものがベストです
  • 英小文字、数字、ダッシュを使ってください
  • 英小文字で始めてください。ダッシュで終わることはできません
  • ", - "team_signup_url.next": "次へ", - "team_signup_url.regex": "英小文字、数字、ダッシュのみ使用できます。英小文字で始めてください。ダッシュで終わることはできません。", - "team_signup_url.required": "この項目は必須です", - "team_signup_url.taken": "URLが取得済みか、予約された単語を含んでいます", - "team_signup_url.teamUrl": "チームURL", - "team_signup_url.unavailable": "このURLは使用できません。他のものを試してください。", - "team_signup_url.webAddress": "あなたの新しいチームのウェブアドレスを選択してください。", - "team_signup_username.back": "前のステップに戻る", - "team_signup_username.chooseUsername": "ユーザー名を入力してください", - "team_signup_username.hint": "ユーザー名は英小文字で始めてください。また{min}から{max} 文字の英数字と'.'、'-'、'_'の記号だけで構成してください。", - "team_signup_username.invalid": "ユーザー名は英小文字で始めてください。また{min}から{max} 文字の英数字と'.'、'-'、'_'の記号だけで構成してください。", - "team_signup_username.memorable": "チームメイトが認識しやすいように覚えやすいユーザー名を選択してください:", - "team_signup_username.next": "次へ", - "team_signup_username.reserved": "このユーザー名は予約されています。他のユーザー名を使ってください。", - "team_signup_username.username": "あなたのユーザー名", - "team_signup_welcome.address": "電子メールアドレス", - "team_signup_welcome.admin": "あなたは新しいチームサイトの管理者になります。
    後ほど他の人を管理者として追加することができます。", - "team_signup_welcome.confirm": "あなたの電子メールアドレスを確認してください:", - "team_signup_welcome.different": "違う電子メールアドレスを使う", - "team_signup_welcome.instead": "これを代わりに使用する", - "team_signup_welcome.lets": "新しいチームを作りましょう", - "team_signup_welcome.storageError": "このサービスを使用するにはローカルストレージを有効にしてください。有効にするかプライベートブラウジングを止めてください。", - "team_signup_welcome.validEmailError": "有効な電子メールアドレスを入力してください", - "team_signup_welcome.welcome": "ようこそ:", - "team_signup_welcome.yes": "この電子メールアドレスは正しいです", "textbox.bold": "**太字**", "textbox.edit": "メッセージを編集する", "textbox.help": "ヘルプ", diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json index 97fea8396..0fcb958c2 100644 --- a/webapp/i18n/pt.json +++ b/webapp/i18n/pt.json @@ -721,7 +721,7 @@ "choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email específicado", "claim.email_to_ldap.enterLdapPwd": "Entre o ID e a senha para sua conta LDAP", - "claim.email_to_ldap.enterPwd": "Entre a senha para o sua conta com email {team} {site}", + "claim.email_to_ldap.enterPwd": "Entre a senha para o sua conta com email {site}", "claim.email_to_ldap.ldapId": "LDAP ID", "claim.email_to_ldap.ldapIdError": "Por favor digite seu ID LDAP.", "claim.email_to_ldap.ldapPasswordError": "Por favor digite a sua senha LDAP.", @@ -732,7 +732,7 @@ "claim.email_to_ldap.ssoType": "Ao retirar a sua conta, você só vai ser capaz de logar com LDAP", "claim.email_to_ldap.switchTo": "Trocar a conta para LDAP", "claim.email_to_ldap.title": "Trocar E-mail/Senha da Conta para LDAP", - "claim.email_to_oauth.enterPwd": "Entre a senha para o sua conta {team} {site}", + "claim.email_to_oauth.enterPwd": "Entre a senha para o sua conta {site}", "claim.email_to_oauth.pwd": "Senha", "claim.email_to_oauth.pwdError": "Por favor digite a sua senha.", "claim.email_to_oauth.ssoNote": "Você precisa já ter uma conta {type} válida", @@ -741,7 +741,6 @@ "claim.email_to_oauth.title": "Trocar E-mail/Senha da Conta para {uiType}", "claim.ldap_to_email.confirm": "Confirmar senha", "claim.ldap_to_email.email": "Você vai usar o email {email} para logar", - "claim.ldap_to_email.enterLdapPwd": "Entre a sua senha LDAP para o sua conta {team} {site}", "claim.ldap_to_email.enterPwd": "Entre a nova senha para o sua conta com email.", "claim.ldap_to_email.ldapPasswordError": "Por favor digite a sua senha LDAP.", "claim.ldap_to_email.ldapPwd": "Senha LDAP", @@ -758,7 +757,6 @@ "claim.oauth_to_email.pwdNotMatch": "As senha não correspondem.", "claim.oauth_to_email.switchTo": "Trocar {type} para email e senha", "claim.oauth_to_email.title": "Trocar Conta {type} para E-mail", - "claim.oauth_to_email_newPwd": "Entre a nova senha para o sua conta {team} {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Adicionar um comentário...", "create_comment.comment": "Adicionar Comentário", @@ -841,8 +839,6 @@ "general_tab.includeDirDesc": "Incluindo esta equipe irá exibir o nome da equipe da seção Diretório Equipe da página inicial, e fornecer um link para a página de login.", "general_tab.includeDirTitle": "Incluir esta equipe no Diretório de Equipe", "general_tab.no": "Não", - "general_tab.openInviteDesc": "Quando permitido, um link para a criação da conta será incluído na página de login da equipe e permitir que qualquer visitante inscreva-se.", - "general_tab.openInviteTitle": "Permitir que qualquer pessoa se inscreva a partir da página de login", "general_tab.regenerate": "Re-Gerar", "general_tab.required": "Este campo é obrigatório", "general_tab.teamName": "Nome da Equipe", @@ -1111,7 +1107,6 @@ "sidebar_right_menu.report": "Relatar um Problema", "sidebar_right_menu.teamLink": "Obter Link para Convite de Equipe", "sidebar_right_menu.teamSettings": "Configurações da Equipe", - "signup_team.choose": "Escolha uma Equipe", "signup_team.createTeam": "Ou Criar uma Equipe", "signup_team.disabled": "A criação de equipe foi desativada. Por favor, entre em contato com um administrador para o acesso.", "signup_team.noTeams": "Não existe equipes incluidas no Diretório de Equipe e a criação de equipes foi desativada.", @@ -1181,61 +1176,6 @@ "team_settings_modal.generalTab": "Geral", "team_settings_modal.importTab": "Importar", "team_settings_modal.title": "Configurações da Equipe", - "team_signup_display_name.back": "Voltar para o passo anterior", - "team_signup_display_name.charLength": "O nome deve ser de 4 ou mais caracteres até um máximo de 15", - "team_signup_display_name.nameHelp": "Nome da sua equipe em qualquer idioma. Seu nome de equipe é mostrado em menus e títulos.", - "team_signup_display_name.next": "Próximo", - "team_signup_display_name.required": "Este campo é obrigatório", - "team_signup_display_name.teamName": "Nome Da Equipe", - "team_signup_email.address": "Endereço de E-mail", - "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado na inscrição", - "team_signup_email.validEmail": "Por favor entre um endereço de e-mail válido", - "team_signup_password.agreement": "Ao prosseguir para criar sua conta e usar {siteName}, você concorda com nosso Termo de Serviço e Politica de Privacidade. Se você não concorda, você não pode usar {siteName}.", - "team_signup_password.back": "Voltar para o passo anterior", - "team_signup_password.choosePwd": "Escolha sua senha", - "team_signup_password.creating": "Criando um equipe...", - "team_signup_password.email": "E-mail", - "team_signup_password.finish": "Terminar", - "team_signup_password.hint": "Senhas precisam conter {min} a {max} caracteres. Sua senha será segura se conter uma mistura de símbolos, números, e caracteres maiúsculos e minúsculos.", - "team_signup_password.passwordError": "Por favor, insira pelo menos {chars} caracteres", - "team_signup_password.selectPassword": "Selecione uma senha que você irá usar no login com seu endereço de email:", - "team_signup_password.yourPassword": "Sua senha", - "team_signup_send_invites.addInvitation": "Adicionar Convite", - "team_signup_send_invites.back": "Voltar para o passo anterior", - "team_signup_send_invites.disabled": "Email está desativado para a sua equipe, e emails e não podem ser enviados. Contate o seu administrador do sistema para ativar e-mail e convites por e-mail.", - "team_signup_send_invites.forNow": "agora.", - "team_signup_send_invites.next": "Próximo", - "team_signup_send_invites.prefer": "se você preferir, você pode convidar membros da equipe depois
    e ", - "team_signup_send_invites.skip": "pular este passo ", - "team_signup_send_invites.title": "Convidar Membros da Equipe", - "team_signup_url.back": "Voltar para o passo anterior", - "team_signup_url.charLength": "O nome deve ser de 4 ou mais caracteres até um máximo de 15", - "team_signup_url.hint": "
  • Curto e memorizável é o melhor
  • Use letras minúsculas, números e traços
  • Deve começar com uma letra e não pode terminar em um traço
  • ", - "team_signup_url.next": "Próximo", - "team_signup_url.regex": "Utilize apenas letras minúsculas, números e traços. Deve começar com uma letra e não pode terminar em um traço.", - "team_signup_url.required": "Este campo é obrigatório", - "team_signup_url.taken": "URL é usada ou contém uma palavra reservada", - "team_signup_url.teamUrl": "Equipe URL", - "team_signup_url.unavailable": "Está URL está indisponível. Por favor tente outra.", - "team_signup_url.webAddress": "Escolha o endereço web para sua nova equipe:", - "team_signup_username.back": "Voltar para o passo anterior", - "team_signup_username.chooseUsername": "Escolha o seu nome de usuário", - "team_signup_username.hint": "O nome de usuário precisa começar com uma letra, e conter entre {min} e {max} caracteres minúsculos contendo números, letras, e os símbolos '.', '-' e '_'", - "team_signup_username.invalid": "O nome de usuário precisa começar com uma letra, e conter entre {min} e {max} caracteres no total, podendo ser números, letras minúsculas, ou qualquer dos símbolos '.', '-' ou '_'", - "team_signup_username.memorable": "Escolha um nome de usuário memorizável que torna fácil para sua equipe de trabalho identificá-lo:", - "team_signup_username.next": "Próximo", - "team_signup_username.reserved": "Este nome de usuário é reservado, por favor, escolha uma nova.", - "team_signup_username.username": "Seu usuário", - "team_signup_welcome.address": "Endereço de E-mail", - "team_signup_welcome.admin": "Sua conta irá administrar o novo site da equipe.
    Você pode adicionar outros administradores depois.", - "team_signup_welcome.confirm": "Por favor confirme seu endereço de e-mail:", - "team_signup_welcome.different": "Utilize um e-mail diferente", - "team_signup_welcome.instead": "Use este ao invez", - "team_signup_welcome.lets": "Vamos configurar sua nova equipe", - "team_signup_welcome.storageError": "Este serviço requer um armazenamento local para ser ativado. Por favor, habilite ou saia da navegação privada.", - "team_signup_welcome.validEmailError": "Por favor entre um endereço de e-mail válido", - "team_signup_welcome.welcome": "Bem-vindo:", - "team_signup_welcome.yes": "Sim, este endereço de email está correto", "textbox.bold": "**negrito**", "textbox.edit": "Editar mensagem", "textbox.help": "Ajuda", diff --git a/webapp/package.json b/webapp/package.json index 2e9efadb3..3ab971c09 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -28,7 +28,8 @@ "react-router": "2.0.1", "react-textarea-autosize": "3.3.0", "twemoji": "1.4.1", - "velocity-animate": "1.2.3" + "velocity-animate": "1.2.3", + "superagent": "1.8.3" }, "devDependencies": { "babel-eslint": "5.0.0", @@ -53,12 +54,22 @@ "sass-loader": "3.2.0", "style-loader": "0.13.0", "url-loader": "0.5.7", - "webpack": "2.1.0-beta.5" + "webpack": "2.1.0-beta.5", + + "mocha": "*", + "mocha-webpack": "*", + "webpack-node-externals": "*", + "mocha-jsdom": "*", + "jsdom": "*", + "jsdom-global": "*", + "react-addons-test-utils": "*", + "jquery-deferred": "*" }, "scripts": { "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .", "build": "webpack", "run": "webpack --progress --watch", - "run-fullmap": "webpack --progress --watch" + "run-fullmap": "webpack --progress --watch", + "test": "mocha-webpack --webpack-config webpack.config-test.js \"**/*.test.jsx\"" } } diff --git a/webapp/root.html b/webapp/root.html index 1612bdce4..cc2b7cd61 100644 --- a/webapp/root.html +++ b/webapp/root.html @@ -46,7 +46,18 @@ -
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/webapp/root.jsx b/webapp/root.jsx index 9268643f3..b0625438f 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -10,10 +10,10 @@ import 'sass/styles.scss'; import React from 'react'; import ReactDOM from 'react-dom'; -import {Router, Route, IndexRoute, IndexRedirect, Redirect, browserHistory} from 'react-router'; +import {Router, Route, IndexRoute, Redirect, browserHistory} from 'react-router'; import Root from 'components/root.jsx'; import LoggedIn from 'components/logged_in.jsx'; -import NotLoggedIn from 'components/not_logged_in.jsx'; +import HeaderFooterTemplate from 'components/header_footer_template.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'; @@ -21,16 +21,15 @@ import ChannelView from 'components/channel_view.jsx'; import PermalinkView from 'components/permalink_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 Websockets from 'action_creators/websocket_actions.jsx'; +import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; + +import Client from 'utils/web_client.jsx'; + +import * as Websockets from 'action_creators/websocket_actions.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'; @@ -47,14 +46,9 @@ import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; import AddCommand from 'components/backstage/add_command.jsx'; import ErrorPage from 'components/error_page.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'; +import AppDispatcher from './dispatcher/app_dispatcher.jsx'; +import Constants from './utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; import Claim from 'components/claim/claim.jsx'; import EmailToOAuth from 'components/claim/components/email_to_oauth.jsx'; @@ -63,6 +57,10 @@ import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx'; import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx'; import Login from 'components/login/login.jsx'; +import SelectTeam from 'components/select_team/select_team.jsx'; +import CreateTeam from 'components/create_team/create_team.jsx'; +import CreateTeamDisplayName from 'components/create_team/components/display_name.jsx'; +import CreateTeamTeamUrl from 'components/create_team/components/team_url.jsx'; import * as I18n from 'i18n/i18n.jsx'; @@ -76,53 +74,33 @@ const notFoundParams = { // 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'); + window.onerror = (msg, url, line, column, stack) => { + var l = {}; + l.level = 'ERROR'; + l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; + + $.ajax({ + url: '/api/v3/admin/log_client', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(l) + }); + + if (window.mm_config && 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(); } - ); + }; - const d2 = Client.getClientLicenceConfig( - (data) => { - if (!data) { - return; - } + var d1 = $.Deferred(); //eslint-disable-line new-cap - global.window.mm_license = data; - }, - (err) => { - AsyncClient.dispatchError(err, 'getClientLicenceConfig'); + GlobalActions.emitInitialLoad( + () => { + d1.resolve(); } ); - // 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 - }; - // Make sure the websockets close $(window).on('beforeunload', () => { @@ -132,7 +110,9 @@ function preRenderSetup(callwhendone) { function afterIntl() { I18n.doAddLocaleData(); - $.when(d1, d2).done(callwhendone); + $.when(d1).done(() => { + callwhendone(); + }); } if (global.Intl) { @@ -143,18 +123,59 @@ function preRenderSetup(callwhendone) { } function preLoggedIn(nextState, replace, callback) { - const d1 = Client.getAllPreferences( + ErrorStore.clearLastError(); + callback(); +} + +function preNeedsTeam(nextState, replace, callback) { + // First check to make sure you're in the current team + // for the current url. + var teamName = Utils.getTeamNameFromUrl(); + var team = TeamStore.getByName(teamName); + + if (!team) { + browserHistory.push('/error'); + return; + } + + GlobalActions.emitCloseRightHandSide(); + + TeamStore.saveMyTeam(team); + TeamStore.emitChange(); + + var d1 = $.Deferred(); //eslint-disable-line new-cap + var d2 = $.Deferred(); //eslint-disable-line new-cap + + Client.getChannels( (data) => { - PreferenceStore.setPreferencesFromServer(data); + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_CHANNELS, + channels: data.channels, + members: data.members + }); + + d1.resolve(); }, (err) => { - AsyncClient.dispatchError(err, 'getAllPreferences'); + AsyncClient.dispatchError(err, 'getChannels'); + d1.resolve(); } ); - const d2 = AsyncClient.getChannels(); + Client.getProfiles( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES, + profiles: data + }); - ErrorStore.clearLastError(); + d2.resolve(); + }, + (err) => { + AsyncClient.dispatchError(err, 'getProfiles'); + d2.resolve(); + } + ); $.when(d1, d2).done(() => { callback(); @@ -163,21 +184,20 @@ function preLoggedIn(nextState, replace, callback) { function onPermalinkEnter(nextState) { const postId = nextState.params.postid; - GlobalActions.emitPostFocusEvent(postId); } -function onChannelEnter(nextState) { - doChannelChange(nextState); +function onChannelEnter(nextState, replace) { + doChannelChange(nextState, replace); } -function onChannelChange(prevState, nextState) { +function onChannelChange(prevState, nextState, replace) { if (prevState.params.channel !== nextState.params.channel) { - doChannelChange(nextState); + doChannelChange(nextState, replace); } } -function doChannelChange(state) { +function doChannelChange(state, replace) { let channel; if (state.location.query.fakechannel) { channel = JSON.parse(state.location.query.fakechannel); @@ -187,28 +207,13 @@ function doChannelChange(state) { channel = ChannelStore.getMoreByName(state.params.channel); } if (!channel) { - console.error('Unable to get channel to change to.'); //eslint-disable-line no-console + replace('/'); + return; } } GlobalActions.emitChannelClickEvent(channel); } -function onLoggedOut(nextState) { - const teamName = nextState.params.team; - Client.logout( - () => { - browserHistory.push('/' + teamName + '/login'); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - PreferenceStore.clear(); - }, - () => { - browserHistory.push('/' + teamName + '/login'); - } - ); -} - function renderRootComponent() { ReactDOM.render(( - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - + - + + + + + + + + + + + + - + ), diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss index 3257f6582..ebfe97ee4 100644 --- a/webapp/sass/routes/_backstage.scss +++ b/webapp/sass/routes/_backstage.scss @@ -216,6 +216,7 @@ body { .item-details__description, .item-details__token, .item-details__trigger-words, + .item-details__url, .item-details__creation { display: inline-block; margin-top: 10px; diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss index 77ccdf4ed..08bd0d12d 100644 --- a/webapp/sass/routes/_signup.scss +++ b/webapp/sass/routes/_signup.scss @@ -419,6 +419,17 @@ } } + .signup-team-dir-err { + background: #fafafa; + border-top: 1px solid #d5d5d5; + color: inherit; + padding: 5px 15px; + + &:first-child { + border: none; + } + } + .signup-team-dir__name { float: left; overflow: hidden; diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx index d605aac80..2dae78f46 100644 --- a/webapp/stores/browser_store.jsx +++ b/webapp/stores/browser_store.jsx @@ -35,18 +35,6 @@ class BrowserStoreClass { this.isSignallingLogin = this.isSignallingLogin.bind(this); } - checkVersion() { - var currentVersion = this.getGlobalItem('storage_version'); - if (currentVersion !== global.window.mm_config.Version) { - this.clearAll(); - try { - this.setGlobalItem('storage_version', global.window.mm_config.Version); - } catch (e) { - // Do nothing - } - } - } - setItem(name, value) { this.setGlobalItem(getPrefix() + name, value); } diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx index 715029185..3e043dd78 100644 --- a/webapp/stores/error_store.jsx +++ b/webapp/stores/error_store.jsx @@ -20,6 +20,12 @@ class ErrorStoreClass extends EventEmitter { this.removeChangeListener = this.removeChangeListener.bind(this); this.getLastError = this.getLastError.bind(this); this.storeLastError = this.storeLastError.bind(this); + this.getIgnoreEmailPreview = this.getIgnoreEmailPreview.bind(this); + this.ignore_email_preview = false; + } + + getIgnoreEmailPreview() { + return this.ignore_email_preview; } emitChange() { @@ -57,6 +63,11 @@ class ErrorStoreClass extends EventEmitter { } clearLastError() { + var lastError = this.getLastError(); + if (lastError && lastError.email_preview) { + this.ignore_email_preview = true; + } + BrowserStore.removeGlobalItem('last_error'); BrowserStore.removeGlobalItem('last_error_conn'); this.emitChange(); diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx index fcfd1c426..1a461f39f 100644 --- a/webapp/stores/preference_store.jsx +++ b/webapp/stores/preference_store.jsx @@ -112,4 +112,4 @@ class PreferenceStoreClass extends EventEmitter { const PreferenceStore = new PreferenceStoreClass(); export default PreferenceStore; -window.PreferenceStore = PreferenceStore; +global.window.PreferenceStore = PreferenceStore; diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index e1fc9167d..356df7b07 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -33,7 +33,14 @@ class TeamStoreClass extends EventEmitter { this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + this.clear(); + } + + clear() { this.teams = {}; + this.team_members = []; + this.members_for_team = []; + this.teamListings = {}; this.currentTeamId = ''; } @@ -119,6 +126,34 @@ class TeamStoreClass extends EventEmitter { this.saveTeam(team); this.currentTeamId = team.id; } + + saveTeamMembers(members) { + this.team_members = members; + } + + appendTeamMember(member) { + this.team_members.push(member); + } + + getTeamMembers() { + return this.team_members; + } + + saveMembersForTeam(members) { + this.members_for_team = members; + } + + getMembersForTeam() { + return this.members_for_team; + } + + saveTeamListings(teams) { + this.teamListings = teams; + } + + getTeamListings() { + return this.teamListings; + } } var TeamStore = new TeamStoreClass(); @@ -135,6 +170,18 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; + case ActionTypes.RECEIVED_TEAM_MEMBERS: + TeamStore.saveTeamMembers(action.team_members); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAM_LISTINGS: + TeamStore.saveTeamListings(action.teams); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_MEMBERS_FOR_TEAM: + TeamStore.saveMembersForTeam(action.team_members); + TeamStore.emitChange(); + break; default: } }); diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 4213e6e8c..2c6bfade3 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -16,11 +16,17 @@ const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); + this.clear(); + } + + clear() { this.profiles = {}; + this.direct_profiles = {}; this.statuses = {}; this.sessions = {}; this.audits = {}; this.currentUserId = ''; + this.noAccounts = false; } emitChange(userId) { @@ -116,7 +122,12 @@ class UserStoreClass extends EventEmitter { return this.getCurrentUser(); } - return this.getProfiles()[userId]; + const user = this.getProfiles()[userId]; + if (user) { + return user; + } + + return this.getDirectProfiles()[userId]; } getProfileByUsername(username) { @@ -137,6 +148,14 @@ class UserStoreClass extends EventEmitter { return profileUsernameMap; } + getDirectProfiles() { + return this.direct_profiles; + } + + saveDirectProfiles(profiles) { + this.direct_profiles = profiles; + } + getProfiles() { return this.profiles; } @@ -259,6 +278,14 @@ class UserStoreClass extends EventEmitter { getStatus(id) { return this.getStatuses()[id]; } + + getNoAccounts() { + return this.noAccounts; + } + + setNoAccounts(noAccounts) { + this.noAccounts = noAccounts; + } } var UserStore = new UserStoreClass(); @@ -272,6 +299,10 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.saveProfiles(action.profiles); UserStore.emitChange(); break; + case ActionTypes.RECEIVED_DIRECT_PROFILES: + UserStore.saveDirectProfiles(action.profiles); + UserStore.emitChange(); + break; case ActionTypes.RECEIVED_ME: UserStore.setCurrentUser(action.me); UserStore.emitChange(action.me.id); diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx new file mode 100644 index 000000000..b8374123c --- /dev/null +++ b/webapp/tests/client_channel.test.jsx @@ -0,0 +1,334 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Channels', function() { + this.timeout(100000); + + it('createChannel', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.fakeChannel(); + channel.team_id = TestHelper.basicTeam().id; + TestHelper.basicClient().createChannel( + channel, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.name, channel.name); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('createDirectChannel', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().createUser( + TestHelper.fakeUser(), + function(user2) { + TestHelper.basicClient().addUserToTeam( + user2.id, + function() { + TestHelper.basicClient().createDirectChannel( + user2.id, + function(data) { + assert.equal(data.id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateChannel', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + channel.display_name = 'changed'; + TestHelper.basicClient().updateChannel( + channel, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.display_name, 'changed'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateChannelHeader', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + channel.display_name = 'changed'; + TestHelper.basicClient().updateChannelHeader( + channel.id, + 'new header', + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.header, 'new header'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateChannelPurpose', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + channel.display_name = 'changed'; + TestHelper.basicClient().updateChannelPurpose( + channel.id, + 'new purpose', + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.purpose, 'new purpose'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateChannelNotifyProps', function(done) { + TestHelper.initBasic(() => { + var props = {}; + props.channel_id = TestHelper.basicChannel().id; + props.user_id = TestHelper.basicUser().id; + props.desktop = 'all'; + TestHelper.basicClient().updateChannelNotifyProps( + props, + function(data) { + assert.equal(data.desktop, 'all'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('leaveChannel', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + TestHelper.basicClient().leaveChannel( + channel.id, + function(data) { + assert.equal(data.id, channel.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('joinChannel', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + TestHelper.basicClient().leaveChannel( + channel.id, + function() { + TestHelper.basicClient().joinChannel( + channel.id, + function(data) { + assert.equal(data.id, channel.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('deleteChannel', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + TestHelper.basicClient().deleteChannel( + channel.id, + function(data) { + assert.equal(data.id, channel.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateLastViewedAt', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + TestHelper.basicClient().updateLastViewedAt( + channel.id, + function(data) { + assert.equal(data.id, channel.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getChannels', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getChannels( + function(data) { + assert.equal(data.channels.length, 3); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getChannel', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getChannel( + TestHelper.basicChannel().id, + function(data) { + assert.equal(TestHelper.basicChannel().id, data.channel.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getMoreChannels', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getMoreChannels( + function(data) { + assert.equal(data.channels.length, 0); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getChannelCounts', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getChannelCounts( + function(data) { + assert.equal(data.counts[TestHelper.basicChannel().id], 1); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getChannelExtraInfo', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getChannelExtraInfo( + TestHelper.basicChannel().id, + 5, + function(data) { + assert.equal(data.member_count, 1); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('addChannelMember', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().createUser( + TestHelper.fakeUser(), + function(user2) { + TestHelper.basicClient().addUserToTeam( + user2.id, + function() { + TestHelper.basicClient().addChannelMember( + TestHelper.basicChannel().id, + user2.id, + function(data) { + assert.equal(data.channel_id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('removeChannelMember', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().removeChannelMember( + TestHelper.basicChannel().id, + TestHelper.basicUser().id, + function(data) { + assert.equal(data.channel_id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_command.test.jsx b/webapp/tests/client_command.test.jsx new file mode 100644 index 000000000..f7f0d2b25 --- /dev/null +++ b/webapp/tests/client_command.test.jsx @@ -0,0 +1,123 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Commands', function() { + this.timeout(100000); + + it('listCommands', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().listCommands( + function(data) { + assert.equal(data.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('listTeamCommands', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().listTeamCommands( + function() { + done(new Error('cmds not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.command.disabled.app_error'); + done(); + } + ); + }); + }); + + it('executeCommand', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().executeCommand( + TestHelper.basicChannel().id, + '/shrug', + null, + function(data) { + assert.equal(data.response_type, 'in_channel'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('addCommand', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var cmd = {}; + cmd.url = 'http://www.gonowhere.com'; + cmd.trigger = '/hello'; + cmd.method = 'P'; + cmd.username = ''; + cmd.icon_url = ''; + cmd.auto_complete = false; + cmd.auto_complete_desc = ''; + cmd.auto_complete_hint = ''; + cmd.display_name = 'Unit Test'; + + TestHelper.basicClient().addCommand( + cmd, + function() { + done(new Error('cmds not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.command.disabled.app_error'); + done(); + } + ); + }); + }); + + it('deleteCommand', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().deleteCommand( + TestHelper.generateId(), + function() { + done(new Error('cmds not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.command.disabled.app_error'); + done(); + } + ); + }); + }); + + it('regenCommandToken', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().regenCommandToken( + TestHelper.generateId(), + function() { + done(new Error('cmds not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.command.disabled.app_error'); + done(); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_general.test.jsx b/webapp/tests/client_general.test.jsx new file mode 100644 index 000000000..870c11257 --- /dev/null +++ b/webapp/tests/client_general.test.jsx @@ -0,0 +1,333 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +var assert = require('assert'); +import TestHelper from './test_helper.jsx'; + +describe('Client.General', function() { + this.timeout(10000); + + it('Admin.getClientConfig', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getClientConfig( + function(data) { + assert.equal(data.SiteName, 'Mattermost'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('Admin.getComplianceReports', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getComplianceReports( + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.saveComplianceReports', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var job = {}; + job.desc = 'desc'; + job.emails = ''; + job.keywords = 'test'; + job.start_at = new Date(); + job.end_at = new Date(); + + TestHelper.basicClient().saveComplianceReports( + job, + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.getLogs', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getLogs( + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.getServerAudits', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getServerAudits( + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.getConfig', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getConfig( + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.getAnalytics', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getAnalytics( + 'standard', + null, + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.getTeamAnalytics', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().getTeamAnalytics( + TestHelper.basicTeam().id, + 'standard', + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.saveConfig', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var config = {}; + config.site_name = 'test'; + + TestHelper.basicClient().saveConfig( + config, + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.testEmail', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var config = {}; + config.site_name = 'test'; + + TestHelper.basicClient().testEmail( + config, + function() { + done(new Error('should need system admin permissions')); + }, + function(err) { + assert.equal(err.id, 'api.context.system_permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.logClientError', function(done) { + TestHelper.initBasic(() => { + var config = {}; + config.site_name = 'test'; + TestHelper.basicClient().logClientError('this is a test'); + done(); + }); + }); + + it('Admin.adminResetMfa', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + TestHelper.basicClient().adminResetMfa( + TestHelper.basicUser().id, + function() { + done(new Error('should need a license')); + }, + function(err) { + assert.equal(err.id, 'api.context.permissions.app_error'); + done(); + } + ); + }); + }); + + it('Admin.adminResetPassword', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var user = TestHelper.basicUser(); + + TestHelper.basicClient().resetPassword( + user.id, + 'new_password', + function() { + throw Error('shouldnt work'); + }, + function(err) { + // this should fail since you're not a system admin + assert.equal(err.id, 'api.context.invalid_param.app_error'); + done(); + } + ); + }); + }); + + it('License.getClientLicenceConfig', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getClientLicenceConfig( + function(data) { + assert.equal(data.IsLicensed, 'false'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('License.removeLicenseFile', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().removeLicenseFile( + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.context.permissions.app_error'); + done(); + } + ); + }); + }); + + /*it('License.uploadLicenseFile', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().uploadLicenseFile( + 'form data', + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.context.permissions.app_error'); + done(); + } + ); + }); + });*/ + + // TODO XXX FIX ME - this test depends on make dist + + // it('General.getTranslations', function(done) { + // TestHelper.initBasic(() => { + // TestHelper.basicClient().getTranslations( + // 'http://localhost:8065/static/i18n/es.json', + // function(data) { + // assert.equal(data['login.or'], 'o'); + // done(); + // }, + // function(err) { + // done(new Error(err.message)); + // } + // ); + // }); + // }); + + it('File.getFileInfo', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + TestHelper.basicClient().getFileInfo( + `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`, + function(data) { + assert.equal(data.filename, 'filename.txt'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('File.getPublicLink', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var data = {}; + data.channel_id = TestHelper.basicChannel().id; + data.user_id = TestHelper.basicUser().id; + data.filename = `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`; + + TestHelper.basicClient().getPublicLink( + data, + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.file.get_public_link.disabled.app_error'); + done(); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_hooks.test.jsx b/webapp/tests/client_hooks.test.jsx new file mode 100644 index 000000000..0cad22153 --- /dev/null +++ b/webapp/tests/client_hooks.test.jsx @@ -0,0 +1,139 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Hooks', function() { + this.timeout(100000); + + it('addIncomingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var hook = {}; + hook.channel_id = TestHelper.basicChannel().id; + hook.description = 'desc'; + hook.display_name = 'Unit Test'; + + TestHelper.basicClient().addIncomingHook( + hook, + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.create_incoming.disabled.app_errror'); + done(); + } + ); + }); + }); + + it('deleteIncomingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().deleteIncomingHook( + TestHelper.generateId(), + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.delete_incoming.disabled.app_errror'); + done(); + } + ); + }); + }); + + it('listIncomingHooks', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().listIncomingHooks( + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.get_incoming.disabled.app_error'); + done(); + } + ); + }); + }); + + it('addOutgoingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var hook = {}; + hook.channel_id = TestHelper.basicChannel().id; + hook.description = 'desc'; + hook.display_name = 'Unit Test'; + + TestHelper.basicClient().addOutgoingHook( + hook, + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.create_outgoing.disabled.app_error'); + done(); + } + ); + }); + }); + + it('deleteOutgoingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().deleteOutgoingHook( + TestHelper.generateId(), + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.delete_outgoing.disabled.app_error'); + done(); + } + ); + }); + }); + + it('listOutgoingHooks', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().listOutgoingHooks( + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.get_outgoing.disabled.app_error'); + done(); + } + ); + }); + }); + + it('regenOutgoingHookToken', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().regenOutgoingHookToken( + TestHelper.generateId(), + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.regen_outgoing_token.disabled.app_error'); + done(); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_oauth.test.jsx b/webapp/tests/client_oauth.test.jsx new file mode 100644 index 000000000..df2fc665b --- /dev/null +++ b/webapp/tests/client_oauth.test.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.OAuth', function() { + this.timeout(100000); + + it('registerOAuthApp', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var app = {}; + app.name = 'test'; + app.homepage = 'homepage'; + app.description = 'desc'; + app.callback_urls = ''; + + TestHelper.basicClient().registerOAuthApp( + app, + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.oauth.register_oauth_app.turn_off.app_error'); + done(); + } + ); + }); + }); + + it('allowOAuth2', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + TestHelper.basicClient().allowOAuth2( + 'GET', + '123456', + 'http://nowhere.com', + 'state', + 'scope', + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.oauth.allow_oauth.turn_off.app_error'); + done(); + } + ); + }); + }); +}); diff --git a/webapp/tests/client_post.test.jsx b/webapp/tests/client_post.test.jsx new file mode 100644 index 000000000..db48e4000 --- /dev/null +++ b/webapp/tests/client_post.test.jsx @@ -0,0 +1,207 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Posts', function() { + this.timeout(100000); + + it('createPost', function(done) { + TestHelper.initBasic(() => { + var post = TestHelper.fakePost(); + post.channel_id = TestHelper.basicChannel().id; + + TestHelper.basicClient().createPost( + post, + function(data) { + assert.equal(data.id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPostById', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getPostById( + TestHelper.basicPost().id, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPost', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getPost( + TestHelper.basicChannel().id, + TestHelper.basicPost().id, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updatePost', function(done) { + TestHelper.initBasic(() => { + var post = TestHelper.basicPost(); + post.message = 'new message'; + post.channel_id = TestHelper.basicChannel().id; + + TestHelper.basicClient().updatePost( + post, + function(data) { + assert.equal(data.id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('deletePost', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().deletePost( + TestHelper.basicChannel().id, + TestHelper.basicPost().id, + function(data) { + assert.equal(data.id, TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('searchPost', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().search( + 'unit test', + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPostsPage', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getPostsPage( + TestHelper.basicChannel().id, + 0, + 10, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPosts', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getPosts( + TestHelper.basicChannel().id, + 0, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPostsBefore', function(done) { + TestHelper.initBasic(() => { + var post = TestHelper.fakePost(); + post.channel_id = TestHelper.basicChannel().id; + + TestHelper.basicClient().createPost( + post, + function(rpost) { + TestHelper.basicClient().getPostsBefore( + TestHelper.basicChannel().id, + rpost.id, + 0, + 10, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPostsAfter', function(done) { + TestHelper.initBasic(() => { + var post = TestHelper.fakePost(); + post.channel_id = TestHelper.basicChannel().id; + + TestHelper.basicClient().createPost( + post, + function(rpost) { + TestHelper.basicClient().getPostsAfter( + TestHelper.basicChannel().id, + TestHelper.basicPost().id, + 0, + 10, + function(data) { + assert.equal(data.order[0], rpost.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_preferences.test.jsx b/webapp/tests/client_preferences.test.jsx new file mode 100644 index 000000000..987728704 --- /dev/null +++ b/webapp/tests/client_preferences.test.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Preferences', function() { + this.timeout(100000); + + it('getAllPreferences', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getAllPreferences( + function(data) { + assert.equal(data[0].category, 'tutorial_step'); + assert.equal(data[0].user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('savePreferences', function(done) { + TestHelper.initBasic(() => { + var perf = {}; + perf.user_id = TestHelper.basicUser().id; + perf.category = 'test'; + perf.name = 'name'; + perf.value = 'value'; + + var perfs = []; + perfs.push(perf); + + TestHelper.basicClient().savePreferences( + perfs, + function(data) { + assert.equal(data, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPreferenceCategory', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getPreferenceCategory( + 'tutorial_step', + function(data) { + assert.equal(data[0].category, 'tutorial_step'); + assert.equal(data[0].user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx new file mode 100644 index 000000000..e8b71d2f8 --- /dev/null +++ b/webapp/tests/client_team.test.jsx @@ -0,0 +1,235 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.Team', function() { + this.timeout(100000); + + it('findTeamByName', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().findTeamByName( + TestHelper.basicTeam().name, + function(data) { + assert.equal(data, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('signupTeam', function(done) { + var client = TestHelper.createClient(); + var email = TestHelper.fakeEmail(); + + client.signupTeam( + email, + function(data) { + assert.equal(data.email, email); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('createTeamFromSignup', function(done) { + var client = TestHelper.createClient(); + var email = TestHelper.fakeEmail(); + + client.signupTeam( + email, + function(data) { + var teamSignup = {}; + teamSignup.invites = []; + teamSignup.data = decodeURIComponent(data.follow_link.split('&h=')[0].replace('/signup_team_complete/?d=', '')); + teamSignup.hash = decodeURIComponent(data.follow_link.split('&h=')[1]); + + teamSignup.user = TestHelper.fakeUser(); + teamSignup.team = TestHelper.fakeTeam(); + teamSignup.team.email = teamSignup.user.email; + + client.createTeamFromSignup( + teamSignup, + function(data2) { + assert.equal(data2.team.id.length > 0, true); + assert.equal(data2.user.id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('createTeam', function(done) { + var client = TestHelper.createClient(); + var team = TestHelper.fakeTeam(); + client.createTeam( + team, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.name, team.name); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('getAllTeams', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getAllTeams( + function(data) { + assert.equal(data[TestHelper.basicTeam().id].name, TestHelper.basicTeam().name); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getAllTeamListings', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getAllTeamListings( + function(data) { + console.log(data); + assert.equal(data != null, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getMyTeam', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getMyTeam( + function(data) { + assert.equal(data.name, TestHelper.basicTeam().name); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('GetTeamMembers', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getTeamMembers( + TestHelper.basicTeam().id, + function(data) { + assert.equal(data.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('inviteMembers', function(done) { + TestHelper.initBasic(() => { + var data = {}; + data.invites = []; + var invite = {}; + invite.email = TestHelper.fakeEmail(); + invite.firstName = 'first'; + invite.lastName = 'last'; + data.invites.push(invite); + + TestHelper.basicClient().inviteMembers( + data, + function(dataBack) { + assert.equal(dataBack.invites.length, 1); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateTeam', function(done) { + TestHelper.initBasic(() => { + var team = TestHelper.basicTeam(); + team.display_name = 'test_updated'; + + TestHelper.basicClient().updateTeam( + team, + function(data) { + assert.equal(data.display_name, 'test_updated'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('addUserToTeam', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().createUser( + TestHelper.fakeUser(), + function(user2) { + TestHelper.basicClient().addUserToTeam( + user2.id, + function(data) { + assert.equal(data.user_id, user2.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getInviteInfo', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getInviteInfo( + TestHelper.basicTeam().invite_id, + function(data) { + assert.equal(data.display_name.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); +}); + diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx new file mode 100644 index 000000000..e0ead2de9 --- /dev/null +++ b/webapp/tests/client_user.test.jsx @@ -0,0 +1,550 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +describe('Client.User', function() { + this.timeout(100000); + + it('getMe', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getMe( + function(data) { + assert.equal(data.id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getInitialLoad', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getInitialLoad( + function(data) { + assert.equal(data.user.id.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('createUser', function(done) { + var client = TestHelper.createClient(); + var user = TestHelper.fakeUser(); + client.createUser( + user, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.email, user.email); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('loginByEmail', function(done) { + var client = TestHelper.createClient(); + var user = TestHelper.fakeUser(); + client.createUser( + user, + function() { + client.login( + user.email, + null, + user.password, + null, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.email, user.email); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('loginByUsername', function(done) { + var client = TestHelper.createClient(); + var user = TestHelper.fakeUser(); + client.createUser( + user, + function() { + client.login( + null, + user.username, + user.password, + null, + function(data) { + assert.equal(data.id.length > 0, true); + assert.equal(data.email, user.email); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('loginByLdap', function(done) { + var client = TestHelper.createClient(); + client.enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var user = TestHelper.fakeUser(); + client.createUser( + user, + function() { + client.loginByLdap( + user.username, + user.password, + null, + function() { + done(new Error()); + }, + function(err) { + assert.equal(err.id, 'api.user.login_ldap.disabled.app_error'); + done(); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + + it('updateUser', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + user.nickname = 'updated'; + + TestHelper.basicClient().updateUser( + user, + function(data) { + assert.equal(data.nickname, 'updated'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updatePassword', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + TestHelper.basicClient().updatePassword( + user.id, + user.password, + 'update_password', + function(data) { + assert.equal(data.user_id, user.id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateUserNotifyProps', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + var notifyProps = { + all: 'true', + channel: 'true', + desktop: 'all', + desktop_sound: 'true', + email: 'false', + first_name: 'false', + mention_keys: '', + user_id: user.id + }; + + TestHelper.basicClient().updateUserNotifyProps( + notifyProps, + function(data) { + assert.equal(data.notify_props.email, 'false'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateRoles', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + TestHelper.basicClient().updateRoles( + user.id, + '', + function(data) { + assert.equal(data.roles, ''); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateActive', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + TestHelper.basicClient().updateActive( + user.id, + false, + function(data) { + assert.equal(data.last_activity_at > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('sendPasswordReset', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + TestHelper.basicClient().sendPasswordReset( + user.email, + function(data) { + assert.equal(data.email, user.email); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('resetPassword', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + TestHelper.basicClient().resetPassword( + '', + 'new_password', + function() { + throw Error('shouldnt work'); + }, + function(err) { + // this should fail since you're not a system admin + assert.equal(err.id, 'api.context.invalid_param.app_error'); + done(); + } + ); + }); + }); + + it('emailToOAuth', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var user = TestHelper.basicUser(); + + TestHelper.basicClient().emailToOAuth( + user.email, + 'new_password', + 'gitlab', + function() { + throw Error('shouldnt work'); + }, + function(err) { + // this should fail since you're not a system admin + assert.equal(err.id, 'api.user.check_user_password.invalid.app_error'); + done(); + } + ); + }); + }); + + it('oauthToEmail', function(done) { + TestHelper.initBasic(() => { + var user = TestHelper.basicUser(); + + TestHelper.basicClient().oauthToEmail( + user.email, + 'new_password', + function(data) { + assert.equal(data.follow_link.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('emailToLdap', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var user = TestHelper.basicUser(); + + TestHelper.basicClient().emailToLdap( + user.email, + user.password, + 'unknown_id', + 'unknown_pwd', + function() { + throw Error('shouldnt work'); + }, + function(err) { + assert.equal(err.id, 'ent.ldap.do_login.licence_disable.app_error'); + done(); + } + ); + }); + }); + + it('ldapToEmail', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + var user = TestHelper.basicUser(); + + TestHelper.basicClient().ldapToEmail( + user.email, + 'new_password', + 'new_password', + function() { + throw Error('shouldnt work'); + }, + function(err) { + assert.equal(err.id, 'api.user.ldap_to_email.not_ldap_account.app_error'); + done(); + } + ); + }); + }); + + it('logout', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().logout( + function(data) { + assert.equal(data.user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('checkMfa', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().checkMfa( + 'email', + TestHelper.generateId(), + function(data) { + assert.equal(data.mfa_required, 'false'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getSessions', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getSessions( + TestHelper.basicUser().id, + function(data) { + assert.equal(data[0].user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('revokeSession', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getSessions( + TestHelper.basicUser().id, + function(sessions) { + TestHelper.basicClient().revokeSession( + sessions[0].id, + function(data) { + assert.equal(data.id, sessions[0].id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getAudits', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getAudits( + TestHelper.basicUser().id, + function(data) { + assert.equal(data[0].user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getDirectProfiles', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getDirectProfiles( + function(data) { + assert.equal(Object.keys(data).length === 0, true); + done(); + }, + function(err) { + done(new Error(err.getDirectProfiles)); + } + ); + }); + }); + + it('getProfiles', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getProfiles( + function(data) { + assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getProfilesForTeam', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getProfilesForTeam( + TestHelper.basicTeam().id, + function(data) { + assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getStatuses', function(done) { + TestHelper.initBasic(() => { + var ids = []; + ids.push(TestHelper.basicUser().id); + + TestHelper.basicClient().getStatuses( + ids, + function(data) { + assert.equal(data[TestHelper.basicUser().id], 'online'); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('verifyEmail', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().verifyEmail( + 'junk', + 'junk', + function() { + done(new Error('should be invalid')); + }, + function(err) { + assert.equal(err.id, 'api.context.invalid_param.app_error'); + done(); + } + ); + }); + }); + + it('resendVerification', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().resendVerification( + TestHelper.basicUser().email, + function() { + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('updateMfa', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + TestHelper.basicClient().updateMfa( + 'junk', + true, + function() { + done(new Error('not enabled')); + }, + function(err) { + assert.equal(err.id, 'ent.mfa.license_disable.app_error'); + done(); + } + ); + }); + }); +}); diff --git a/webapp/tests/spinner_button.test.jsx b/webapp/tests/spinner_button.test.jsx new file mode 100644 index 000000000..0e282e0ee --- /dev/null +++ b/webapp/tests/spinner_button.test.jsx @@ -0,0 +1,24 @@ +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ + +var jsdom = require('mocha-jsdom'); +var assert = require('assert'); +import TestUtils from 'react-addons-test-utils'; +import SpinnerButton from '../components/spinner_button.jsx'; +import React from 'react'; + +describe('SpinnerButton', function() { + jsdom(); + + it('check props', function() { + const spinner = TestUtils.renderIntoDocument( + + ); + + assert.equal(spinner.props.spinning, false, 'should start in the default false state'); + }); +}); \ No newline at end of file diff --git a/webapp/tests/test_helper.jsx b/webapp/tests/test_helper.jsx new file mode 100644 index 000000000..385279360 --- /dev/null +++ b/webapp/tests/test_helper.jsx @@ -0,0 +1,183 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ +/* eslint-disable global-require */ +/* eslint-disable func-names */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable no-unreachable */ +/* eslint-disable new-cap */ + +import Client from '../client/client.jsx'; +import jqd from 'jquery-deferred'; + +class TestHelperClass { + basicClient = () => { + return this.basicc; + } + + basicTeam = () => { + return this.basict; + } + + basicUser = () => { + return this.basicu; + } + + basicChannel = () => { + return this.basicch; + } + + basicPost = () => { + return this.basicp; + } + + generateId = () => { + // implementation taken from http://stackoverflow.com/a/2117523 + var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + + id = id.replace(/[xy]/g, function replaceRandom(c) { + var r = Math.floor(Math.random() * 16); + + var v; + if (c === 'x') { + v = r; + } else { + v = r & 0x3 | 0x8; + } + + return v.toString(16); + }); + + return 'uid' + id; + } + + createClient() { + var c = new Client(); + c.setUrl('http://localhost:8065'); + c.useHeaderToken(); + c.enableLogErrorsToConsole(true); + return c; + } + + fakeEmail = () => { + return 'success' + this.generateId() + '@simulator.amazonses.com'; + } + + fakeUser = () => { + var user = {}; + user.email = this.fakeEmail(); + user.allow_marketing = true; + user.password = 'password1'; + user.username = this.generateId(); + return user; + } + + fakeTeam = () => { + var team = {}; + team.name = this.generateId(); + team.display_name = `Unit Test ${team.name}`; + team.type = 'O'; + team.email = this.fakeEmail(); + team.allowed_domains = ''; + return team; + } + + fakeChannel = () => { + var channel = {}; + channel.name = this.generateId(); + channel.display_name = `Unit Test ${channel.name}`; + channel.type = 'O'; // open channel + return channel; + } + + fakePost = () => { + var post = {}; + post.message = `Unit Test ${this.generateId()}`; + return post; + } + + initBasic = (callback) => { + this.basicc = this.createClient(); + + var d1 = jqd.Deferred(); + var email = this.fakeEmail(); + var outer = this; // eslint-disable-line consistent-this + + this.basicClient().signupTeam( + email, + function(rsignUp) { + var teamSignup = {}; + teamSignup.invites = []; + teamSignup.data = decodeURIComponent(rsignUp.follow_link.split('&h=')[0].replace('/signup_team_complete/?d=', '')); + teamSignup.hash = decodeURIComponent(rsignUp.follow_link.split('&h=')[1]); + + teamSignup.user = outer.fakeUser(); + teamSignup.team = outer.fakeTeam(); + teamSignup.team.email = email; + teamSignup.user.email = email; + var password = teamSignup.user.password; + + outer.basicClient().createTeamFromSignup( + teamSignup, + function(rteamSignup) { + outer.basict = rteamSignup.team; + outer.basicu = rteamSignup.user; + outer.basicu.password = password; + outer.basicClient().setTeamId(outer.basict.id); + outer.basicClient().login( + rteamSignup.user.email, + null, + password, + null, + function() { + outer.basicClient().useHeaderToken(); + var channel = outer.fakeChannel(); + channel.team_id = outer.basicTeam().id; + outer.basicClient().createChannel( + channel, + function(rchannel) { + outer.basicch = rchannel; + var post = outer.fakePost(); + post.channel_id = rchannel.id; + + outer.basicClient().createPost( + post, + function(rpost) { + outer.basicp = rpost; + d1.resolve(); + }, + function(err) { + throw err; + } + ); + }, + function(err) { + throw err; + } + ); + }, + function(err) { + throw err; + } + ); + }, + function(err) { + throw err; + } + ); + }, + function(err) { + throw err; + } + ); + + jqd.when(d1).done(() => { + callback(); + }); + } +} + +var TestHelper = new TestHelperClass(); +export default TestHelper; diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 80a08dc21..189b159e8 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import $ from 'jquery'; -import * as client from './client.jsx'; +import Client from './web_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'; @@ -11,6 +11,7 @@ import PreferenceStore from 'stores/preference_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as utils from './utils.jsx'; +import ErrorStore from 'stores/error_store.jsx'; import Constants from './constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -19,6 +20,8 @@ const StatTypes = Constants.StatTypes; // Used to track in progress async calls const callTracker = {}; +const ASYNC_CLIENT_TIMEOUT = 5000; + export function dispatchError(err, method) { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_ERROR, @@ -36,7 +39,7 @@ function isCallInProgress(callName) { return false; } - if (utils.getTimestamp() - callTracker[callName] > 5000) { + if (utils.getTimestamp() - callTracker[callName] > ASYNC_CLIENT_TIMEOUT) { //console.log('AsyncClient call ' + callName + ' expired after more than 5 seconds'); return false; } @@ -51,16 +54,12 @@ export function getChannels(checkVersion) { callTracker.getChannels = utils.getTimestamp(); - return client.getChannels( - (data, textStatus, xhr) => { + return Client.getChannels( + (data) => { callTracker.getChannels = 0; - if (xhr.status === 304 || !data) { - return; - } - if (checkVersion) { - var serverVersion = xhr.getResponseHeader('X-Version-ID'); + var serverVersion = Client.getServerVersion(); if (serverVersion !== BrowserStore.getLastServerVersion()) { if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') { @@ -93,14 +92,10 @@ export function getChannel(id) { callTracker['getChannel' + id] = utils.getTimestamp(); - client.getChannel(id, - (data, textStatus, xhr) => { + Client.getChannel(id, + (data) => { callTracker['getChannel' + id] = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNEL, channel: data.channel, @@ -131,13 +126,17 @@ export function updateLastViewedAt(id) { } callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp(); - client.updateLastViewedAt( + Client.updateLastViewedAt( channelId, () => { callTracker.updateLastViewed = 0; + ErrorStore.clearLastError(); + ErrorStore.emitChange(); }, (err) => { callTracker.updateLastViewed = 0; + var count = ErrorStore.getConnectionErrorCount(); + ErrorStore.setConnectionErrorCount(count + 1); dispatchError(err, 'updateLastViewedAt'); } ); @@ -150,21 +149,17 @@ export function getMoreChannels(force) { if (ChannelStore.getMoreAll().loading || force) { callTracker.getMoreChannels = utils.getTimestamp(); - client.getMoreChannels( - function getMoreChannelsSuccess(data, textStatus, xhr) { + Client.getMoreChannels( + (data) => { callTracker.getMoreChannels = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_MORE_CHANNELS, channels: data.channels, members: data.members }); }, - function getMoreChannelsFailure(err) { + (err) => { callTracker.getMoreChannels = 0; dispatchError(err, 'getMoreChannels'); } @@ -187,16 +182,12 @@ export function getChannelExtraInfo(id, memberLimit) { callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp(); - client.getChannelExtraInfo( + Client.getChannelExtraInfo( channelId, memberLimit, - (data, textStatus, xhr) => { + (data) => { callTracker['getChannelExtraInfo_' + channelId] = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNEL_EXTRA_INFO, extra_info: data @@ -210,53 +201,90 @@ export function getChannelExtraInfo(id, memberLimit) { } } +export function getTeamMembers(teamId) { + if (isCallInProgress('getTeamMembers')) { + return; + } + + callTracker.getTeamMembers = utils.getTimestamp(); + Client.getTeamMembers( + teamId, + (data) => { + callTracker.getTeamMembers = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MEMBERS_FOR_TEAM, + team_members: data + }); + }, + (err) => { + callTracker.getTeamMembers = 0; + dispatchError(err, 'getTeamMembers'); + } + ); +} + export function getProfiles() { if (isCallInProgress('getProfiles')) { return; } callTracker.getProfiles = utils.getTimestamp(); - client.getProfiles( - function getProfilesSuccess(data, textStatus, xhr) { + Client.getProfiles( + (data) => { callTracker.getProfiles = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_PROFILES, profiles: data }); }, - function getProfilesFailure(err) { + (err) => { callTracker.getProfiles = 0; dispatchError(err, 'getProfiles'); } ); } +export function getDirectProfiles() { + if (isCallInProgress('getDirectProfiles')) { + return; + } + + callTracker.getDirectProfiles = utils.getTimestamp(); + Client.getDirectProfiles( + (data) => { + callTracker.getDirectProfiles = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_DIRECT_PROFILES, + profiles: data + }); + }, + (err) => { + callTracker.getDirectProfiles = 0; + dispatchError(err, 'getDirectProfiles'); + } + ); +} + export function getSessions() { if (isCallInProgress('getSessions')) { return; } callTracker.getSessions = utils.getTimestamp(); - client.getSessions( + Client.getSessions( UserStore.getCurrentId(), - function getSessionsSuccess(data, textStatus, xhr) { + (data) => { callTracker.getSessions = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SESSIONS, sessions: data }); }, - function getSessionsFailure(err) { + (err) => { callTracker.getSessions = 0; dispatchError(err, 'getSessions'); } @@ -269,21 +297,17 @@ export function getAudits() { } callTracker.getAudits = utils.getTimestamp(); - client.getAudits( + Client.getAudits( UserStore.getCurrentId(), - function getAuditsSuccess(data, textStatus, xhr) { + (data) => { callTracker.getAudits = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_AUDITS, audits: data }); }, - function getAuditsFailure(err) { + (err) => { callTracker.getAudits = 0; dispatchError(err, 'getAudits'); } @@ -296,14 +320,10 @@ export function getLogs() { } callTracker.getLogs = utils.getTimestamp(); - client.getLogs( - (data, textStatus, xhr) => { + Client.getLogs( + (data) => { callTracker.getLogs = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_LOGS, logs: data @@ -322,14 +342,10 @@ export function getServerAudits() { } callTracker.getServerAudits = utils.getTimestamp(); - client.getServerAudits( - (data, textStatus, xhr) => { + Client.getServerAudits( + (data) => { callTracker.getServerAudits = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SERVER_AUDITS, audits: data @@ -348,14 +364,10 @@ export function getComplianceReports() { } callTracker.getComplianceReports = utils.getTimestamp(); - client.getComplianceReports( - (data, textStatus, xhr) => { + Client.getComplianceReports( + (data) => { callTracker.getComplianceReports = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SERVER_COMPLIANCE_REPORTS, complianceReports: data @@ -374,14 +386,10 @@ export function getConfig() { } callTracker.getConfig = utils.getTimestamp(); - client.getConfig( - (data, textStatus, xhr) => { + Client.getConfig( + (data) => { callTracker.getConfig = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CONFIG, config: data @@ -400,14 +408,10 @@ export function getAllTeams() { } callTracker.getAllTeams = utils.getTimestamp(); - client.getAllTeams( - (data, textStatus, xhr) => { + Client.getAllTeams( + (data) => { callTracker.getAllTeams = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_ALL_TEAMS, teams: data @@ -420,27 +424,45 @@ export function getAllTeams() { ); } +export function getAllTeamListings() { + if (isCallInProgress('getAllTeamListings')) { + return; + } + + callTracker.getAllTeamListings = utils.getTimestamp(); + Client.getAllTeamListings( + (data) => { + callTracker.getAllTeamListings = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_ALL_TEAM_LISTINGS, + teams: data + }); + }, + (err) => { + callTracker.getAllTeams = 0; + dispatchError(err, 'getAllTeamListings'); + } + ); +} + export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; } callTracker['search_' + String(terms)] = utils.getTimestamp(); - client.search( + Client.search( terms, - function searchSuccess(data, textStatus, xhr) { + (data) => { callTracker['search_' + String(terms)] = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SEARCH, results: data }); }, - function searchFailure(err) { + (err) => { callTracker['search_' + String(terms)] = 0; dispatchError(err, 'search'); } @@ -478,15 +500,11 @@ export function getPostsPage(id, maxPosts) { if (channelId != null) { callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); - client.getPostsPage( + Client.getPostsPage( channelId, 0, numPosts, - (data, textStatus, xhr) => { - if (xhr.status === 304 || !data) { - return; - } - + (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POSTS, id: channelId, @@ -536,14 +554,10 @@ export function getPosts(id) { callTracker['getPosts_' + channelId] = utils.getTimestamp(); - client.getPosts( + Client.getPosts( channelId, latestPostTime, - (data, textStatus, xhr) => { - if (xhr.status === 304 || !data) { - return; - } - + (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POSTS, id: channelId, @@ -573,16 +587,12 @@ export function getPostsBefore(postId, offset, numPost) { return; } - client.getPostsBefore( + Client.getPostsBefore( channelId, postId, offset, numPost, - (data, textStatus, xhr) => { - if (xhr.status === 304 || !data) { - return; - } - + (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POSTS, id: channelId, @@ -612,16 +622,12 @@ export function getPostsAfter(postId, offset, numPost) { return; } - client.getPostsAfter( + Client.getPostsAfter( channelId, postId, offset, numPost, - (data, textStatus, xhr) => { - if (xhr.status === 304 || !data) { - return; - } - + (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POSTS, id: channelId, @@ -647,14 +653,10 @@ export function getMe() { } callTracker.getMe = utils.getTimestamp(); - return client.getMe( - (data, textStatus, xhr) => { + return Client.getMe( + (data) => { callTracker.getMe = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_ME, me: data @@ -684,14 +686,10 @@ export function getStatuses() { } callTracker.getStatuses = utils.getTimestamp(); - client.getStatuses(teammateIds, - (data, textStatus, xhr) => { + Client.getStatuses(teammateIds, + (data) => { callTracker.getStatuses = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_STATUSES, statuses: data @@ -710,20 +708,16 @@ export function getMyTeam() { } callTracker.getMyTeam = utils.getTimestamp(); - return client.getMyTeam( - function getMyTeamSuccess(data, textStatus, xhr) { + return Client.getMyTeam( + (data) => { callTracker.getMyTeam = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, - function getMyTeamFailure(err) { + (err) => { callTracker.getMyTeam = 0; dispatchError(err, 'getMyTeam'); } @@ -736,14 +730,10 @@ export function getAllPreferences() { } callTracker.getAllPreferences = utils.getTimestamp(); - client.getAllPreferences( - (data, textStatus, xhr) => { + Client.getAllPreferences( + (data) => { callTracker.getAllPreferences = 0; - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_PREFERENCES, preferences: data @@ -768,15 +758,13 @@ export function savePreference(category, name, value, success, error) { } export function savePreferences(preferences, success, error) { - client.savePreferences( + Client.savePreferences( preferences, - (data, textStatus, xhr) => { - if (xhr.status !== 304) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PREFERENCES, - preferences - }); - } + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PREFERENCES, + preferences + }); if (success) { success(data); @@ -793,12 +781,12 @@ export function savePreferences(preferences, success, error) { } export function getSuggestedCommands(command, suggestionId, component) { - client.listCommands( + Client.listCommands( (data) => { var matches = []; data.forEach((cmd) => { if (('/' + cmd.trigger).indexOf(command) === 0) { - let s = '/' + cmd.trigger; + const s = '/' + cmd.trigger; let hint = ''; if (cmd.auto_complete_hint && cmd.auto_complete_hint.length !== 0) { hint = cmd.auto_complete_hint; @@ -842,7 +830,7 @@ export function getFileInfo(filename) { callTracker[callName] = utils.getTimestamp(); - client.getFileInfo( + Client.getFileInfo( filename, (data) => { callTracker[callName] = 0; @@ -870,7 +858,7 @@ export function getStandardAnalytics(teamId) { callTracker[callName] = utils.getTimestamp(); - client.getAnalytics( + Client.getAnalytics( 'standard', teamId, (data) => { @@ -923,7 +911,7 @@ export function getAdvancedAnalytics(teamId) { callTracker[callName] = utils.getTimestamp(); - client.getAnalytics( + Client.getAnalytics( 'extra_counts', teamId, (data) => { @@ -980,7 +968,7 @@ export function getPostsPerDayAnalytics(teamId) { callTracker[callName] = utils.getTimestamp(); - client.getAnalytics( + Client.getAnalytics( 'post_counts_day', teamId, (data) => { @@ -1014,7 +1002,7 @@ export function getUsersPerDayAnalytics(teamId) { callTracker[callName] = utils.getTimestamp(); - client.getAnalytics( + Client.getAnalytics( 'user_counts_with_posts_day', teamId, (data) => { @@ -1048,7 +1036,7 @@ export function getRecentAndNewUsersAnalytics(teamId) { callTracker[callName] = utils.getTimestamp(); - client.getProfilesForTeam( + Client.getProfilesForTeam( teamId, (users) => { const stats = {}; @@ -1129,7 +1117,7 @@ export function listIncomingHooks() { callTracker.listIncomingHooks = utils.getTimestamp(); - client.listIncomingHooks( + Client.listIncomingHooks( (data) => { callTracker.listIncomingHooks = 0; @@ -1152,7 +1140,7 @@ export function listOutgoingHooks() { callTracker.listOutgoingHooks = utils.getTimestamp(); - client.listOutgoingHooks( + Client.listOutgoingHooks( (data) => { callTracker.listOutgoingHooks = 0; @@ -1169,7 +1157,7 @@ export function listOutgoingHooks() { } export function addIncomingHook(hook, success, error) { - client.addIncomingHook( + Client.addIncomingHook( hook, (data) => { AppDispatcher.handleServerAction({ @@ -1192,7 +1180,7 @@ export function addIncomingHook(hook, success, error) { } export function addOutgoingHook(hook, success, error) { - client.addOutgoingHook( + Client.addOutgoingHook( hook, (data) => { AppDispatcher.handleServerAction({ @@ -1215,8 +1203,8 @@ export function addOutgoingHook(hook, success, error) { } export function deleteIncomingHook(id) { - client.deleteIncomingHook( - {id}, + Client.deleteIncomingHook( + id, () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_INCOMING_WEBHOOK, @@ -1230,8 +1218,8 @@ export function deleteIncomingHook(id) { } export function deleteOutgoingHook(id) { - client.deleteOutgoingHook( - {id}, + Client.deleteOutgoingHook( + id, () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_OUTGOING_WEBHOOK, @@ -1245,8 +1233,8 @@ export function deleteOutgoingHook(id) { } export function regenOutgoingHookToken(id) { - client.regenOutgoingHookToken( - {id}, + Client.regenOutgoingHookToken( + id, (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.UPDATED_OUTGOING_WEBHOOK, @@ -1266,7 +1254,7 @@ export function listTeamCommands() { callTracker.listTeamCommands = utils.getTimestamp(); - client.listTeamCommands( + Client.listTeamCommands( (data) => { callTracker.listTeamCommands = 0; @@ -1283,7 +1271,7 @@ export function listTeamCommands() { } export function addCommand(command, success, error) { - client.addCommand( + Client.addCommand( command, (data) => { AppDispatcher.handleServerAction({ @@ -1306,8 +1294,8 @@ export function addCommand(command, success, error) { } export function deleteCommand(id) { - client.deleteCommand( - {id}, + Client.deleteCommand( + id, () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_COMMAND, @@ -1321,8 +1309,8 @@ export function deleteCommand(id) { } export function regenCommandToken(id) { - client.regenCommandToken( - {id}, + Client.regenCommandToken( + id, (data) => { AppDispatcher.handleServerAction({ type: ActionTypes.UPDATED_COMMAND, diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index ddd615581..1d18e26ba 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -9,6 +9,7 @@ import UserProfile from 'components/user_profile.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import Constants from 'utils/constants.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; +import Client from 'utils/web_client.jsx'; import React from 'react'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl'; @@ -40,7 +41,7 @@ export function createDMIntroMessage(channel) {
    diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx deleted file mode 100644 index 687d47da4..000000000 --- a/webapp/utils/client.jsx +++ /dev/null @@ -1,1759 +0,0 @@ -// See License.txt for license information. - -import BrowserStore from 'stores/browser_store.jsx'; -import $ from 'jquery'; - -import {browserHistory} from 'react-router'; - -let translations = { - connectionError: 'There appears to be a problem with your internet connection.', - unknownError: 'We received an unexpected status code from the server.' -}; - -export function setTranslations(messages) { - translations = messages; -} - -export function track(category, action, label, property, value) { - global.window.analytics.track(action, {category, label, property, value}); -} - -export function trackPage() { - global.window.analytics.page(); -} - -function handleError(methodName, xhr, status, err) { - var e = null; - try { - e = JSON.parse(xhr.responseText); - } catch (parseError) { - e = null; - } - - var msg = ''; - - if (e) { - msg = 'method=' + methodName + ' msg=' + e.message + ' detail=' + e.detailed_error + ' rid=' + e.request_id; - } else { - msg = 'method=' + methodName + ' status=' + status + ' statusCode=' + xhr.status + ' err=' + err; - - if (xhr.status === 0) { - e = {message: translations.connectionError}; - } else { - e = {message: translations.unknownError + ' (' + xhr.status + ')'}; - } - } - - console.error(msg); //eslint-disable-line no-console - console.error(e); //eslint-disable-line no-console - - track('api', 'api_weberror', methodName, 'message', msg); - - if (xhr.status === 401) { - const team = window.location.pathname.split('/')[1]; - browserHistory.push('/' + team + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); - } - - return e; -} - -export function getTranslations(url, success, error) { - $.ajax({ - url: url, - dataType: 'json', - success, - error: function onError(xhr, status, err) { - var e = handleError('getTranslations', xhr, status, err); - error(e); - } - }); -} - -export function createTeamFromSignup(teamSignup, success, error) { - $.ajax({ - url: '/api/v1/teams/create_from_signup', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(teamSignup), - success, - error: function onError(xhr, status, err) { - var e = handleError('createTeamFromSignup', xhr, status, err); - error(e); - } - }); -} - -export function createTeamWithLdap(teamSignup, success, error) { - $.ajax({ - url: '/api/v1/teams/create_with_ldap', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(teamSignup), - success, - error: function onError(xhr, status, err) { - var e = handleError('createTeamFromSignup', xhr, status, err); - error(e); - } - }); -} - -export function createTeamWithSSO(team, service, success, error) { - $.ajax({ - url: '/api/v1/teams/create_with_sso/' + service, - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(team), - success, - error: function onError(xhr, status, err) { - var e = handleError('createTeamWithSSO', xhr, status, err); - error(e); - } - }); -} - -export function createUser(user, data, emailHash, success, error) { - $.ajax({ - url: '/api/v1/users/create?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash), - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(user), - success, - error: function onError(xhr, status, err) { - var e = handleError('createUser', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_create', user.team_id, 'email', user.email); -} - -export function updateUser(user, success, error) { - $.ajax({ - url: '/api/v1/users/update', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(user), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateUser', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_update'); -} - -export function updatePassword(data, success, error) { - $.ajax({ - url: '/api/v1/users/newpassword', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('newPassword', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_newpassword'); -} - -export function updateUserNotifyProps(data, success, error) { - $.ajax({ - url: '/api/v1/users/update_notify', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateUserNotifyProps', xhr, status, err); - error(e); - } - }); -} - -export function updateRoles(data, success, error) { - $.ajax({ - url: '/api/v1/users/update_roles', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateRoles', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_update_roles'); -} - -export function updateActive(userId, active, success, error) { - var data = {}; - data.user_id = userId; - data.active = '' + active; - - $.ajax({ - url: '/api/v1/users/update_active', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateActive', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_update_roles'); -} - -export function sendPasswordReset(data, success, error) { - $.ajax({ - url: '/api/v1/users/send_password_reset', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('sendPasswordReset', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_send_password_reset'); -} - -export function resetPassword(data, success, error) { - $.ajax({ - url: '/api/v1/users/reset_password', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('resetPassword', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_reset_password'); -} - -export function emailToOAuth(data, success, error) { - $.ajax({ - url: '/api/v1/users/claim/email_to_oauth', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('emailToOAuth', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_email_to_oauth'); -} - -export function oauthToEmail(data, success, error) { - $.ajax({ - url: '/api/v1/users/claim/oauth_to_email', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('oauthToEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_oauth_to_email'); -} - -export function emailToLDAP(data, success, error) { - $.ajax({ - url: '/api/v1/users/claim/email_to_ldap', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('emailToLDAP', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_email_to_ldap'); -} - -export function ldapToEmail(data, success, error) { - $.ajax({ - url: '/api/v1/users/claim/ldap_to_email', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('ldapToEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_users_ldap_to_email'); -} - -export function logout(success, error) { - track('api', 'api_users_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 checkMfa(method, team, loginId, success, error) { - $.ajax({ - url: '/api/v1/users/mfa', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({method, team_name: team, login_id: loginId}), - success, - error: function onError(xhr, status, err) { - var e = handleError('checkMfa', xhr, status, err); - error(e); - } - }); -} - -export function loginByEmail(name, email, password, token, success, error) { - $.ajax({ - url: '/api/v1/users/login', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({name, email, password, token}), - success: function onSuccess(data, textStatus, xhr) { - track('api', 'api_users_login_success', data.team_id, 'email', data.email); - sessionStorage.removeItem(data.id + '_last_error'); - BrowserStore.signalLogin(); - success(data, textStatus, xhr); - }, - error: function onError(xhr, status, err) { - track('api', 'api_users_login_fail', name, 'email', email); - - var e = handleError('loginByEmail', xhr, status, err); - error(e); - } - }); -} - -export function loginByUsername(name, username, password, success, error) { - $.ajax({ - url: '/api/v1/users/login', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({name, username, password}), - success: function onSuccess(data, textStatus, xhr) { - track('api', 'api_users_login_success', data.team_id, 'username', data.username); - sessionStorage.removeItem(data.id + '_last_error'); - BrowserStore.signalLogin(); - success(data, textStatus, xhr); - }, - error: function onError(xhr, status, err) { - track('api', 'api_users_login_fail', name, 'username', username); - - var e = handleError('loginByUsername', xhr, status, err); - error(e); - } - }); -} - -export function loginByLdap(teamName, id, password, token, success, error) { - $.ajax({ - url: '/api/v1/users/login_ldap', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({teamName, id, password, token}), - success: function onSuccess(data, textStatus, xhr) { - track('api', 'api_users_loginLdap_success', data.team_id, 'id', id); - sessionStorage.removeItem(data.id + '_last_error'); - BrowserStore.signalLogin(); - success(data, textStatus, xhr); - }, - error: function onError(xhr, status, err) { - track('api', 'api_users_loginLdap_fail', teamName, 'id', id); - - var e = handleError('loginByLdap', xhr, status, err); - error(e); - } - }); -} - -export function revokeSession(altId, success, error) { - $.ajax({ - url: '/api/v1/users/revoke_session', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({id: altId}), - success, - error: function onError(xhr, status, err) { - var e = handleError('revokeSession', xhr, status, err); - error(e); - } - }); -} - -export function getSessions(userId, success, error) { - $.ajax({ - cache: false, - url: '/api/v1/users/' + userId + '/sessions', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getSessions', xhr, status, err); - error(e); - } - }); -} - -export function getAudits(userId, success, error) { - $.ajax({ - url: '/api/v1/users/' + userId + '/audits', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getAudits', xhr, status, err); - error(e); - } - }); -} - -export function getComplianceReports(success, error) { - $.ajax({ - url: '/api/v1/admin/compliance_reports', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getComplianceReports', xhr, status, err); - error(e); - } - }); -} - -export function saveComplianceReports(job, success, error) { - $.ajax({ - url: '/api/v1/admin/save_compliance_report', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(job), - success, - error: (xhr, status, err) => { - var e = handleError('saveComplianceReports', xhr, status, err); - error(e); - } - }); -} - -export function getLogs(success, error) { - $.ajax({ - url: '/api/v1/admin/logs', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getLogs', xhr, status, err); - error(e); - } - }); -} - -export function getServerAudits(success, error) { - $.ajax({ - url: '/api/v1/admin/audits', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getServerAudits', xhr, status, err); - error(e); - } - }); -} - -export function getConfig(success, error) { - return $.ajax({ - url: '/api/v1/admin/config', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getConfig', xhr, status, err); - error(e); - } - }); -} - -export function getAnalytics(name, teamId, success, error) { - let url = '/api/v1/admin/analytics/'; - if (teamId == null) { - url += name; - } else { - url += teamId + '/' + name; - } - $.ajax({ - url, - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('getSystemAnalytics', xhr, status, err); - error(e); - } - }); -} - -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', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(config), - success, - error: function onError(xhr, status, err) { - var e = handleError('saveConfig', xhr, status, err); - error(e); - } - }); -} - -export function logClientError(msg) { - var l = {}; - l.level = 'ERROR'; - l.message = msg; - - $.ajax({ - url: '/api/v1/admin/log_client', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(l) - }); -} - -export function testEmail(config, success, error) { - $.ajax({ - url: '/api/v1/admin/test_email', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(config), - success, - error: function onError(xhr, status, err) { - var e = handleError('testEmail', xhr, status, err); - error(e); - } - }); -} - -export function getAllTeams(success, error) { - $.ajax({ - url: '/api/v1/teams/all', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getAllTeams', xhr, status, err); - error(e); - } - }); -} - -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({ - cache: false, - url: '/api/v1/users/me', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success: function gotUser(data, textStatus, xhr) { - currentUser = data; - if (success) { - success(data, textStatus, xhr); - } - }, - error: function onError(xhr, status, err) { - if (error) { - var e = handleError('getMe', xhr, status, err); - error(e); - } - } - }); - - return currentUser; -} - -export function inviteMembers(data, success, error) { - $.ajax({ - url: '/api/v1/teams/invite_members', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('inviteMembers', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_invite_members'); -} - -export function updateTeam(team, success, error) { - $.ajax({ - url: '/api/v1/teams/update', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(team), - success, - error: (xhr, status, err) => { - var e = handleError('updateTeam', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_update_name'); -} - -export function signupTeam(email, success, error) { - $.ajax({ - url: '/api/v1/teams/signup', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('singupTeam', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_signup'); -} - -export function createTeam(team, success, error) { - $.ajax({ - url: '/api/v1/teams/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(team), - success, - error: function onError(xhr, status, err) { - var e = handleError('createTeam', xhr, status, err); - error(e); - } - }); -} - -export function findTeamByName(teamName, success, error) { - $.ajax({ - url: '/api/v1/teams/find_team_by_name', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({name: teamName}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamByName', xhr, status, err); - error(e); - } - }); -} - -export function createChannel(channel, success, error) { - $.ajax({ - url: '/api/v1/channels/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(channel), - success, - error: function onError(xhr, status, err) { - var e = handleError('createChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_create'); -} - -export function createDirectChannel(channel, userId, success, error) { - $.ajax({ - url: '/api/v1/channels/create_direct', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({user_id: userId}), - success, - error: function onError(xhr, status, err) { - var e = handleError('createDirectChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_create_direct'); -} - -export function updateChannel(channel, success, error) { - $.ajax({ - url: '/api/v1/channels/update', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(channel), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_update'); -} - -export function updateChannelHeader(channelId, header, success, error) { - const data = { - channel_id: channelId, - channel_header: header - }; - - $.ajax({ - url: '/api/v1/channels/update_header', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateChannelHeader', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_header'); -} - -export function updateChannelPurpose(data, success, error) { - $.ajax({ - url: '/api/v1/channels/update_purpose', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateChannelPurpose', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_purpose'); -} - -export function updateNotifyProps(data, success, error) { - $.ajax({ - url: '/api/v1/channels/update_notify_props', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('updateNotifyProps', xhr, status, err); - error(e); - } - }); -} - -export function joinChannel(id, success, error) { - $.ajax({ - url: '/api/v1/channels/' + id + '/join', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - success, - error: function onError(xhr, status, err) { - var e = handleError('joinChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_join'); -} - -export function leaveChannel(id, success, error) { - $.ajax({ - url: '/api/v1/channels/' + id + '/leave', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - success, - error: function onError(xhr, status, err) { - var e = handleError('leaveChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_leave'); -} - -export function deleteChannel(id, success, error) { - $.ajax({ - url: '/api/v1/channels/' + id + '/delete', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - success, - error: function onError(xhr, status, err) { - var e = handleError('deleteChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_delete'); -} - -export function updateLastViewedAt(channelId, success, error) { - $.ajax({ - url: '/api/v1/channels/' + channelId + '/update_last_viewed_at', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - success, - error: function onError(xhr, status, err) { - var e = handleError('updateLastViewedAt', xhr, status, err); - error(e); - } - }); -} - -export function getChannels(success, error) { - return $.ajax({ - cache: false, - url: '/api/v1/channels/', - dataType: 'json', - type: 'GET', - success, - ifModified: true, - error: function onError(xhr, status, err) { - var e = handleError('getChannels', xhr, status, err); - error(e); - } - }); -} - -export function getChannel(id, success, error) { - $.ajax({ - cache: false, - url: '/api/v1/channels/' + id + '/', - dataType: 'json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getChannel', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channel_get'); -} - -export function getMoreChannels(success, error) { - $.ajax({ - url: '/api/v1/channels/more', - dataType: 'json', - type: 'GET', - success, - ifModified: true, - error: function onError(xhr, status, err) { - var e = handleError('getMoreChannels', xhr, status, err); - error(e); - } - }); -} - -export function getChannelCounts(success, error) { - $.ajax({ - cache: false, - url: '/api/v1/channels/counts', - dataType: 'json', - type: 'GET', - success, - ifModified: true, - error: function onError(xhr, status, err) { - var e = handleError('getChannelCounts', xhr, status, err); - error(e); - } - }); -} - -export function getChannelExtraInfo(id, memberLimit, success, error) { - let url = '/api/v1/channels/' + id + '/extra_info'; - - if (memberLimit) { - url += '/' + memberLimit; - } - - return $.ajax({ - url, - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getChannelExtraInfo', xhr, status, err); - error(e); - } - }); -} - -export function executeCommand(channelId, command, suggest, success, error) { - $.ajax({ - url: '/api/v1/commands/execute', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({channelId, command, suggest: '' + suggest}), - success, - error: function onError(xhr, status, err) { - var e = handleError('executeCommand', xhr, status, err); - error(e); - } - }); -} - -export function addCommand(cmd, success, error) { - $.ajax({ - url: '/api/v1/commands/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(cmd), - success, - error: (xhr, status, err) => { - var e = handleError('addCommand', xhr, status, err); - error(e); - } - }); -} - -export function deleteCommand(data, success, error) { - $.ajax({ - url: '/api/v1/commands/delete', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('deleteCommand', xhr, status, err); - error(e); - } - }); -} - -export function listTeamCommands(success, error) { - $.ajax({ - url: '/api/v1/commands/list_team_commands', - dataType: 'json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('listTeamCommands', xhr, status, err); - error(e); - } - }); -} - -export function regenCommandToken(data, success, error) { - $.ajax({ - url: '/api/v1/commands/regen_token', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('regenCommandToken', xhr, status, err); - error(e); - } - }); -} - -export function listCommands(success, error) { - $.ajax({ - url: '/api/v1/commands/list', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('listCommands', xhr, status, err); - error(e); - } - }); -} - -export function getPostsPage(channelId, offset, limit, success, error, complete) { - $.ajax({ - cache: false, - url: '/api/v1/channels/' + channelId + '/posts/' + offset + '/' + limit, - dataType: 'json', - type: 'GET', - ifModified: true, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPosts', xhr, status, err); - error(e); - }, - complete: complete - }); -} - -export function getPosts(channelId, since, success, error, complete) { - return $.ajax({ - url: '/api/v1/channels/' + channelId + '/posts/' + since, - dataType: 'json', - type: 'GET', - ifModified: true, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPosts', xhr, status, err); - error(e); - }, - complete: complete - }); -} - -export function getPostsBefore(channelId, post, offset, numPost, success, error, complete) { - $.ajax({ - url: '/api/v1/channels/' + channelId + '/post/' + post + '/before/' + offset + '/' + numPost, - dataType: 'json', - type: 'GET', - ifModified: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPostsBefore', xhr, status, err); - error(e); - }, - complete: complete - }); -} - -export function getPostsAfter(channelId, post, offset, numPost, success, error, complete) { - $.ajax({ - url: '/api/v1/channels/' + channelId + '/post/' + post + '/after/' + offset + '/' + numPost, - dataType: 'json', - type: 'GET', - ifModified: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPostsAfter', xhr, status, err); - error(e); - }, - complete: complete - }); -} - -export function getPost(channelId, postId, success, error, complete) { - $.ajax({ - cache: false, - url: '/api/v1/channels/' + channelId + '/post/' + postId, - dataType: 'json', - type: 'GET', - ifModified: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPost', xhr, status, err); - error(e); - }, - complete - }); -} - -export function getPostById(postId, success, error, complete) { - $.ajax({ - cache: false, - url: '/api/v1/posts/' + postId, - dataType: 'json', - type: 'GET', - ifModified: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('getPostById', xhr, status, err); - error(e); - }, - complete - }); -} - -export function search(terms, success, error) { - $.ajax({ - url: '/api/v1/posts/search', - dataType: 'json', - type: 'GET', - data: {terms: terms}, - success, - error: function onError(xhr, status, err) { - var e = handleError('search', xhr, status, err); - error(e); - } - }); - - track('api', 'api_posts_search'); -} - -export function deletePost(channelId, id, success, error) { - $.ajax({ - url: '/api/v1/channels/' + channelId + '/post/' + id + '/delete', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - success, - error: function onError(xhr, status, err) { - var e = handleError('deletePost', xhr, status, err); - error(e); - } - }); - - track('api', 'api_posts_delete'); -} - -export function createPost(post, channel, success, error) { - $.ajax({ - url: '/api/v1/channels/' + post.channel_id + '/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(post), - success, - error: function onError(xhr, status, err) { - var e = handleError('createPost', xhr, status, err); - error(e); - } - }); - - track('api', 'api_posts_create', channel.name, 'length', post.message.length); - - // global.window.analytics.track('api_posts_create', { - // category: 'api', - // channel_name: channel.name, - // channel_type: channel.type, - // length: post.message.length, - // files: (post.filenames || []).length, - // mentions: (post.message.match('//g') || []).length - // }); -} - -export function updatePost(post, success, error) { - $.ajax({ - url: '/api/v1/channels/' + post.channel_id + '/update', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(post), - success, - error: function onError(xhr, status, err) { - var e = handleError('updatePost', xhr, status, err); - error(e); - } - }); - - track('api', 'api_posts_update'); -} - -export function addChannelMember(id, data, success, error) { - $.ajax({ - url: '/api/v1/channels/' + id + '/add', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('addChannelMember', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_add_member'); -} - -export function removeChannelMember(id, data, success, error) { - $.ajax({ - url: '/api/v1/channels/' + id + '/remove', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('removeChannelMember', xhr, status, err); - error(e); - } - }); - - track('api', 'api_channels_remove_member'); -} - -export function getProfiles(success, error) { - $.ajax({ - cache: false, - url: '/api/v1/users/profiles', - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - ifModified: true, - error: function onError(xhr, status, err) { - var e = handleError('getProfiles', xhr, status, err); - error(e); - } - }); -} - -export function getProfilesForTeam(teamId, success, error) { - $.ajax({ - cache: false, - url: '/api/v1/users/profiles/' + teamId, - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: function onError(xhr, status, err) { - var e = handleError('getProfilesForTeam', xhr, status, err); - error(e); - } - }); -} - -export function uploadFile(formData, success, error) { - var request = $.ajax({ - url: '/api/v1/files/upload', - type: 'POST', - data: formData, - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - if (err !== 'abort') { - var e = handleError('uploadFile', xhr, status, err); - error(e); - } - } - }); - - track('api', 'api_files_upload'); - - return request; -} - -export function getFileInfo(filename, success, error) { - $.ajax({ - url: '/api/v1/files/get_info' + filename, - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success: (data) => { - success(data); - }, - error: function onError(xhr, status, err) { - var e = handleError('getFileInfo', xhr, status, err); - error(e); - } - }); -} - -export function getPublicLink(data, success, error) { - $.ajax({ - url: '/api/v1/files/get_public_link', - dataType: 'json', - type: 'POST', - data: JSON.stringify(data), - success, - error: function onError(xhr, status, err) { - var e = handleError('getPublicLink', xhr, status, err); - error(e); - } - }); -} - -export function uploadProfileImage(imageData, success, error) { - $.ajax({ - url: '/api/v1/users/newimage', - type: 'POST', - data: imageData, - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('uploadProfileImage', xhr, status, err); - error(e); - } - }); -} - -export function importSlack(fileData, success, error) { - $.ajax({ - url: '/api/v1/teams/import_team', - type: 'POST', - data: fileData, - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('importTeam', xhr, status, err); - error(e); - } - }); -} - -export function exportTeam(success, error) { - $.ajax({ - url: '/api/v1/teams/export_team', - type: 'GET', - dataType: 'json', - success, - error: function onError(xhr, status, err) { - var e = handleError('exportTeam', xhr, status, err); - error(e); - } - }); -} - -export function getStatuses(ids, success, error) { - $.ajax({ - url: '/api/v1/users/status', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(ids), - success, - error: function onError(xhr, status, err) { - var e = handleError('getStatuses', xhr, status, err); - error(e); - } - }); -} - -export function getMyTeam(success, error) { - return $.ajax({ - url: '/api/v1/teams/me', - dataType: 'json', - type: 'GET', - success, - ifModified: true, - error: function onError(xhr, status, err) { - var e = handleError('getMyTeam', xhr, status, err); - error(e); - } - }); -} - -export function registerOAuthApp(app, success, error) { - $.ajax({ - url: '/api/v1/oauth/register', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(app), - success: success, - error: (xhr, status, err) => { - const e = handleError('registerApp', xhr, status, err); - error(e); - } - }); - - module.exports.track('api', 'api_apps_register'); -} - -export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) { - $.ajax({ - url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state, - dataType: 'json', - contentType: 'application/json', - type: 'GET', - success, - error: (xhr, status, err) => { - const e = handleError('allowOAuth2', xhr, status, err); - error(e); - } - }); - - module.exports.track('api', 'api_users_allow_oauth2'); -} - -export function addIncomingHook(hook, success, error) { - $.ajax({ - url: '/api/v1/hooks/incoming/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(hook), - success, - error: (xhr, status, err) => { - var e = handleError('addIncomingHook', xhr, status, err); - error(e); - } - }); -} - -export function deleteIncomingHook(data, success, error) { - $.ajax({ - url: '/api/v1/hooks/incoming/delete', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('deleteIncomingHook', xhr, status, err); - error(e); - } - }); -} - -export function listIncomingHooks(success, error) { - $.ajax({ - url: '/api/v1/hooks/incoming/list', - dataType: 'json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('listIncomingHooks', xhr, status, err); - error(e); - } - }); -} - -export function getAllPreferences(success, error) { - return $.ajax({ - url: '/api/v1/preferences/', - dataType: 'json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('getAllPreferences', xhr, status, err); - error(e); - } - }); -} - -export function getPreferenceCategory(category, success, error) { - $.ajax({ - url: `/api/v1/preferences/${category}`, - dataType: 'json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('getPreferenceCategory', xhr, status, err); - error(e); - } - }); -} - -export function savePreferences(preferences, success, error) { - $.ajax({ - url: '/api/v1/preferences/save', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(preferences), - success, - error: (xhr, status, err) => { - var e = handleError('savePreferences', xhr, status, err); - error(e); - } - }); -} - -export function addOutgoingHook(hook, success, error) { - $.ajax({ - url: '/api/v1/hooks/outgoing/create', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(hook), - success, - error: (xhr, status, err) => { - var e = handleError('addOutgoingHook', xhr, status, err); - error(e); - } - }); -} - -export function deleteOutgoingHook(data, success, error) { - $.ajax({ - url: '/api/v1/hooks/outgoing/delete', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('deleteOutgoingHook', xhr, status, err); - error(e); - } - }); -} - -export function listOutgoingHooks(success, error) { - $.ajax({ - url: '/api/v1/hooks/outgoing/list', - dataType: 'json', - type: 'GET', - success, - error: (xhr, status, err) => { - var e = handleError('listOutgoingHooks', xhr, status, err); - error(e); - } - }); -} - -export function regenOutgoingHookToken(data, success, error) { - $.ajax({ - url: '/api/v1/hooks/outgoing/regen_token', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('regenOutgoingHookToken', xhr, status, err); - error(e); - } - }); -} - -export function uploadLicenseFile(formData, success, error) { - $.ajax({ - url: '/api/v1/license/add', - type: 'POST', - data: formData, - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('uploadLicenseFile', xhr, status, err); - error(e); - } - }); - - track('api', 'api_license_upload'); -} - -export function removeLicenseFile(success, error) { - $.ajax({ - url: '/api/v1/license/remove', - type: 'POST', - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('removeLicenseFile', xhr, status, err); - error(e); - } - }); - - 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); - } - } - }); -} - -export function updateMfa(data, success, error) { - $.ajax({ - url: '/api/v1/users/update_mfa', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify(data), - success, - error: (xhr, status, err) => { - var e = handleError('updateMfa', xhr, status, err); - error(e); - } - }); -} - -export function uploadBrandImage(image, success, error) { - const formData = new FormData(); - formData.append('image', image, image.name); - - $.ajax({ - url: '/api/v1/admin/upload_brand_image', - type: 'POST', - data: formData, - cache: false, - contentType: false, - processData: false, - success, - error: function onError(xhr, status, err) { - var e = handleError('uploadBrandImage', xhr, status, err); - error(e); - } - }); -} diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 87f4153fb..9bdf348cd 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -61,6 +61,7 @@ export default { RECEIVED_ADD_MENTION: null, RECEIVED_PROFILES: null, + RECEIVED_DIRECT_PROFILES: null, RECEIVED_ME: null, RECEIVED_SESSIONS: null, RECEIVED_AUDITS: null, @@ -92,6 +93,9 @@ export default { RECEIVED_SERVER_AUDITS: null, RECEIVED_SERVER_COMPLIANCE_REPORTS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_ALL_TEAM_LISTINGS: null, + RECEIVED_TEAM_MEMBERS: null, + RECEIVED_MEMBERS_FOR_TEAM: null, RECEIVED_LOCALE: null, @@ -205,6 +209,7 @@ export default { LDAP_SERVICE: 'ldap', USERNAME_SERVICE: 'username', SIGNIN_CHANGE: 'signin_change', + PASSWORD_CHANGE: 'password_change', SIGNIN_VERIFIED: 'verified', SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index c4f4a025e..a1e16928b 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -10,9 +10,8 @@ import PreferenceStore from 'stores/preference_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; var ActionTypes = Constants.ActionTypes; -import * as Client from './client.jsx'; import * as AsyncClient from './async_client.jsx'; -import * as client from './client.jsx'; +import Client from './web_client.jsx'; import React from 'react'; import {browserHistory} from 'react-router'; @@ -1114,7 +1113,7 @@ 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) { - return getWindowLocationOrigin() + '/api/v1/files/get' + filename; + return getWindowLocationOrigin() + Client.getFilesRoute() + '/get' + filename; } // Gets the name of a file (including extension) from a given url or file path. @@ -1210,13 +1209,17 @@ export function importSlack(file, success, error) { formData.append('filesize', file.size); formData.append('importFrom', 'slack'); - client.importSlack(formData, success, error); + Client.importSlack(formData, success, error); } export function getTeamURLFromAddressBar() { return window.location.origin + '/' + window.location.pathname.split('/')[1]; } +export function getTeamNameFromUrl() { + return window.location.pathname.split('/')[1]; +} + export function getTeamURLNoOriginFromAddressBar() { return '/' + window.location.pathname.split('/')[1]; } @@ -1263,16 +1266,11 @@ export function openDirectChannelToUser(user, successCb, errorCb) { }; Client.createDirectChannel( - channel, user.id, (data) => { Client.getChannel( data.id, - (data2, textStatus, xhr) => { - if (xhr.status === 304 || !data2) { - return; - } - + (data2) => { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNEL, channel: data2.channel, @@ -1400,7 +1398,7 @@ export function localizeMessage(id, defaultMessage) { } export function getProfilePicSrcForPost(post, timestamp) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; + let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp; if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; diff --git a/webapp/utils/web_client.jsx b/webapp/utils/web_client.jsx new file mode 100644 index 000000000..6071b4bb4 --- /dev/null +++ b/webapp/utils/web_client.jsx @@ -0,0 +1,67 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from '../client/client.jsx'; +import TeamStore from '../stores/team_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; + +const HTTP_UNAUTHORIZED = 401; + +class WebClientClass extends Client { + constructor() { + super(); + this.enableLogErrorsToConsole(true); + TeamStore.addChangeListener(this.onTeamStoreChanged); + } + + onTeamStoreChanged = () => { + this.setTeamId(TeamStore.getCurrentId()); + } + + track = (category, action, label, property, value) => { + if (global.window && global.window.analytics) { + global.window.analytics.track(action, {category, label, property, value}); + } + } + + trackPage = () => { + if (global.window && global.window.analytics) { + global.window.analytics.page(); + } + } + + handleError = (err, res) => { // eslint-disable-line no-unused-vars + if (err.status === HTTP_UNAUTHORIZED) { + GlobalActions.emitUserLoggedOutEvent('/login'); + } + } + + // not sure why but super.login doesn't work if using an () => arrow functions. + // I think this might be a webpack issue. + webLogin(email, username, password, token, success, error) { + this.login( + email, + username, + password, + token, + (data) => { + this.track('api', 'api_users_login_success', '', 'email', data.email); + BrowserStore.signalLogin(); + + if (success) { + success(data); + } + }, + (err) => { + this.track('api', 'api_users_login_fail', name, 'email', email); + if (error) { + error(err); + } + } + ); + } +} + +var WebClient = new WebClientClass(); +export default WebClient; diff --git a/webapp/webpack.config-test.js b/webapp/webpack.config-test.js new file mode 100644 index 000000000..aaeefeb8c --- /dev/null +++ b/webapp/webpack.config-test.js @@ -0,0 +1,131 @@ +const webpack = require('webpack'); +const path = require('path'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const nodeExternals = require('webpack-node-externals'); + +const htmlExtract = new ExtractTextPlugin('html', 'root.html'); + +const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env + +var DEV = true; +var FULLMAP = false; +if (NPM_TARGET === 'run' || NPM_TARGET === 'run-fullmap') { + DEV = true; + if (NPM_TARGET === 'run-fullmap') { + FULLMAP = true; + } +} + +var config = { + target: 'node', + externals: [nodeExternals()], + module: { + loaders: [ + { + test: /\.jsx?$/, + loader: 'babel', + exclude: /(node_modules|non_npm_dependencies)/, + query: { + presets: ['react', 'es2015-webpack', 'stage-0'], + plugins: ['transform-runtime'], + cacheDirectory: DEV + } + }, + { + test: /\.json$/, + loader: 'json' + }, + { + test: /(node_modules|non_npm_dependencies)\/.+\.(js|jsx)$/, + loader: 'imports', + query: { + $: 'jquery', + jQuery: 'jquery' + } + }, + { + test: /\.scss$/, + loaders: ['style', 'css', 'sass'] + }, + { + test: /\.css$/, + loaders: ['style', 'css'] + }, + { + test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/, + loader: 'file', + query: { + name: 'files/[hash].[ext]' + } + }, + { + test: /\.html$/, + loader: htmlExtract.extract('html?attrs=link:href') + } + ] + }, + sassLoader: { + includePaths: ['node_modules/compass-mixins/lib'] + }, + plugins: [ + new webpack.ProvidePlugin({ + 'window.jQuery': 'jquery' + }), + htmlExtract, + new CopyWebpackPlugin([ + {from: 'images/emoji', to: 'emoji'} + ]), + new webpack.LoaderOptionsPlugin({ + minimize: !DEV, + debug: false + }) + ], + resolve: { + alias: { + jquery: 'jquery/dist/jquery' + }, + modules: [ + 'node_modules', + 'non_npm_dependencies', + path.resolve(__dirname) + ] + } +}; + +// Development mode configuration +if (DEV) { + if (FULLMAP) { + config.devtool = 'source-map'; + } else { + config.devtool = 'eval-cheap-module-source-map'; + } +} + +// Production mode configuration +if (!DEV) { + config.devtool = 'source-map'; + config.plugins.push( + new webpack.optimize.UglifyJsPlugin({ + 'screw-ie8': true, + mangle: { + toplevel: false + }, + compress: { + warnings: false + }, + comments: false + }) + ); + config.plugins.push( + new webpack.optimize.AggressiveMergingPlugin() + ); + config.plugins.push( + new webpack.optimize.OccurrenceOrderPlugin(true) + ); + config.plugins.push( + new webpack.optimize.DedupePlugin() + ); +} + +module.exports = config; diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index 4e2d6b70d..dac074c0b 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -88,7 +88,8 @@ var config = { ], resolve: { alias: { - jquery: 'jquery/dist/jquery' + jquery: 'jquery/dist/jquery', + superagent: 'node_modules/superagent/lib/client' }, modules: [ 'node_modules', -- cgit v1.2.3-1-g7c22