diff --git a/CHANGELOG.md b/CHANGELOG.md index eac1e61e53..425902cf01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Mattermost Changelog -## UNDER DEVELOPMENT - Release v1.1.0 +## UNDER DEVELOPMENT Release v1.2.0 The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release. -- **Final release anticipated:** 2015-10-16 +- **Final release anticipated:** 2015-11-16 + +## Release v1.1.0 + +Released: 2015-10-16 ### Release Highlights @@ -44,6 +48,11 @@ Messaging and Notifications - Fixed bug where System Administrator did not have Team Administrator permissions - Fixed bug causing scrolling to jump when the right hand sidebar opened and closed +### Known Issues + +- Slack import is unstable due to change in Slack export format +- Uploading a .flac file breaks the file previewer on iOS + ### Contributors Many thanks to our external contributors. In no particular order: diff --git a/Makefile b/Makefile index 103370288c..5e798ee765 100644 --- a/Makefile +++ b/Makefile @@ -106,11 +106,12 @@ travis: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - @sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-0.14.0.js|react-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-dom-0.14.0.js|react-dom-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-bootstrap-0.27.1.js|react-bootstrap-0.27.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html rm $(DIST_PATH)/web/templates/*.bak @@ -290,11 +291,12 @@ dist: install mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - @sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-0.14.0.js|react-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-dom-0.14.0.js|react-dom-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-bootstrap-0.27.1.js|react-bootstrap-0.27.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html rm $(DIST_PATH)/web/templates/*.bak diff --git a/api/api_test.go b/api/api_test.go index bea949ad22..2ef4e196db 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -19,6 +19,8 @@ func Setup() { StartServer() InitApi() Client = model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) + + Srv.Store.MarkSystemRanUnitTests() } } diff --git a/doc/README.md b/doc/README.md index 7ee2bb46cb..ccb702a5d0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,15 +8,17 @@ Get up and running quickly with Docker-based install - [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md) - [Docker Single Container Preview Setup](install/Docker-Single-Container.md) - [SMTP Email Setup](install/SMTP-Email-Setup.md) +- [System Console Settings](install/Configuration-Settings.md) #### Production Installation Set up Mattermost in your data center - [Software and Hardware Requirements](install/Requirements.md) - [Production Ubuntu Setup](install/Production-Ubuntu.md) - [SMTP Email Setup](install/SMTP-Email-Setup.md) +- [System Console Settings](install/Configuration-Settings.md) #### Configuration and Management -- Configuration Settings Overview +- [System Console Settings](install/Configuration-Settings.md) - [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md) - [Mattermost Release Numbering Scheme](install/Release-Numbering.md) diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md index afaef4ee46..d806b7a9ba 100644 --- a/doc/developer/Setup.md +++ b/doc/developer/Setup.md @@ -61,7 +61,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org 3. Reload your bashrc `source ~/.bashrc` 6. Install Node.js - 1. Download the newest version of the Node.js sources from https://nodejs.org/download/ + 1. Download the newest version of the Node.js sources from https://nodejs.org/en/download/ 2. Extract the contents of the package and cd into the extracted files 3. Compile and install Node.js `./configure` diff --git a/doc/help/Slack-Import.md b/doc/help/Slack-Import.md index a2914570b7..f834d51774 100644 --- a/doc/help/Slack-Import.md +++ b/doc/help/Slack-Import.md @@ -1,22 +1,29 @@ -### Slack Import (Beta) +### Slack Import -*Note: As a SaaS service, Slack is able to change its export format quickly. If you encounter issues not mentioned in the documentation below, please let us know by [filing an issue](https://github.com/mattermost/platform/issues).* +*Note: As a proprietary SaaS service, Slack is able to change its export format quickly and without notice. If you encounter issues not mentioned in the documentation below, please alert the product team by [filing an issue](https://github.com/mattermost/platform/issues).* #### Usage -The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option. +The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. To use: -- **It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions to install Mattermost in preview mode in less than 5 minutes](../install/Docker-Single-Container.md). +1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export** -Mattermost currently supports the processing of an "Export" file from Slack containing account information and public channel archives from a Slack team. +2. In Mattermost go to **Team Settings > Import > Import from Slack**. _Team Owner_ or _Team Administrator_ role is required to access this menu option. -- Emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials. -- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost. +3. Click **Select file** to upload Slack export file and click **Import**. -#### Limitations: +4. Emails and usernames from Slack are used to create new Mattermost accounts + +5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts + +6. Once logged in, the Mattermost users will have access to previous Slack messages in the public channels imported from Slack. + +**It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions](../install/Docker-Single-Container.md) to install Mattermost in preview mode in less than 5 minutes. + +#### Notes: - Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported. - Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI. - Slack does not export any content from private groups or direct messages that your team has stored in Slack's database. -- The Beta release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still shows Slack ID). +- In Beta, Slack accounts with username or email address collisions with existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered. diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md index 05a56c4120..87f8edb848 100644 --- a/doc/install/Production-Ubuntu.md +++ b/doc/install/Production-Ubuntu.md @@ -28,7 +28,7 @@ ## Set up Mattermost Server 1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2 1. Download the latest Mattermost Server by typing: - * ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz``` + * ``` wget https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz``` 1. Unzip the Mattermost Server by typing: * ``` tar -xvzf mattermost.tar.gz``` 1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`. diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md index fcf0128044..4e06d2f992 100644 --- a/doc/install/SMTP-Email-Setup.md +++ b/doc/install/SMTP-Email-Setup.md @@ -1,50 +1,46 @@ ## SMTP Email Setup -In some product evaluation setups email is intentionally bypassed using a `ByPassEmail=true` option. This option allows account creation and system operation without having to set up an email service (e.g. no email verification is required for account creation). This also means neither email notifications nor password reset by email are available. +In product evaluation setups with single-container Docker instances, email is intentionally disabled. This allows account creation and system operation without having to set up email, but it also means email notification and password reset functionality aren't available. -To enable email, turn this option off by setting `ByPassEmail=false` and configuring an SMTP email service as follows: +### How to enable email -1. **Set up an SMTP email sending service.** (If you already have credentials for a SMTP server you can skip this step.) - 1. [Setup Amazon Simple Email Service](https://console.aws.amazon.com/ses) - 2. From the `SMTP Settings` menu click `Create My SMTP Credentials` - 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` - 4. From the `Domains` menu setup and verify a new domain. It it also a good practice to enable `Generate DKIM Settings` for this domain. - 5. Choose an email address like `feedback@example.com` for Mattermost to send emails from. - 6. Test sending an email from `feedback@example.com` by clicking the `Send a Test Email` button and verify everything appears to be working correctly. -2. **Modify the Mattermost configuration file config.json or config_docker.json with the SMTP information.** - 1. If you're running Mattermost on Amazon Beanstalk you can shell into the instance with the following commands - 2. `ssh ec2-user@[domain for the docker instance]` - 3. `sudo gpasswd -a ec2-user docker` - 4. Retrieve the name of the container with `sudo docker ps` - 5. `sudo docker exec -ti container_name /bin/bash` -3. **Edit the config file `vi /config_docker.json` with the settings you captured from the step above.** - 1. See an example below and notice `ByPassEmail` has been set to `false` - ``` bash - "EmailSettings": { - "ByPassEmail" : false, - "SMTPUsername": "AKIADTOVBGERKLCBV", - "SMTPPassword": "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY", - "SMTPServer": "email-smtp.us-east-1.amazonaws.com:465", - "UseTLS": true, - "FeedbackEmail": "feedback@example.com", - "FeedbackName": "Feedback", - "ApplePushServer": "", - "ApplePushCertPublic": "", - "ApplePushCertPrivate": "" - } - ``` -4. **Restart Mattermost** - 1. Find the process id with `ps -A` and look for the process named `platform` - 2. Kill the process `kill pid` - 3. The service should restart automatically. Verify the Mattermost service is running with `ps -A` - 4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address. +To enable email, configure an SMTP email service as follows: + +1. **Set up an SMTP email sending service** (if you don't yet have an SMTP service with credentials) + 1. Any SMTP email service can be used, you just need the following information: `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`. + 2. If you don't have an SMTP service, here are simple instructions to set one up with [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/): + 2. Go to [Amazon SES console](https://console.aws.amazon.com/ses) then `SMTP Settings > Create My SMTP Credentials` + 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below. + 4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain. + 5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly. +2. **Configure SMTP settings** + 1. Open the **System Console** by logging into an existing team and accessing "System Console" from the main menu. + 1. Alternatively, if a team doesn't yet exist, go to `http://dockerhost:8065/` in your browser, create a team, then from the main menu click **System Console** + 2. Go to the **Email Settings** tab and configure the following: + 1. **Allow Sign Up With Email:** `true` + 2. **Send Email Notifications:** `true` + 3. **Require Email Verification:** `true` + 4. **Notification Display Name:** Display name on email account sending notifications + 5. **Notification Email Address:** Email address displayed on email account used to send notifications + 6. **SMTP Username**: `SMTP Username` from Step 1 + 7. **SMTP Password**: `SMTP Password` from Step 1 + 8. **SMTP Server**: `SMTP Server` from Step 1 + 9. **SMTP Port**: `SMTP Port` from Step 1 + 10. **Connection Security**: `TLS (Recommended)` + 11. Then click **Save** + +3. **Restart Mattermost** + 1. Use `ps -A` to find the process ID ("pid") for service named `platform` and stop it using `kill [pid]` + 2. The service should restart automatically. Run `ps -A` to verify the `platform` is running again + 3. Use the reset password page (E.g. _example.com/teamname/reset_password_) to test that email is now working by entering your email and clicking **Reset my password**. + 4. Note: The next time users log out, or when their session tokens expire, each will be required to verify their email address. ### Troubleshooting SMTP -If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open `System Console > Logs` to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments. +If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open **System Console -> Logs** to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments. -For example, if `System Console > Logs` has an error code reading: +For example, if **System Console -> Logs** has an error code reading: ``` Connection unsuccessful: Failed to add to email address - 554 5.7.1 : Client host rejected: Access denied diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md index 8d82100d8c..b87663ab35 100644 --- a/doc/install/Troubleshooting.md +++ b/doc/install/Troubleshooting.md @@ -3,5 +3,5 @@ #### Important notes 1. **DO NOT manipulate the Mattermost database** - - In particular, DO NOT delete data from the database, as this will most likely crash Mattermost in strange ways. Mattermost is designed to archive content continously and generally assumes data is never deleted. + - In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted. diff --git a/mattermost.go b/mattermost.go index d2a0567f45..48487ee739 100644 --- a/mattermost.go +++ b/mattermost.go @@ -84,16 +84,16 @@ func securityAndDiagnosticsJob() { if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { if result := <-api.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) - lastSecurityTime, _ := strconv.ParseInt(props["LastSecurityTime"], 10, 0) + lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0) currentTime := model.GetMillis() if (currentTime - lastSecurityTime) > 1000*60*60*24*1 { l4g.Debug("Checking for security update from Mattermost") - id := props["DiagnosticId"] + id := props[model.SYSTEM_DIAGNOSTIC_ID] if len(id) == 0 { id = model.NewId() - systemId := &model.System{Name: "DiagnosticId", Value: id} + systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id} <-api.Srv.Store.System().Save(systemId) } @@ -104,7 +104,13 @@ func securityAndDiagnosticsJob() { v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS) v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT) - systemSecurityLastTime := &model.System{Name: "LastSecurityTime", Value: strconv.FormatInt(currentTime, 10)} + if len(props[model.SYSTEM_RAN_UNIT_TESTS]) > 0 { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "1") + } else { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "0") + } + + systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)} if lastSecurityTime == 0 { <-api.Srv.Store.System().Save(systemSecurityLastTime) } else { diff --git a/model/system.go b/model/system.go index 033f660b33..70db529d5d 100644 --- a/model/system.go +++ b/model/system.go @@ -8,6 +8,12 @@ import ( "io" ) +const ( + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" +) + type System struct { Name string `json:"name"` Value string `json:"value"` diff --git a/store/sql_store.go b/store/sql_store.go index 4b055e455d..692ac2664e 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -16,17 +16,18 @@ import ( "encoding/json" "errors" "fmt" - "github.com/go-gorp/gorp" - _ "github.com/go-sql-driver/mysql" - _ "github.com/lib/pq" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "io" sqltrace "log" "math/rand" "os" "strings" "time" + + "github.com/go-gorp/gorp" + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) type SqlStore struct { @@ -179,6 +180,17 @@ func (ss SqlStore) GetCurrentSchemaVersion() string { return version } +func (ss SqlStore) MarkSystemRanUnitTests() { + if result := <-ss.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + unitTests := props[model.SYSTEM_RAN_UNIT_TESTS] + if len(unitTests) == 0 { + systemTests := &model.System{Name: model.SYSTEM_RAN_UNIT_TESTS, Value: "1"} + <-ss.System().Save(systemTests) + } + } +} + func (ss SqlStore) DoesTableExist(tableName string) bool { if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { count, err := ss.GetMaster().SelectInt( diff --git a/store/sql_store_test.go b/store/sql_store_test.go index a9e25cb333..1e04b676c3 100644 --- a/store/sql_store_test.go +++ b/store/sql_store_test.go @@ -4,10 +4,11 @@ package store import ( - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "strings" "testing" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) var store Store @@ -16,6 +17,8 @@ func Setup() { if store == nil { utils.LoadConfig("config.json") store = NewSqlStore() + + store.MarkSystemRanUnitTests() } } diff --git a/store/store.go b/store/store.go index 6e1614ccbd..b436a5c403 100644 --- a/store/store.go +++ b/store/store.go @@ -38,6 +38,7 @@ type Store interface { System() SystemStore Webhook() WebhookStore Preference() PreferenceStore + MarkSystemRanUnitTests() Close() } diff --git a/utils/diagnostic.go b/utils/diagnostic.go index 8572c2f514..fd24c2c25e 100644 --- a/utils/diagnostic.go +++ b/utils/diagnostic.go @@ -19,6 +19,7 @@ const ( PROP_DIAGNOSTIC_OS = "os" PROP_DIAGNOSTIC_USER_COUNT = "uc" PROP_DIAGNOSTIC_ACTIVE_USER_COUNT = "auc" + PROP_DIAGNOSTIC_UNIT_TESTS = "ut" ) func SendDiagnostic(values url.Values) { diff --git a/web/react/.eslintrc b/web/react/.eslintrc index c4167829d0..6a35d31238 100644 --- a/web/react/.eslintrc +++ b/web/react/.eslintrc @@ -19,6 +19,7 @@ }, "globals": { "React": false, + "ReactDOM": false, "ReactBootstrap": false }, "rules": { diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 2ad4d5b002..c8af2553d6 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); +var ChannelStore = require('../stores/channel_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var LoadingScreen = require('./loading_screen.jsx'); var Utils = require('../utils/utils.jsx'); @@ -14,8 +15,10 @@ export default class AccessHistoryModal extends React.Component { this.handleMoreInfo = this.handleMoreInfo.bind(this); this.onHide = this.onHide.bind(this); this.onShow = this.onShow.bind(this); + this.formatAuditInfo = this.formatAuditInfo.bind(this); + this.handleRevokedSession = this.handleRevokedSession.bind(this); - let state = this.getStateFromStoresForAudits(); + const state = this.getStateFromStoresForAudits(); state.moreInfo = []; this.state = state; @@ -34,9 +37,9 @@ export default class AccessHistoryModal extends React.Component { } componentDidMount() { UserStore.addAuditsChangeListener(this.onAuditChange); - $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow); - $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); + $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); } componentWillUnmount() { UserStore.removeAuditsChangeListener(this.onAuditChange); @@ -52,23 +55,269 @@ export default class AccessHistoryModal extends React.Component { newMoreInfo[index] = true; this.setState({moreInfo: newMoreInfo}); } + handleRevokedSession(sessionId) { + return 'The session with id ' + sessionId + ' was revoked'; + } + formatAuditInfo(currentAudit) { + const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); + + let currentAuditDesc = ''; + + if (currentActionURL.indexOf('/channels') === 0) { + const channelInfo = currentAudit.extra_info.split(' '); + const channelNameField = channelInfo[0].split('='); + + let channelURL = ''; + let channelObj; + let channelName = ''; + if (channelNameField.indexOf('name') >= 0) { + channelURL = channelNameField[channelNameField.indexOf('name') + 1]; + channelObj = ChannelStore.getByName(channelURL); + if (channelObj) { + channelName = channelObj.display_name; + } else { + channelName = channelURL; + } + } + + switch (currentActionURL) { + case '/channels/create': + currentAuditDesc = 'Created the ' + channelName + ' channel/group'; + break; + case '/channels/create_direct': + currentAuditDesc = 'Established a direct message channel with ' + Utils.getDirectTeammate(channelObj.id).username; + break; + case '/channels/update': + currentAuditDesc = 'Updated the ' + channelName + ' channel/group name'; + break; + case '/channels/update_desc': + currentAuditDesc = 'Updated the ' + channelName + ' channel/group description'; + break; + default: + let userIdField = []; + let userId = ''; + let username = ''; + + if (channelInfo[1]) { + userIdField = channelInfo[1].split('='); + + if (userIdField.indexOf('user_id') >= 0) { + userId = userIdField[userIdField.indexOf('user_id') + 1]; + username = UserStore.getProfile(userId).username; + } + } + + if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) { + currentAuditDesc = 'Deleted the channel/group with the URL ' + channelURL; + } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) { + currentAuditDesc = 'Added ' + username + ' to the ' + channelName + ' channel/group'; + } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) { + currentAuditDesc = 'Removed ' + username + ' from the ' + channelName + ' channel/group'; + } + + break; + } + } else if (currentActionURL.indexOf('/oauth') === 0) { + const oauthInfo = currentAudit.extra_info.split(' '); + + switch (currentActionURL) { + case '/oauth/register': + const clientIdField = oauthInfo[0].split('='); + + if (clientIdField[0] === 'client_id') { + currentAuditDesc = 'Attempted to register a new OAuth Application with ID ' + clientIdField[1]; + } + + break; + case '/oauth/allow': + if (oauthInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to allow a new OAuth service access'; + } else if (oauthInfo[0] === 'success') { + currentAuditDesc = 'Successfully gave a new OAuth service access'; + } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') { + currentAuditDesc = 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'; + } + + break; + case '/oauth/access_token': + if (oauthInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to get an OAuth access token'; + } else if (oauthInfo[0] === 'success') { + currentAuditDesc = 'Successfully added a new OAuth service'; + } else { + const oauthTokenFailure = oauthInfo[0].split('-'); + + if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) { + currentAuditDesc = 'Failed to get an OAuth access token - ' + oauthTokenFailure[1].trim(); + } + } + + break; + default: + break; + } + } else if (currentActionURL.indexOf('/users') === 0) { + const userInfo = currentAudit.extra_info.split(' '); + + switch (currentActionURL) { + case '/users/login': + if (userInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to login'; + } else if (userInfo[0] === 'success') { + currentAuditDesc = 'Successfully logged in'; + } else if (userInfo[0]) { + currentAuditDesc = 'FAILED login attempt'; + } + + break; + case '/users/revoke_session': + currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]); + break; + case '/users/newimage': + currentAuditDesc = 'Updated your profile picture'; + break; + case '/users/update': + currentAuditDesc = 'Updated the general settings of your account'; + break; + case '/users/newpassword': + if (userInfo[0] === 'attempted') { + currentAuditDesc = 'Attempted to change password'; + } else if (userInfo[0] === 'completed') { + currentAuditDesc = 'Successfully changed password'; + } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') { + currentAuditDesc = 'Failed to change password - tried to update user password who was logged in through oauth'; + } + + break; + case '/users/update_roles': + const userRoles = userInfo[0].split('=')[1]; + + currentAuditDesc = 'Updated user role(s) to '; + if (userRoles.trim()) { + currentAuditDesc += userRoles; + } else { + currentAuditDesc += 'member'; + } + + break; + case '/users/update_active': + const updateType = userInfo[0].split('=')[0]; + const updateField = userInfo[0].split('=')[1]; + + /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */ + if (updateType === 'active') { + if (updateField === 'true') { + currentAuditDesc = 'Account made active'; + } else if (updateField === 'false') { + currentAuditDesc = 'Account made inactive'; + } + + const actingUserInfo = userInfo[1].split('='); + if (actingUserInfo[0] === 'session_user') { + const actingUser = UserStore.getProfile(actingUserInfo[1]); + const currentUser = UserStore.getCurrentUser(); + if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) { + currentAuditDesc += ' by ' + actingUser.username; + } else if (currentUser && actingUser) { + currentAuditDesc += ' by an admin'; + } + } + } else if (updateType === 'session_id') { + currentAuditDesc = this.handleRevokedSession(updateField); + } + + break; + case '/users/send_password_reset': + currentAuditDesc = 'Sent an email to ' + userInfo[0].split('=')[1] + ' to reset your password'; + break; + case '/users/reset_password': + if (userInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to reset password'; + } else if (userInfo[0] === 'success') { + currentAuditDesc = 'Successfully reset password'; + } + + break; + case '/users/update_notify': + currentAuditDesc = 'Updated your global notification settings'; + break; + default: + break; + } + } else if (currentActionURL.indexOf('/hooks') === 0) { + const webhookInfo = currentAudit.extra_info.split(' '); + + switch (currentActionURL) { + case '/hooks/incoming/create': + if (webhookInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to create a webhook'; + } else if (webhookInfo[0] === 'success') { + currentAuditDesc = 'Successfully created a webhook'; + } else if (webhookInfo[0] === 'fail - bad channel permissions') { + currentAuditDesc = 'Failed to create a webhook - bad channel permissions'; + } + + break; + case '/hooks/incoming/delete': + if (webhookInfo[0] === 'attempt') { + currentAuditDesc = 'Attempted to delete a webhook'; + } else if (webhookInfo[0] === 'success') { + currentAuditDesc = 'Successfully deleted a webhook'; + } else if (webhookInfo[0] === 'fail - inappropriate conditions') { + currentAuditDesc = 'Failed to delete a webhook - inappropriate conditions'; + } + + break; + default: + break; + } + } else { + switch (currentActionURL) { + case '/logout': + currentAuditDesc = 'Logged out of your account'; + break; + case '/verify_email': + currentAuditDesc = 'Sucessfully verified your email address'; + break; + default: + break; + } + } + + /* If all else fails... */ + if (!currentAuditDesc) { + /* Currently not called anywhere */ + if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) { + currentAuditDesc = 'Revoked all current sessions for the team'; + } else { + let currentActionDesc = ''; + if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) { + currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' '); + currentActionDesc = Utils.toTitleCase(currentActionDesc); + } + + let currentExtraInfoDesc = ''; + if (currentAudit.extra_info) { + currentExtraInfoDesc = currentAudit.extra_info; + + if (currentExtraInfoDesc.indexOf('=') !== -1) { + currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1); + } + } + currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc; + } + } + + const currentDate = new Date(currentAudit.create_at); + const currentAuditInfo = currentDate.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc; + return currentAuditInfo; + } render() { var accessList = []; - var currentHistoryDate = null; for (var i = 0; i < this.state.audits.length; i++) { - var currentAudit = this.state.audits[i]; - var newHistoryDate = new Date(currentAudit.create_at); - var newDate = null; - - if (!currentHistoryDate || currentHistoryDate.toLocaleDateString() !== newHistoryDate.toLocaleDateString()) { - currentHistoryDate = newHistoryDate; - newDate = (
{currentHistoryDate.toDateString()}
); - } - - if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) { - currentAudit.session_id = 'N/A (Login attempt)'; - } + const currentAudit = this.state.audits[i]; + const currentAuditInfo = this.formatAuditInfo(currentAudit); var moreInfo = ( - More info + {'More info'} ); if (this.state.moreInfo[i]) { + if (!currentAudit.session_id) { + currentAudit.session_id = 'N/A'; + + if (currentAudit.action.search('/users/login') >= 0) { + if (currentAudit.extra_info === 'attempt') { + currentAudit.session_id += ' (Login attempt)'; + } else { + currentAudit.session_id += ' (Login failure)'; + } + } + } + moreInfo = (
+
{'IP: ' + currentAudit.ip_address}
{'Session ID: ' + currentAudit.session_id}
-
{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}
); } @@ -99,11 +360,9 @@ export default class AccessHistoryModal extends React.Component { key={'accessHistoryEntryKey' + i} className='access-history__table' > -
{newDate}
-
{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}
+
{currentAuditInfo}
-
{'IP: ' + currentAudit.ip_address}
{moreInfo}
{divider} @@ -138,13 +397,13 @@ export default class AccessHistoryModal extends React.Component { data-dismiss='modal' aria-label='Close' > - +

- Access History + {'Access History'}

{ + $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { this.blockToggle = true; setTimeout(() => { this.blockToggle = false; @@ -36,7 +36,7 @@ export default class AdminNavbarDropdown extends React.Component { } componentWillUnmount() { - $(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); + $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); } render() { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index bc6ad19317..4c2a473b61 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -100,6 +100,7 @@ export default class AdminSidebar extends React.Component { className='menu-icon--right menu__close' onClick={this.removeTeam.bind(this, team.id)} style={{cursor: 'pointer'}} + title='Remove team from sidebar menu' > {'x'} @@ -233,7 +234,10 @@ export default class AdminSidebar extends React.Component { href='#' onClick={this.showTeamSelect} > - + diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index c028d605da..01759b2220 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -41,28 +41,28 @@ export default class EmailSettings extends React.Component { buildConfig() { var config = this.props.config; - config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked; - config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked; - config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked; - config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked; - config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim(); - config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim(); - config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim(); - config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim(); - config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim(); - config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim(); - config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim(); + config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked; + config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; + config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked; + config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; + config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim(); + config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim(); + config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim(); + config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim(); + config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim(); + config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim(); + config.EmailSettings.ConnectionSecurity = ReactDOM.findDOMNode(this.refs.ConnectionSecurity).value.trim(); - config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim(); + config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim(); if (config.EmailSettings.InviteSalt === '') { config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); - React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt; + ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt; } - config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim(); + config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim(); if (config.EmailSettings.PasswordResetSalt === '') { config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); - React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt; + ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt; } return config; @@ -70,14 +70,14 @@ export default class EmailSettings extends React.Component { handleGenerateInvite(e) { e.preventDefault(); - React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); var s = {saveNeeded: true, serverError: this.state.serverError}; this.setState(s); } handleGenerateReset(e) { e.preventDefault(); - React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); var s = {saveNeeded: true, serverError: this.state.serverError}; this.setState(s); } @@ -447,7 +447,7 @@ export default class EmailSettings extends React.Component {
@@ -258,7 +260,7 @@ export default class GitLabSettings extends React.Component { } -//config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim(); +//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim(); //
//