From b8c80c6d9520ec0af0407c576ee565df85fcc41f Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Wed, 14 Oct 2015 20:21:25 +0200 Subject: [PATCH 01/38] Don't auto-add http:// to mailto: and ftp: markdown links --- web/react/utils/markdown.jsx | 2 +- web/react/utils/text_formatting.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 12d6dd4249..2813798d22 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -32,7 +32,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer { link(href, title, text) { let outHref = href; - if (outHref.lastIndexOf('http', 0) !== 0) { + if (!(/^(mailto|https?|ftp)/.test(outHref))) { outHref = `http://${outHref}`; } diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 2b6e6e14e2..d79aeed68c 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -89,7 +89,7 @@ function autolinkUrls(text, tokens) { if (match.getType() === 'email') { url = `mailto:${url}`; - } else if (url.lastIndexOf('http', 0) !== 0) { + } else if (!(/^(mailto|https?|ftp)/.test(url))) { url = `http://${url}`; } From f0a3c5983492130adb7bf6b92d2682b029c302e9 Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Wed, 14 Oct 2015 21:41:03 +0200 Subject: [PATCH 02/38] PLT-42: Add "File uploading" text left of "Add Comment" when file is uploading --- web/react/components/create_comment.jsx | 12 ++++++++++++ web/sass-files/sass/partials/_post_right.scss | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 680d693f14..2ac5d2179e 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -255,6 +255,17 @@ export default class CreateComment extends React.Component { postFooterClassName += ' has-error'; } + let uploadsInProgressText = null; + if (this.state.uploadsInProgress.length > 0) { + uploadsInProgressText = ( + + {this.state.uploadsInProgress.length === 1 ? 'File uploading' : 'Files uploading'} + + ); + } + return (
@@ -295,6 +306,7 @@ export default class CreateComment extends React.Component { value='Add Comment' onClick={this.handleSubmit} /> + {uploadsInProgressText} {postError} {serverError}
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index e4860b2866..b72176a119 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -36,6 +36,11 @@ .post-create-footer { padding-top: 10px; } + .post-right-comments-upload-in-progress { + padding: 6px 0; + color: #a8adb7; + margin-right: 10px; + } } } From 536c55dc5f9f80ea67e2ccb39c1b4a276fbddc07 Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Wed, 7 Oct 2015 16:58:27 -0700 Subject: [PATCH 03/38] Initial changes to audit log along with structure for handling different audit types --- web/react/components/access_history_modal.jsx | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 2ad4d5b002..9aeb7f6822 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -15,7 +15,7 @@ export default class AccessHistoryModal extends React.Component { this.onHide = this.onHide.bind(this); this.onShow = this.onShow.bind(this); - let state = this.getStateFromStoresForAudits(); + const state = this.getStateFromStoresForAudits(); state.moreInfo = []; this.state = state; @@ -54,29 +54,91 @@ export default class AccessHistoryModal extends React.Component { } 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()}
); - } + var currentDate = new Date(currentAudit.create_at); if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) { currentAudit.session_id = 'N/A (Login attempt)'; } + const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); + + let currentAuditDesc = ''; + + /* Handle specific audit type formatting semi-individually and + fall back to a best guess case if none exists + + Supported audits: + /channels + - Create Channel X + - Update Channel X + - Update Channel Description X + - Delete Channel X + - Add User to Channel X + - Remove User from Channel X + + /oauth + - Register X + - Allow Attempt/Success/Failure X + + /team + - Revoke All Sessions X (NO CORRESPONDING ADDRESS) + + - Update (users - ?) X + - Update Notify (?) X + - Login Attempt X + - Login (success/failure) X + - Logout (/logout) X + */ + switch (currentActionURL) { + case '/channels/create': + break; + case '/channels/update': + break; + case '/channels/update_desc': + break; + case /\/channels\/[A-Za-z0-9]+\/delete/: + break; + case /\/channels\/[A-Za-z0-9]+\/add/: + break; + case /\/channels\/[A-Za-z0-9]+\/remove/: + break; + case '/oauth/register': + break; + case '/oauth/allow': + break; + case '/team/': + break; + default: + 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; + break; + } + var moreInfo = ( - More info + {'More info'} ); @@ -84,7 +146,6 @@ export default class AccessHistoryModal extends React.Component { moreInfo = (
{'Session ID: ' + currentAudit.session_id}
-
{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}
); } @@ -99,11 +160,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'})}
+
{currentDate.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc + ' from ' + currentAudit.ip_address}
-
{'IP: ' + currentAudit.ip_address}
{moreInfo}
{divider} @@ -138,13 +197,13 @@ export default class AccessHistoryModal extends React.Component { data-dismiss='modal' aria-label='Close' > - +

- Access History + {'Access History'}

Date: Fri, 9 Oct 2015 16:35:59 -0700 Subject: [PATCH 04/38] Finished structure for formatting audits and began writing individual formatting for each audit --- web/react/components/access_history_modal.jsx | 181 ++++++++++++------ 1 file changed, 126 insertions(+), 55 deletions(-) diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 9aeb7f6822..41a865df40 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -14,6 +14,7 @@ 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); const state = this.getStateFromStoresForAudits(); state.moreInfo = []; @@ -52,74 +53,127 @@ export default class AccessHistoryModal extends React.Component { newMoreInfo[index] = true; this.setState({moreInfo: newMoreInfo}); } - render() { - var accessList = []; + formatAuditInfo(currentAudit) { + const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); - for (var i = 0; i < this.state.audits.length; i++) { - var currentAudit = this.state.audits[i]; - var currentDate = new Date(currentAudit.create_at); + let currentAuditDesc = ' '; - if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) { - currentAudit.session_id = 'N/A (Login attempt)'; + /* Handle audit formatting semi-individually for each type and + fall back to a best guess case if none exists + + Supported audits: + /channels + - Create Channel X + - Update Channel X + - Update Channel Description X + - Delete Channel X + - Add User to Channel X + - Remove User from Channel X + + /oauth + - Register X + - Allow Attempt/Success/Failure X + + /team + - Revoke All Sessions X (NO CORRESPONDING ADDRESS/FUNCTION) + + - Revoke Session X + - Update (users - ?) X + - Update Notify (?) X + - Login Attempt X + - Login (success/failure) X + - Logout (/logout) X + - Verify Email (/verify_email) X + */ + switch (currentActionURL) { + + /* BREAK UP CHANNEL INTO OWN SWITCH STATEMENT TO REUSE VARIABLES AND BE CLEAN */ + case '/channels/create': + const createChannelInfo = currentAudit.extra_info.split('='); + let channelName = ''; + + if (createChannelInfo[0] === 'name') { + channelName = createChannelInfo[1]; } - const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); + currentAuditDesc = 'Created a new channel/group named ' + channelName; - let currentAuditDesc = ''; + break; + case '/channels/update': + const updateChannelInfo = currentAudit.extra_info.split('='); + let originalChannelName = ''; - /* Handle specific audit type formatting semi-individually and - fall back to a best guess case if none exists + if (updateChannelInfo[0] === 'name') { + originalChannelName = updateChannelInfo[1]; + } - Supported audits: - /channels - - Create Channel X - - Update Channel X - - Update Channel Description X - - Delete Channel X - - Add User to Channel X - - Remove User from Channel X + currentAuditDesc = 'Updated the channel/group name for ' + originalChannelName; + break; - /oauth - - Register X - - Allow Attempt/Success/Failure X + /* case '/channels/update_desc': + const updateChannelInfo = currentAudit.extra_info.split('='); + let originalChannelName = ''; - /team - - Revoke All Sessions X (NO CORRESPONDING ADDRESS) + if (updateChannelInfo[0] === 'name') { + originalChannelName = updateChannelInfo[1]; + } - - Update (users - ?) X - - Update Notify (?) X - - Login Attempt X - - Login (success/failure) X - - Logout (/logout) X - */ - switch (currentActionURL) { - case '/channels/create': - break; - case '/channels/update': - break; - case '/channels/update_desc': - break; - case /\/channels\/[A-Za-z0-9]+\/delete/: - break; - case /\/channels\/[A-Za-z0-9]+\/add/: - break; - case /\/channels\/[A-Za-z0-9]+\/remove/: - break; - case '/oauth/register': - break; - case '/oauth/allow': - break; - case '/team/': - break; - default: - let currentActionDesc = ''; + currentAuditDesc = 'Updated the channel/group name for ' + originalChannelName; + break;*/ + case /\/channels\/[A-Za-z0-9]+\/delete/: + break; + case /\/channels\/[A-Za-z0-9]+\/add/: + break; + case /\/channels\/[A-Za-z0-9]+\/remove/: + break; + case '/oauth/register': + break; + case '/oauth/allow': + break; + case '/users/login': + break; + case '/users/revoke_session': + break; + case '/users/newimage': + break; + case '/users/update': + break; + case '/users/newpassword': + break; + case '/users/update_roles': + break; + case '/users/update_active': + break; + case '/users/send_password_reset': + break; + case '/users/reset_password': + break; + case '/users/update_notify': + break; + case '/logout': + break; + case '/hooks/incoming/create': + break; + case '/hooks/incoming/delete': + break; + case '/verify_email': + break; + case '/oauth/access_token': + break; + case '': + break; + default: + if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) { + // do stuff + } else { + let currentActionDesc = ' '; if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) { currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' '); currentActionDesc = Utils.toTitleCase(currentActionDesc); } - let currentExtraInfoDesc = ''; + let currentExtraInfoDesc = ' '; if (currentAudit.extra_info) { currentExtraInfoDesc = currentAudit.extra_info; @@ -127,11 +181,23 @@ export default class AccessHistoryModal extends React.Component { currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1); } } - currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc; - break; } + break; + } + + 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 = []; + + for (var i = 0; i < this.state.audits.length; i++) { + const currentAudit = this.state.audits[i]; + const currentAuditInfo = this.formatAuditInfo(currentAudit); + var moreInfo = ( +
{'IP: ' + currentAudit.ip_address}
{'Session ID: ' + currentAudit.session_id}
); @@ -161,7 +232,7 @@ export default class AccessHistoryModal extends React.Component { className='access-history__table' >
-
{currentDate.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc + ' from ' + currentAudit.ip_address}
+
{currentAuditInfo}
{moreInfo}
From 63b0ca8e4b63231e7b3d7cdb8b2e35d70d328114 Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Mon, 12 Oct 2015 17:00:27 -0700 Subject: [PATCH 05/38] Added more supported audits to audit log --- web/react/components/access_history_modal.jsx | 219 ++++++++++++------ 1 file changed, 143 insertions(+), 76 deletions(-) diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 41a865df40..cb50ee4e79 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'); @@ -56,16 +57,17 @@ export default class AccessHistoryModal extends React.Component { formatAuditInfo(currentAudit) { const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); - let currentAuditDesc = ' '; + let currentAuditDesc = ''; /* Handle audit formatting semi-individually for each type and fall back to a best guess case if none exists Supported audits: /channels - - Create Channel X - - Update Channel X - - Update Channel Description X + - Create Channel + - Create Direct Channel + - Update Channel + - Update Channel Description - Delete Channel X - Add User to Channel X - Remove User from Channel X @@ -85,84 +87,147 @@ export default class AccessHistoryModal extends React.Component { - Logout (/logout) X - Verify Email (/verify_email) X */ - switch (currentActionURL) { + if (currentActionURL.indexOf('/channels') === 0) { + const channelInfo = currentAudit.extra_info.split(' '); + const channelNameField = channelInfo[0].split('='); - /* BREAK UP CHANNEL INTO OWN SWITCH STATEMENT TO REUSE VARIABLES AND BE CLEAN */ - case '/channels/create': - const createChannelInfo = currentAudit.extra_info.split('='); + let channelURL = ''; + let channelObj; let channelName = ''; - - if (createChannelInfo[0] === 'name') { - channelName = createChannelInfo[1]; + if (channelNameField.indexOf('name') >= 0) { + channelURL = channelNameField[channelNameField.indexOf('name') + 1]; + channelObj = ChannelStore.getByName(channelURL); + if (channelObj) { + channelName = channelObj.display_name; + } else { + channelName = channelURL; + } } - currentAuditDesc = 'Created a new channel/group named ' + channelName; + 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 = ''; - break; - case '/channels/update': - const updateChannelInfo = currentAudit.extra_info.split('='); - let originalChannelName = ''; + if (channelInfo[1]) { + userIdField = channelInfo[1].split('='); - if (updateChannelInfo[0] === 'name') { - originalChannelName = updateChannelInfo[1]; + 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; } - - currentAuditDesc = 'Updated the channel/group name for ' + originalChannelName; - break; - - /* case '/channels/update_desc': - const updateChannelInfo = currentAudit.extra_info.split('='); - let originalChannelName = ''; - - if (updateChannelInfo[0] === 'name') { - originalChannelName = updateChannelInfo[1]; + } else if (currentActionURL.indexOf('/oauth') === 0) { + switch (currentActionURL) { + case '/oauth/register': + break; + case '/oauth/allow': + break; + case '/oauth/access_token': + 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 = 'Login attempted'; + } else if (userInfo[0] === 'success') { + currentAuditDesc = 'Successful login attempt'; + } else if (userInfo[0]) { + currentAuditDesc = 'FAILED login attempt'; + } - currentAuditDesc = 'Updated the channel/group name for ' + originalChannelName; - break;*/ - case /\/channels\/[A-Za-z0-9]+\/delete/: - break; - case /\/channels\/[A-Za-z0-9]+\/add/: - break; - case /\/channels\/[A-Za-z0-9]+\/remove/: - break; - case '/oauth/register': - break; - case '/oauth/allow': - break; - case '/users/login': - break; - case '/users/revoke_session': - break; - case '/users/newimage': - break; - case '/users/update': - break; - case '/users/newpassword': - break; - case '/users/update_roles': - break; - case '/users/update_active': - break; - case '/users/send_password_reset': - break; - case '/users/reset_password': - break; - case '/users/update_notify': - break; - case '/logout': - break; - case '/hooks/incoming/create': - break; - case '/hooks/incoming/delete': - break; - case '/verify_email': - break; - case '/oauth/access_token': - break; - case '': - break; - default: + break; + case '/users/revoke_session': + const revokedSessionId = userInfo[0].split('=')[1]; + + currentAuditDesc = 'Revoked the session with id ' + revokedSessionId; + 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 = 'Password change attempted'; + } else if (userInfo[0] === 'completed') { + currentAuditDesc = 'Password change success'; + } + + 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': + break; + case '/users/send_password_reset': + break; + case '/users/reset_password': + break; + case '/users/update_notify': + break; + default: + break; + } + } else if (currentActionURL.indexOf('/hooks') === 0) { + switch (currentActionURL) { + case '/hooks/incoming/create': + break; + case '/hooks/incoming/delete': + break; + default: + break; + } + } else { + switch (currentActionURL) { + case '/logout': + break; + case '/verify_email': + break; + default: + break; + } + } + + /* If all else fails... */ + if (!currentAuditDesc) { if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) { // do stuff @@ -183,8 +248,6 @@ export default class AccessHistoryModal extends React.Component { } currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc; } - - break; } const currentDate = new Date(currentAudit.create_at); @@ -210,7 +273,11 @@ export default class AccessHistoryModal extends React.Component { if (this.state.moreInfo[i]) { if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) { - currentAudit.session_id = 'N/A (Login attempt)'; + if (currentAudit.extra_info === 'attempt') { + currentAudit.session_id = 'N/A (Login attempt)'; + } else { + currentAudit.session_id = 'N/A (Login failure)'; + } } moreInfo = ( From e7e83b44d8be1f0081b70274a351486bade35d5b Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Tue, 13 Oct 2015 11:15:44 -0700 Subject: [PATCH 06/38] Added more individual audit handling, primarily involving the user --- web/react/components/access_history_modal.jsx | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index cb50ee4e79..4a169258a5 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -16,6 +16,7 @@ export default class AccessHistoryModal extends React.Component { this.onHide = this.onHide.bind(this); this.onShow = this.onShow.bind(this); this.formatAuditInfo = this.formatAuditInfo.bind(this); + this.handleRevokedSession = this.handleRevokedSession.bind(this); const state = this.getStateFromStoresForAudits(); state.moreInfo = []; @@ -54,6 +55,9 @@ 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]/, ''); @@ -142,6 +146,7 @@ export default class AccessHistoryModal extends React.Component { break; } } else if (currentActionURL.indexOf('/oauth') === 0) { + /* NEEDS TO BE DONE */ switch (currentActionURL) { case '/oauth/register': break; @@ -166,9 +171,7 @@ export default class AccessHistoryModal extends React.Component { break; case '/users/revoke_session': - const revokedSessionId = userInfo[0].split('=')[1]; - - currentAuditDesc = 'Revoked the session with id ' + revokedSessionId; + currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]); break; case '/users/newimage': currentAuditDesc = 'Updated your profile picture'; @@ -196,30 +199,73 @@ export default class AccessHistoryModal extends React.Component { 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': + + /* NEEDS TO BE TESTED! */ + if (userInfo[0] === 'attempted') { + currentAuditDesc = 'Password reset attempted'; + } else if (userInfo[0] === 'completed') { + currentAuditDesc = 'Password reset success'; + } + break; case '/users/update_notify': + currentAuditDesc = 'Updated your global notification settings'; break; default: break; } } else if (currentActionURL.indexOf('/hooks') === 0) { + /* NEEDS TO BE TESTED */ switch (currentActionURL) { case '/hooks/incoming/create': + currentAuditDesc = 'Attempted to create a webhook'; + currentAuditDesc = 'Successfully created a webhook'; break; case '/hooks/incoming/delete': + currentAuditDesc = 'Attempted to delete a webhook'; + currentAuditDesc = 'Successfully deleted a webhook'; break; default: break; } } else { + /* NEEDS TO BE TESTED */ switch (currentActionURL) { case '/logout': + currentAuditDesc = 'Logged out of your account'; break; case '/verify_email': + currentAuditDesc = 'Sucessfully verified your email address'; break; default: break; @@ -228,9 +274,9 @@ export default class AccessHistoryModal extends React.Component { /* If all else fails... */ if (!currentAuditDesc) { + /* Currently not called anywhere */ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) { - - // do stuff + currentAuditDesc = 'Revoked all current sessions for the team'; } else { let currentActionDesc = ' '; if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) { From 7523c6f2e6b52c6aaa40900d4b9a6d8b461ba446 Mon Sep 17 00:00:00 2001 From: esethna Date: Wed, 14 Oct 2015 14:36:19 -0600 Subject: [PATCH 07/38] Update SMTP instructions Needs dev review. --- doc/install/SMTP-Email-Setup.md | 41 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md index 86e2bb20e5..0c3004c5f9 100644 --- a/doc/install/SMTP-Email-Setup.md +++ b/doc/install/SMTP-Email-Setup.md @@ -1,17 +1,17 @@ ## 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 some product evaluation setups, email is intentionally bypassed by setting `SendEmailNotifications=false`. 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. -To enable email, turn this option off by setting `ByPassEmail=false` and configuring an SMTP email service as follows: +To enable email, turn this option on by setting `SendEmailNotifications=true` and configuring an SMTP email service as follows: 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. + 5. Choose an email address like `mattermost@example.com` for Mattermost to send emails from. + 6. Test sending an email from `mattermost@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]` @@ -19,20 +19,25 @@ To enable email, turn this option off by setting `ByPassEmail=false` and configu 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": "" - } + 1. See an example below and notice `SendEmailNotifications` has been set to `true` + ``` + "EmailSettings": { + "EnableSignUpWithEmail": true, + "SendEmailNotifications": true, + "RequireEmailVerification": true, + "FeedbackName": "No-Reply", + "FeedbackEmail": "mattermost@example.com", + "SMTPUsername": "AFIADTOVDKDLGERR", + "SMTPPassword": "DFKJoiweklsjdflkjOIGHLSDFJewiskdjf", + "SMTPServer": "email-smtp.us-east-1.amazonaws.com", + "SMTPPort": "465", + "ConnectionSecurity": "tls", + "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS", + "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL", + "ApplePushServer": "", + "ApplePushCertPublic": "", + "ApplePushCertPrivate": "" + }, ``` 4. **Restart Mattermost** 1. Find the process id with `ps -A` and look for the process named `platform` From ae0d4dce7bc42f75568d65aa66c8db244f8f329f Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 14 Oct 2015 16:57:03 -0400 Subject: [PATCH 08/38] Updated More Direct Channels modal and converted it to ReactBootstrap --- web/react/components/more_direct_channels.jsx | 348 ++++++++++++------ web/react/components/sidebar.jsx | 69 ++-- web/react/pages/channel.jsx | 6 - web/react/utils/utils.jsx | 12 + web/sass-files/sass/partials/_modal.scss | 46 +++ 5 files changed, 345 insertions(+), 136 deletions(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index bc610cd603..99babe714d 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -1,133 +1,273 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var TeamStore = require('../stores/team_store.jsx'); -var Client = require('../utils/client.jsx'); -var Constants = require('../utils/constants.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var PreferenceStore = require('../stores/preference_store.jsx'); -var utils = require('../utils/utils.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const Constants = require('../utils/constants.jsx'); +const Client = require('../utils/client.jsx'); +const Modal = ReactBootstrap.Modal; +const PreferenceStore = require('../stores/preference_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class MoreDirectChannels extends React.Component { constructor(props) { super(props); - this.state = {channels: [], loadingDMChannel: -1}; + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleHide = this.handleHide.bind(this); + this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); + this.handleUserChange = this.handleUserChange.bind(this); + + this.createRowForUser = this.createRowForUser.bind(this); + + this.state = { + users: this.getUsersFromStore(), + filter: '', + loadingDMChannel: -1 + }; + } + + getUsersFromStore() { + const currentId = UserStore.getCurrentId(); + const profiles = UserStore.getProfiles(); + const users = []; + + for (const id in profiles) { + if (id !== currentId) { + users.push(profiles[id]); + } + } + + users.sort((a, b) => a.username.localeCompare(b.username)); + + return users; } componentDidMount() { - $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { - var button = e.relatedTarget; - this.setState({channels: $(button).data('channels')}); // eslint-disable-line react/no-did-mount-set-state - }); + UserStore.addChangeListener(this.handleUserChange); } - handleJoinDirectChannel(channel) { - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'true'); + componentWillUnmount() { + UserStore.addChangeListener(this.handleUserChange); + } + + handleFilterChange() { + const filter = React.findDOMNode(this.refs.filter).value; + + if (filter !== this.state.filter) { + this.setState({filter}); + } + } + + handleHide() { + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + + this.setState({filter: ''}); + } + + handleShowDirectChannel(teammate, e) { + if (this.state.loadingDMChannel !== -1) { + return; + } + + e.preventDefault(); + + const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id); + let channel = ChannelStore.getByName(channelName); + + const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); AsyncClient.savePreferences([preference]); + + if (channel) { + Utils.switchChannel(channel); + + this.handleHide(); + } else { + this.setState({loadingDMChannel: teammate.id}); + + channel = { + name: channelName, + last_post_at: 0, + total_msg_count: 0, + type: 'D', + display_name: teammate.username, + teammate_id: teammate.id, + status: UserStore.getStatus(teammate.id) + }; + + Client.createDirectChannel( + channel, + teammate.id, + (data) => { + this.setState({loadingDMChannel: -1}); + + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + + this.handleHide(); + }, + () => { + this.setState({loadingDMChannel: -1}); + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName; + } + ); + } + } + + handleUserChange() { + this.setState({users: this.getUsersFromStore()}); + } + + createRowForUser(user) { + const details = []; + + const fullName = Utils.getFullName(user); + if (fullName) { + details.push( + + {fullName} + + ); + } + + if (user.nickname) { + const separator = fullName ? ' - ' : ''; + details.push( + + {separator + user.nickname} + + ); + } + + let joinButton; + if (this.state.loadingDMChannel === user.id) { + joinButton = ( + + ); + } else { + joinButton = ( + + ); + } + + return ( +
  • +
    + +
    +
    +
    + {user.username} +
    +
    + {details} +
    +
    +
    + {joinButton} +
    +
  • + ); } render() { - var directMessageItems = this.state.channels.map((channel, index) => { - var badge = ''; - var titleClass = ''; - var handleClick = null; + if (!this.props.show) { + return null; + } - if (channel.fake) { - // It's a direct message channel that doesn't exist yet so let's create it now - var otherUserId = utils.getUserIdFromChannelName(channel); + let users = this.state.users; + if (this.state.filter !== '') { + users = users.filter((user) => { + return user.username.indexOf(this.state.filter) !== -1 || + user.first_name.indexOf(this.state.filter) !== -1 || + user.last_name.indexOf(this.state.filter) !== -1 || + user.nickname.indexOf(this.state.filter) !== -1; + }); + } - if (this.state.loadingDMChannel === index) { - badge = ( - - ); - } + const userEntries = users.map(this.createRowForUser); - if (this.state.loadingDMChannel === -1) { - handleClick = (e) => { - e.preventDefault(); - this.setState({loadingDMChannel: index}); - this.handleJoinDirectChannel(channel); + if (userEntries.length === 0) { + userEntries.push(
  • {'No users found :('}
  • ); + } - Client.createDirectChannel(channel, otherUserId, - (data) => { - $(React.findDOMNode(this.refs.modal)).modal('hide'); - this.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - utils.switchChannel(data); - }, - () => { - this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - } - ); - }; - } - } else { - if (channel.unread) { - badge = {channel.unread}; - titleClass = 'unread-title'; - } + let memberString = 'Member'; + if (users.length !== 1) { + memberString += 's'; + } - handleClick = (e) => { - e.preventDefault(); - this.handleJoinDirectChannel(channel); - utils.switchChannel(channel); - $(React.findDOMNode(this.refs.modal)).modal('hide'); - }; - } - - return ( -
  • - {badge}{channel.display_name} -
  • - ); - }); + let count; + if (users.length === this.state.users.length) { + count = `${users.length} ${memberString}`; + } else { + count = `${users.length} ${memberString} of ${this.state.users.length} Total`; + } return ( - diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx index 16a69e6644..655daa30c0 100644 --- a/web/react/components/admin_console/sql_settings.jsx +++ b/web/react/components/admin_console/sql_settings.jsx @@ -220,7 +220,7 @@ export default class SqlSettings extends React.Component {

    {'32-character salt available to encrypt and decrypt sensitive fields in database.'}