diff options
Diffstat (limited to 'web/react')
-rw-r--r-- | web/react/components/access_history_modal.jsx | 3 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_controller.jsx | 2 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_sidebar.jsx | 4 | ||||
-rw-r--r-- | web/react/components/admin_console/audits.jsx | 5 | ||||
-rw-r--r-- | web/react/components/admin_console/license_settings.jsx | 18 | ||||
-rw-r--r-- | web/react/components/audit_table.jsx | 655 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 5 | ||||
-rw-r--r-- | web/react/components/delete_post_modal.jsx | 2 | ||||
-rw-r--r-- | web/react/components/post_body.jsx | 30 | ||||
-rw-r--r-- | web/react/components/post_info.jsx | 25 | ||||
-rw-r--r-- | web/react/dispatcher/event_helpers.jsx | 25 | ||||
-rw-r--r-- | web/react/stores/post_store.jsx | 92 | ||||
-rw-r--r-- | web/react/stores/socket_store.jsx | 4 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 5 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 4 |
15 files changed, 452 insertions, 427 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 98b1d7cc1..af4d3fb0f 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -73,7 +73,8 @@ class AccessHistoryModal extends React.Component { content = ( <AuditTable audits={this.state.audits} - moreInfo={this.state.moreInfo} + showIp={true} + showSession={true} /> ); } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 360ae3ef3..695e2083a 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -160,7 +160,7 @@ export default class AdminController extends React.Component { } else if (this.state.selected === 'ldap_settings') { tab = <LdapSettingsTab config={this.state.config} />; } else if (this.state.selected === 'license') { - tab = <LicenseSettingsTab />; + tab = <LicenseSettingsTab config={this.state.config} />; } else if (this.state.selected === 'team_users') { if (this.state.teams) { tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 642bfe9d7..eadd8d412 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -225,7 +225,7 @@ export default class AdminSidebar extends React.Component { > <FormattedMessage id='admin.sidebar.audits' - defaultMessage='Audits' + defaultMessage='Compliance and Auditing' /> </a> </li> @@ -454,6 +454,7 @@ export default class AdminSidebar extends React.Component { </ul> <ul className='nav nav__sub-menu padded'> {licenseSettings} + {audits} <li> <a href='#' @@ -466,7 +467,6 @@ export default class AdminSidebar extends React.Component { /> </a> </li> - {audits} </ul> </li> </ul> diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx index 866539b3d..173e63b45 100644 --- a/web/react/components/admin_console/audits.jsx +++ b/web/react/components/admin_console/audits.jsx @@ -60,8 +60,9 @@ export default class Audits extends React.Component { <div style={{margin: '10px'}}> <AuditTable audits={this.state.audits} - oneLine={true} showUserId={true} + showIp={true} + showSession={true} /> </div> ); @@ -72,7 +73,7 @@ export default class Audits extends React.Component { <h3> <FormattedMessage id='admin.audits.title' - defaultMessage='Server Audits' + defaultMessage='User Activity' /> </h3> <button diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index 539acd869..3332f37ef 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -109,7 +109,17 @@ class LicenseSettings extends React.Component { ); licenseType = ( <FormattedHTMLMessage - id='admin.license.entrepriseType' + id='admin.license.enterpriseType' + values={{ + terms: global.window.mm_config.TermsOfServiceLink, + name: global.window.mm_license.Name, + company: global.window.mm_license.Company, + users: global.window.mm_license.Users, + issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true), + start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)), + expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)), + ldap: global.window.mm_license.LDAP + }} defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p> <p>Your subscription details are as follows:</p> @@ -126,6 +136,7 @@ class LicenseSettings extends React.Component { licenseKey = ( <div className='col-sm-8'> <button + disabled={this.props.config.LdapSettings.Enable} className='btn btn-danger' onClick={this.handleRemove} id='remove-button' @@ -256,7 +267,8 @@ class LicenseSettings extends React.Component { } LicenseSettings.propTypes = { - intl: intlShape.isRequired + intl: intlShape.isRequired, + config: React.PropTypes.object }; -export default injectIntl(LicenseSettings);
\ No newline at end of file +export default injectIntl(LicenseSettings); diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx index cdca7e8d6..49892ff98 100644 --- a/web/react/components/audit_table.jsx +++ b/web/react/components/audit_table.jsx @@ -183,389 +183,388 @@ const holders = defineMessages({ loginFailure: { id: 'audit_table.loginFailure', defaultMessage: ' (Login failure)' - }, - userId: { - id: 'audit_table.userId', - defaultMessage: 'User ID' } }); class AuditTable extends React.Component { constructor(props) { super(props); - - this.handleMoreInfo = this.handleMoreInfo.bind(this); - this.formatAuditInfo = this.formatAuditInfo.bind(this); - this.handleRevokedSession = this.handleRevokedSession.bind(this); - - this.state = {moreInfo: []}; - } - handleMoreInfo(index) { - var newMoreInfo = this.state.moreInfo; - newMoreInfo[index] = true; - this.setState({moreInfo: newMoreInfo}); } - handleRevokedSession(sessionId) { - return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId}); - } - formatAuditInfo(currentAudit) { - const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, ''); + render() { + var accessList = []; const {formatMessage} = this.props.intl; - 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; - } - } + for (var i = 0; i < this.props.audits.length; i++) { + const audit = this.props.audits[i]; + const auditInfo = formatAuditInfo(audit, formatMessage); - switch (currentActionURL) { - case '/channels/create': - currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName}); - break; - case '/channels/create_direct': - currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username}); - break; - case '/channels/update': - currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName}); - break; - case '/channels/update_desc': // support the old path - case '/channels/update_header': - currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName}); - 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; - } - } + let uContent; + if (this.props.showUserId) { + uContent = <td>{auditInfo.userId}</td>; + } - if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) { - currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL}); - } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) { - currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName}); - } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) { - currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName}); - } + let iContent; + if (this.props.showIp) { + iContent = <td>{auditInfo.ip}</td>; + } - break; + let sContent; + if (this.props.showSession) { + sContent = <td>{auditInfo.sessionId}</td>; } + + let descStyle = {}; + if (auditInfo.desc.toLowerCase().indexOf('fail') !== -1) { + descStyle.color = 'red'; } - } else if (currentActionURL.indexOf('/oauth') === 0) { - const oauthInfo = currentAudit.extra_info.split(' '); - switch (currentActionURL) { - case '/oauth/register': { - const clientIdField = oauthInfo[0].split('='); + accessList[i] = ( + <tr key={audit.id}> + <td>{auditInfo.timestamp}</td> + {uContent} + <td style={descStyle}>{auditInfo.desc}</td> + {iContent} + {sContent} + </tr> + ); + } - if (clientIdField[0] === 'client_id') { - currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]}); - } + let userIdContent; + if (this.props.showUserId) { + userIdContent = ( + <th> + <FormattedMessage + id='audit_table.userId' + defaultMessage='User ID' + /> + </th> + ); + } - break; - } - case '/oauth/allow': - if (oauthInfo[0] === 'attempt') { - currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess); - } else if (oauthInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.successfullOAuthAccess); - } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') { - currentAuditDesc = formatMessage(holders.failedOAuthAccess); - } + let ipContent; + if (this.props.showIp) { + ipContent = ( + <th> + <FormattedMessage + id='audit_table.ip' + defaultMessage='IP Address' + /> + </th> + ); + } - break; - case '/oauth/access_token': - if (oauthInfo[0] === 'attempt') { - currentAuditDesc = formatMessage(holders.attemptedOAuthToken); - } else if (oauthInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.successfullOAuthToken); - } else { - const oauthTokenFailure = oauthInfo[0].split('-'); - - if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) { - currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()}); - } - } + let sessionContent; + if (this.props.showSession) { + sessionContent = ( + <th> + <FormattedMessage + id='audit_table.session' + defaultMessage='Session ID' + /> + </th> + ); + } - 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 = formatMessage(holders.attemptedLogin); - } else if (userInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.successfullLogin); - } else if (userInfo[0]) { - currentAuditDesc = formatMessage(holders.failedLogin); - } + return ( + <table className='table'> + <thead> + <tr> + <th> + <FormattedMessage + id='audit_table.timestamp' + defaultMessage='Timestamp' + /> + </th> + {userIdContent} + <th> + <FormattedMessage + id='audit_table.action' + defaultMessage='Action' + /> + </th> + {ipContent} + {sessionContent} + </tr> + </thead> + <tbody> + {accessList} + </tbody> + </table> + ); + } +} - break; - case '/users/revoke_session': - currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]); - break; - case '/users/newimage': - currentAuditDesc = formatMessage(holders.updatePicture); - break; - case '/users/update': - currentAuditDesc = formatMessage(holders.updateGeneral); - break; - case '/users/newpassword': - if (userInfo[0] === 'attempted') { - currentAuditDesc = formatMessage(holders.attemptedPassword); - } else if (userInfo[0] === 'completed') { - currentAuditDesc = formatMessage(holders.successfullPassword); - } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') { - currentAuditDesc = formatMessage(holders.failedPassword); - } +AuditTable.propTypes = { + intl: intlShape.isRequired, + audits: React.PropTypes.array.isRequired, + showUserId: React.PropTypes.bool, + showIp: React.PropTypes.bool, + showSession: React.PropTypes.bool +}; - break; - case '/users/update_roles': { - const userRoles = userInfo[0].split('=')[1]; +export default injectIntl(AuditTable); - currentAuditDesc = formatMessage(holders.updatedRol); - if (userRoles.trim()) { - currentAuditDesc += userRoles; - } else { - currentAuditDesc += formatMessage(holders.member); +export function formatAuditInfo(audit, formatMessage) { + const actionURL = audit.action.replace(/\/api\/v[1-9]/, ''); + let auditDesc = ''; + + if (actionURL.indexOf('/channels') === 0) { + const channelInfo = audit.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 (actionURL) { + case '/channels/create': + auditDesc = formatMessage(holders.channelCreated, {channelName: channelName}); + break; + case '/channels/create_direct': + auditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username}); + break; + case '/channels/update': + auditDesc = formatMessage(holders.nameUpdated, {channelName: channelName}); + break; + case '/channels/update_desc': // support the old path + case '/channels/update_header': + auditDesc = formatMessage(holders.headerUpdated, {channelName: channelName}); + 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; } + } - break; + if (/\/channels\/[A-Za-z0-9]+\/delete/.test(actionURL)) { + auditDesc = formatMessage(holders.channelDeleted, {url: channelURL}); + } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(actionURL)) { + auditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName}); + } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) { + auditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName}); } - 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 = formatMessage(holders.accountActive); - } else if (updateField === 'false') { - currentAuditDesc = formatMessage(holders.accountInactive); - } - 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 += formatMessage(holders.by, {username: actingUser.username}); - } else if (currentUser && actingUser) { - currentAuditDesc += formatMessage(holders.byAdmin); - } - } - } else if (updateType === 'session_id') { - currentAuditDesc = this.handleRevokedSession(updateField); - } + break; + } + } + } else if (actionURL.indexOf('/oauth') === 0) { + const oauthInfo = audit.extra_info.split(' '); + + switch (actionURL) { + case '/oauth/register': { + const clientIdField = oauthInfo[0].split('='); - break; + if (clientIdField[0] === 'client_id') { + auditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]}); } - case '/users/send_password_reset': - currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]}); - break; - case '/users/reset_password': - if (userInfo[0] === 'attempt') { - currentAuditDesc = formatMessage(holders.attemptedReset); - } else if (userInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.successfullReset); - } - break; - case '/users/update_notify': - currentAuditDesc = formatMessage(holders.updateGlobalNotifications); - break; - default: - break; + break; + } + case '/oauth/allow': + if (oauthInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedAllowOAuthAccess); + } else if (oauthInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullOAuthAccess); + } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') { + auditDesc = formatMessage(holders.failedOAuthAccess); } - } else if (currentActionURL.indexOf('/hooks') === 0) { - const webhookInfo = currentAudit.extra_info.split(' '); - - switch (currentActionURL) { - case '/hooks/incoming/create': - if (webhookInfo[0] === 'attempt') { - currentAuditDesc = formatMessage(holders.attemptedWebhookCreate); - } else if (webhookInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate); - } else if (webhookInfo[0] === 'fail - bad channel permissions') { - currentAuditDesc = formatMessage(holders.failedWebhookCreate); - } - break; - case '/hooks/incoming/delete': - if (webhookInfo[0] === 'attempt') { - currentAuditDesc = formatMessage(holders.attemptedWebhookDelete); - } else if (webhookInfo[0] === 'success') { - currentAuditDesc = formatMessage(holders.successfullWebhookDelete); - } else if (webhookInfo[0] === 'fail - inappropriate conditions') { - currentAuditDesc = formatMessage(holders.failedWebhookDelete); + break; + case '/oauth/access_token': + if (oauthInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedOAuthToken); + } else if (oauthInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullOAuthToken); + } else { + const oauthTokenFailure = oauthInfo[0].split('-'); + + if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) { + auditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()}); } + } - break; - default: - break; + break; + default: + break; + } + } else if (actionURL.indexOf('/users') === 0) { + const userInfo = audit.extra_info.split(' '); + + switch (actionURL) { + case '/users/login': + if (userInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedLogin); + } else if (userInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullLogin); + } else if (userInfo[0]) { + auditDesc = formatMessage(holders.failedLogin); } - } else { - switch (currentActionURL) { - case '/logout': - currentAuditDesc = formatMessage(holders.logout); - break; - case '/verify_email': - currentAuditDesc = formatMessage(holders.verified); - break; - default: - break; + + break; + case '/users/revoke_session': + auditDesc = formatMessage(holders.sessionRevoked, {sessionId: userInfo[0].split('=')[1]}); + break; + case '/users/newimage': + auditDesc = formatMessage(holders.updatePicture); + break; + case '/users/update': + auditDesc = formatMessage(holders.updateGeneral); + break; + case '/users/newpassword': + if (userInfo[0] === 'attempted') { + auditDesc = formatMessage(holders.attemptedPassword); + } else if (userInfo[0] === 'completed') { + auditDesc = formatMessage(holders.successfullPassword); + } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') { + auditDesc = formatMessage(holders.failedPassword); } - } - /* If all else fails... */ - if (!currentAuditDesc) { - /* Currently not called anywhere */ - if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) { - currentAuditDesc = formatMessage(holders.revokedAll); + break; + case '/users/update_roles': { + const userRoles = userInfo[0].split('=')[1]; + + auditDesc = formatMessage(holders.updatedRol); + if (userRoles.trim()) { + auditDesc += userRoles; } else { - let currentActionDesc = ''; - if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) { - currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' '); - currentActionDesc = Utils.toTitleCase(currentActionDesc); - } + auditDesc += formatMessage(holders.member); + } - let currentExtraInfoDesc = ''; - if (currentAudit.extra_info) { - currentExtraInfoDesc = currentAudit.extra_info; + 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') { + auditDesc = formatMessage(holders.accountActive); + } else if (updateField === 'false') { + auditDesc = formatMessage(holders.accountInactive); + } - if (currentExtraInfoDesc.indexOf('=') !== -1) { - currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1); + const actingUserInfo = userInfo[1].split('='); + if (actingUserInfo[0] === 'session_user') { + const actingUser = UserStore.getProfile(actingUserInfo[1]); + const user = UserStore.getCurrentUser(); + if (user && actingUser && (Utils.isAdmin(user.roles) || Utils.isSystemAdmin(user.roles))) { + auditDesc += formatMessage(holders.by, {username: actingUser.username}); + } else if (user && actingUser) { + auditDesc += formatMessage(holders.byAdmin); } } - currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc; + } else if (updateType === 'session_id') { + auditDesc = formatMessage(holders.sessionRevoked, {sessionId: updateField}); } - } - const currentDate = new Date(currentAudit.create_at); - let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}); + break; + } + case '/users/send_password_reset': + auditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]}); + break; + case '/users/reset_password': + if (userInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedReset); + } else if (userInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullReset); + } - if (this.props.showUserId) { - currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id; + break; + case '/users/update_notify': + auditDesc = formatMessage(holders.updateGlobalNotifications); + break; + default: + break; } + } else if (actionURL.indexOf('/hooks') === 0) { + const webhookInfo = audit.extra_info.split(' '); + + switch (actionURL) { + case '/hooks/incoming/create': + if (webhookInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedWebhookCreate); + } else if (webhookInfo[0] === 'success') { + auditDesc = formatMessage(holders.succcessfullWebhookCreate); + } else if (webhookInfo[0] === 'fail - bad channel permissions') { + auditDesc = formatMessage(holders.failedWebhookCreate); + } - currentAuditInfo += ' | ' + currentAuditDesc; + break; + case '/hooks/incoming/delete': + if (webhookInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedWebhookDelete); + } else if (webhookInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullWebhookDelete); + } else if (webhookInfo[0] === 'fail - inappropriate conditions') { + auditDesc = formatMessage(holders.failedWebhookDelete); + } - return currentAuditInfo; + break; + default: + break; + } + } else { + switch (actionURL) { + case '/logout': + auditDesc = formatMessage(holders.logout); + break; + case '/verify_email': + auditDesc = formatMessage(holders.verified); + break; + default: + break; + } } - render() { - var accessList = []; - const {formatMessage} = this.props.intl; - for (var i = 0; i < this.props.audits.length; i++) { - const currentAudit = this.props.audits[i]; - const currentAuditInfo = this.formatAuditInfo(currentAudit); - - let moreInfo; - if (!this.props.oneLine) { - moreInfo = ( - <a - href='#' - className='theme' - onClick={this.handleMoreInfo.bind(this, i)} - > - <FormattedMessage - id='audit_table.moreInfo' - defaultMessage='More info' - /> - </a> - ); + /* If all else fails... */ + if (!auditDesc) { + /* Currently not called anywhere */ + if (audit.extra_info.indexOf('revoked_all=') >= 0) { + auditDesc = formatMessage(holders.revokedAll); + } else { + let actionDesc = ''; + if (actionURL && actionURL.lastIndexOf('/') !== -1) { + actionDesc = actionURL.substring(actionURL.lastIndexOf('/') + 1).replace('_', ' '); + actionDesc = Utils.toTitleCase(actionDesc); } - if (this.state.moreInfo[i]) { - if (!currentAudit.session_id) { - currentAudit.session_id = 'N/A'; + let extraInfoDesc = ''; + if (audit.extra_info) { + extraInfoDesc = audit.extra_info; - if (currentAudit.action.search('/users/login') >= 0) { - if (currentAudit.extra_info === 'attempt') { - currentAudit.session_id += formatMessage(holders.loginAttempt); - } else { - currentAudit.session_id += formatMessage(holders.loginFailure); - } - } + if (extraInfoDesc.indexOf('=') !== -1) { + extraInfoDesc = extraInfoDesc.substring(extraInfoDesc.indexOf('=') + 1); } - - moreInfo = ( - <div> - <div> - <FormattedMessage - id='audit_table.ip' - defaultMessage='IP: {ip}' - values={{ - ip: currentAudit.ip_address - }} - /> - </div> - <div> - <FormattedMessage - id='audit_table.session' - defaultMessage='Session ID: {id}' - values={{ - id: currentAudit.session_id - }} - /> - </div> - </div> - ); } - - var divider = null; - if (i < this.props.audits.length - 1) { - divider = (<div className='divider-light'></div>); - } - - accessList[i] = ( - <div - key={'accessHistoryEntryKey' + i} - className='access-history__table' - > - <div className='access__report'> - <div className='report__time'>{currentAuditInfo}</div> - <div className='report__info'> - {moreInfo} - </div> - {divider} - </div> - </div> - ); + auditDesc = actionDesc + ' ' + extraInfoDesc; } - - return <form role='form'>{accessList}</form>; } -} -AuditTable.propTypes = { - intl: intlShape.isRequired, - audits: React.PropTypes.array.isRequired, - oneLine: React.PropTypes.bool, - showUserId: React.PropTypes.bool -}; + const date = new Date(audit.create_at); + let auditInfo = {}; + auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}); + auditInfo.userId = audit.user_id; + auditInfo.desc = auditDesc; + auditInfo.ip = audit.ip_address; + auditInfo.sessionId = audit.session_id; -export default injectIntl(AuditTable); + return auditInfo; +} diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 38011e38f..6ea80cd13 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -40,10 +40,6 @@ const holders = defineMessages({ write: { id: 'create_post.write', defaultMessage: 'Write a message...' - }, - deleteMsg: { - id: 'create_post.deleteMsg', - defaultMessage: '(message deleted)' } }); @@ -69,7 +65,6 @@ class CreatePost extends React.Component { this.sendMessage = this.sendMessage.bind(this); PostStore.clearDraftUploads(); - PostStore.deleteMessage(this.props.intl.formatMessage(holders.deleteMsg)); const draft = this.getCurrentDraft(); diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 34fd724f5..9d7dcb3e5 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -88,7 +88,7 @@ export default class DeletePostModal extends React.Component { } } - PostStore.removePost(this.state.post.id, this.state.post.channel_id); + PostStore.deletePost(this.state.post); AsyncClient.getPosts(this.state.post.channel_id); }, (err) => { diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 16f8528b2..d71ac6ec7 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -44,7 +44,6 @@ class PostBody extends React.Component { this.state = { links: linkData.links, - message: linkData.text, post: this.props.post, hasUserProfiles: profiles && Object.keys(profiles).length > 1 }; @@ -106,7 +105,9 @@ class PostBody extends React.Component { if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) { this.embed = this.createEmbed(linkData.links[0]); } - this.setState({links: linkData.links, message: linkData.text}); + this.setState({ + links: linkData.links + }); } createEmbed(link) { @@ -310,6 +311,23 @@ class PostBody extends React.Component { ); } + let message; + if (this.props.post.state === Constants.POST_DELETED) { + message = ( + <FormattedMessage + id='post_body.deleted' + defaultMessage='(message deleted)' + /> + ); + } else { + message = ( + <span + onClick={TextFormatting.handleClick} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}} + /> + ); + } + return ( <div> {comment} @@ -320,11 +338,7 @@ class PostBody extends React.Component { className={postClass} > {loading} - <span - ref='message_span' - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}} - /> + {message} </div> <PostBodyAdditionalContent post={this.state.post} @@ -346,4 +360,4 @@ PostBody.propTypes = { handleCommentClick: React.PropTypes.func.isRequired }; -export default injectIntl(PostBody);
\ No newline at end of file +export default injectIntl(PostBody); diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ddb393520..b1bc8ca14 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -23,13 +23,14 @@ export default class PostInfo extends React.Component { }; this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this); + this.removePost = this.removePost.bind(this); } createDropdown() { var post = this.props.post; var isOwner = UserStore.getCurrentId() === post.user_id; var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); - if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) { + if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; } @@ -166,6 +167,25 @@ export default class PostInfo extends React.Component { this.setState({copiedLink: false}); } } + removePost() { + EventHelpers.emitRemovePost(this.props.post); + } + createRemovePostButton(post) { + if (!Utils.isPostEphemeral(post)) { + return null; + } + + return ( + <a + href='#' + className='post__remove theme' + type='button' + onClick={this.removePost} + > + {'×'} + </a> + ); + } render() { var post = this.props.post; var comments = ''; @@ -178,7 +198,7 @@ export default class PostInfo extends React.Component { commentCountText = ''; } - if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) { + if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) { comments = ( <a href='#' @@ -264,6 +284,7 @@ export default class PostInfo extends React.Component { > {permalinkOverlay} </Overlay> + {this.createRemovePostButton(post)} </li> </ul> ); diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index 5eb319320..c1041e438 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -9,6 +9,7 @@ import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as AsyncClient from '../utils/async_client.jsx'; import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; export function emitChannelClickEvent(channel) { AsyncClient.getChannels(true); @@ -180,3 +181,27 @@ export function emitPreferenceChangedEvent(preference) { preference }); } + +export function emitRemovePost(post) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.REMOVE_POST, + post + }); +} + +export function sendEphemeralPost(message, channelId) { + const timestamp = Utils.getTimestamp(); + const post = { + id: Utils.generateId(), + user_id: '0', + channel_id: channelId || ChannelStore.getCurrentId(), + message, + type: Constants.POST_TYPE_EPHEMERAL, + create_at: timestamp, + update_at: timestamp, + filenames: [], + props: {} + }; + + emitPostRecievedEvent(post); +} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 08ffef822..8ff58f685 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -57,6 +57,7 @@ class PostStoreClass extends EventEmitter { this.clearFocusedPost = this.clearFocusedPost.bind(this); this.clearChannelVisibility = this.clearChannelVisibility.bind(this); + this.deletePost = this.deletePost.bind(this); this.removePost = this.removePost.bind(this); this.getPendingPosts = this.getPendingPosts.bind(this); @@ -65,10 +66,6 @@ class PostStoreClass extends EventEmitter { this.clearPendingPosts = this.clearPendingPosts.bind(this); this.updatePendingPost = this.updatePendingPost.bind(this); - this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); - this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); - this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); - // These functions are bad and work should be done to remove this system when the RHS dies this.storeSelectedPost = this.storeSelectedPost.bind(this); this.getSelectedPost = this.getSelectedPost.bind(this); @@ -211,28 +208,6 @@ class PostStoreClass extends EventEmitter { postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); } - // Add deleted posts - if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { - Object.assign(postList.posts, this.postsInfo[id].deletedPosts); - - for (const postID in this.postsInfo[id].deletedPosts) { - if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) { - postList.order.push(postID); - } - } - - // Merge would be faster - postList.order.sort((a, b) => { - if (postList.posts[a].create_at > postList.posts[b].create_at) { - return -1; - } - if (postList.posts[a].create_at < postList.posts[b].create_at) { - return 1; - } - return 0; - }); - } - return postList; } @@ -286,15 +261,6 @@ class PostStoreClass extends EventEmitter { if (combinedPosts.order.indexOf(pid) === -1) { combinedPosts.order.push(pid); } - } else { - if (pid in combinedPosts.posts) { - Reflect.deleteProperty(combinedPosts.posts, pid); - } - - const index = combinedPosts.order.indexOf(pid); - if (index !== -1) { - combinedPosts.order.splice(index, 1); - } } } } @@ -365,6 +331,22 @@ class PostStoreClass extends EventEmitter { this.postsInfo[id].atBottom = atBottom; } + deletePost(post) { + const postList = this.postsInfo[post.channel_id].postList; + + if (isPostListNull(postList)) { + return; + } + + if (post.id in postList.posts) { + // make sure to copy the post so that component state changes work properly + postList.posts[post.id] = Object.assign({}, post, { + state: Constants.POST_DELETED, + filenames: [] + }); + } + } + removePost(post) { const channelId = post.channel_id; this.makePostsInfo(channelId); @@ -439,37 +421,6 @@ class PostStoreClass extends EventEmitter { this.emitChange(); } - storeUnseenDeletedPost(post) { - let posts = this.getUnseenDeletedPosts(post.channel_id); - - if (!posts) { - posts = {}; - } - - post.message = this.delete_message; - post.state = Constants.POST_DELETED; - post.filenames = []; - - posts[post.id] = post; - - this.makePostsInfo(post.channel_id); - this.postsInfo[post.channel_id].deletedPosts = posts; - } - - getUnseenDeletedPosts(channelId) { - if (this.postsInfo.hasOwnProperty(channelId)) { - return this.postsInfo[channelId].deletedPosts; - } - - return null; - } - - clearUnseenDeletedPosts(channelId) { - if (this.postsInfo.hasOwnProperty(channelId)) { - Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts'); - } - } - storeSelectedPost(postList) { this.selectedPost = postList; } @@ -581,9 +532,6 @@ class PostStoreClass extends EventEmitter { return commentCount; } - deleteMessage(msg) { - this.delete_message = msg; - } } var PostStore = new PostStoreClass(); @@ -615,7 +563,6 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { case ActionTypes.CLICK_CHANNEL: PostStore.clearFocusedPost(); PostStore.clearChannelVisibility(action.id, true); - PostStore.clearUnseenDeletedPosts(action.prev); break; case ActionTypes.CREATE_POST: PostStore.storePendingPost(action.post); @@ -623,7 +570,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { PostStore.jumpPostsViewToBottom(); break; case ActionTypes.POST_DELETED: - PostStore.storeUnseenDeletedPost(action.post); + PostStore.deletePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.REMOVE_POST: PostStore.removePost(action.post); PostStore.emitChange(); break; diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 744c2c8e5..33604f44b 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -109,6 +109,7 @@ class SocketStoreClass extends EventEmitter { handleMessage(msg) { switch (msg.action) { case SocketEvents.POSTED: + case SocketEvents.EPHEMERAL_MESSAGE: handleNewPostEvent(msg, this.translations); break; @@ -179,7 +180,6 @@ function handleNewPostEvent(msg, translations) { mentions = JSON.parse(msg.props.mentions); } - const channelType = msgProps.channel_type; const channel = ChannelStore.get(msg.channel_id); const user = UserStore.getCurrentUser(); const member = ChannelStore.getMember(msg.channel_id); @@ -191,7 +191,7 @@ function handleNewPostEvent(msg, translations) { if (notifyLevel === 'none') { return; - } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channelType !== Constants.DM_CHANNEL) { + } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) { return; } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 11a8da669..c1bd41b88 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -12,6 +12,7 @@ export default { LEAVE_CHANNEL: null, CREATE_POST: null, POST_DELETED: null, + REMOVE_POST: null, RECIEVED_CHANNELS: null, RECIEVED_CHANNEL: null, @@ -78,7 +79,8 @@ export default { USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', TYPING: 'typing', - PREFERENCE_CHANGED: 'preference_changed' + PREFERENCE_CHANGED: 'preference_changed', + EPHEMERAL_MESSAGE: 'ephemeral_message' }, //SPECIAL_MENTIONS: ['all', 'channel'], @@ -126,6 +128,7 @@ export default { POST_LOADING: 'loading', POST_FAILED: 'failed', POST_DELETED: 'deleted', + POST_TYPE_EPHEMERAL: 'system_ephemeral', POST_TYPE_JOIN_LEAVE: 'system_join_leave', SYSTEM_MESSAGE_PREFIX: 'system_', SYSTEM_MESSAGE_PROFILE_NAME: 'System', diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 494c38bdb..e8cfc82bc 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -1355,3 +1355,7 @@ export function languages() { ] ); } + +export function isPostEphemeral(post) { + return post.type === Constants.POST_TYPE_EPHEMERAL || post.state === Constants.POST_DELETED; +} |