diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js index 9bc9cc703..0272c1535 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js @@ -54,6 +54,15 @@ const withProfileQuery = withQuery( me { id username + state { + status { + username { + history { + created_at + } + } + } + } } ...${getDefinitionName(TabPanel.fragments.root)} ${getSlotFragmentSpreads(slots, 'root')} diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index f50fca335..610557542 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -1,4 +1,5 @@ import get from 'lodash/get'; +import moment from 'moment'; /** * getReliability @@ -33,3 +34,18 @@ export const isSuspended = user => { export const isBanned = user => { return get(user, 'state.status.banned.status'); }; + +/** + * canUsernameBeUpdated + * retrieves boolean whether a username can be updated or not + */ + +export const canUsernameBeUpdated = status => { + const oldestEditTime = moment() + .subtract(14, 'days') + .toDate(); + + return !status.username.history.some(({ created_at }) => + moment(created_at).isAfter(oldestEditTime) + ); +}; diff --git a/perms/reducers/mutation.js b/perms/reducers/mutation.js index f063fe1c2..44f66cf88 100644 --- a/perms/reducers/mutation.js +++ b/perms/reducers/mutation.js @@ -14,25 +14,21 @@ module.exports = (user, perm) => { user.password.length > 0 ); - case types.CHANGE_USERNAME: { + case types.CHANGE_USERNAME: + return user.status.username.status === 'REJECTED'; + + case types.SET_USERNAME: { // Only users who have their usernames rejected or those users who // not changed their usernames within 14 days can change their usernames. - const now = moment(); + const deadline = moment().subtract(14, 'days'); return ( - user.status.username.status === 'REJECTED' || - get(user, 'status.username.history', []) - .filter(({ status }) => status === 'CHANGED') - .every(({ created_at }) => - moment(created_at) - .add(14, 'days') - .isAfter(now) - ) + user.status.username.status === 'UNSET' || + get(user, 'status.username.history', []).every(({ created_at }) => + moment(created_at).isBefore(deadline) + ) ); } - case types.SET_USERNAME: - return user.status.username.status === 'UNSET'; - case types.CREATE_COMMENT: case types.CREATE_ACTION: case types.DELETE_ACTION: diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameContentDialog.js b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameContentDialog.js index 36edd7a59..f06fe5acd 100644 --- a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameContentDialog.js +++ b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameContentDialog.js @@ -19,10 +19,20 @@ class ChangeUsernameContentDialog extends React.Component { confirmChanges = async () => { if (this.formHasError()) { this.showError(); - } else { - await this.props.save(); - this.props.next(); + return; } + + if (!this.props.canUsernameBeUpdated) { + this.props.notify( + 'error', + t('talk-plugin-auth.change_username.change_username_attempt') + ); + return; + } + + await this.props.save(); + this.props.next(); + this.props.closeDialog(); }; formHasError = () => @@ -97,6 +107,8 @@ ChangeUsernameContentDialog.propTypes = { onChange: PropTypes.func, formData: PropTypes.object, username: PropTypes.string, + canUsernameBeUpdated: PropTypes.bool.isRequired, + notify: PropTypes.func.isRequired, }; export default ChangeUsernameContentDialog; diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/Profile.js b/plugins/talk-plugin-auth/client/profile-settings/components/Profile.js index 9ce3b0498..a05a863bc 100644 --- a/plugins/talk-plugin-auth/client/profile-settings/components/Profile.js +++ b/plugins/talk-plugin-auth/client/profile-settings/components/Profile.js @@ -11,6 +11,7 @@ import errorMsj from 'coral-framework/helpers/error'; import ConfirmChangesDialog from './ConfirmChangesDialog'; import ChangeUsernameContentDialog from './ChangeUsernameContentDialog'; import ChangeEmailContentDialog from './ChangeEmailContentDialog'; +import { canUsernameBeUpdated } from 'coral-framework/utils/user'; const initialState = { editing: false, @@ -120,13 +121,10 @@ class Profile extends React.Component { saveUsername = async () => { const { newUsername } = this.state.formData; - const { id } = this.props; + const { setUsername } = this.props; try { - await this.props.changeUsername({ - id, - username: newUsername, - }); + await setUsername(newUsername); this.props.notify( 'success', t('talk-plugin-auth.change_username.changed_username_success_msg') @@ -159,8 +157,13 @@ class Profile extends React.Component { }; render() { - const { username, emailAddress } = this.props; - const { editing, formData } = this.state; + const { + username, + emailAddress, + root: { me: { state: { status } } }, + notify, + } = this.props; + const { editing, formData, showDialog } = this.state; return (
{username !== formData.newUsername && ( - oldestEditTime.isAfter(created_at) + user.status.username.history.some(({ created_at }) => + moment(created_at).isAfter(oldestEditTime) ) ) { throw new ErrPermissionUpdateUsername(); diff --git a/test/server/graph/mutations/changeUsername.js b/test/server/graph/mutations/changeUsername.js index a5c3daed8..eaff85434 100644 --- a/test/server/graph/mutations/changeUsername.js +++ b/test/server/graph/mutations/changeUsername.js @@ -89,7 +89,7 @@ describe('graph.mutations.changeUsername', () => { expect(res.data.changeUsername.errors).to.have.length(1); expect(res.data.changeUsername.errors[0]).to.have.property( 'translation_key', - 'EDIT_USERNAME_NOT_AUTHORIZED' + 'NOT_AUTHORIZED' ); // Set the user to the desired status. diff --git a/test/server/services/users.js b/test/server/services/users.js index a7d7475e9..2b9210f83 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -304,63 +304,63 @@ describe('services.UsersService', () => { await UsersService[func](user.id, user.username); } }); - }); - if (func === 'setUsername') { - it('should let a user set their username from UNSET', async () => { - const user = mockUsers[0]; + if (func === 'setUsername') { + it('should let a user set their username from UNSET', async () => { + const user = mockUsers[0]; - // Set the user to the desired status. - await UsersService.setUsernameStatus(user.id, 'UNSET'); - await UsersService.setUsername(user.id, 'new_username', null); - }); - - describe('time based', () => { - afterEach(() => { - timekeeper.reset(); + // Set the user to the desired status. + await UsersService.setUsernameStatus(user.id, 'UNSET'); + await UsersService.setUsername(user.id, 'new_username', null); }); - ['SET', 'APPROVED'].forEach(status => { - it(`should not allow users to change their username if it was changed within 14 of today from ${status}`, async () => { - const user = mockUsers[0]; + describe('time based', () => { + afterEach(() => { + timekeeper.reset(); + }); - // Set the user to the desired status. - await UsersService.setUsernameStatus(user.id, status); + ['SET', 'APPROVED'].forEach(status => { + it(`should not allow users to change their username if it was changed within 14 of today from ${status}`, async () => { + const user = mockUsers[0]; - timekeeper.travel( - moment() - .add(5, 'days') - .toDate() - ); + // Set the user to the desired status. + await UsersService.setUsernameStatus(user.id, status); - try { - await UsersService.setUsername(user.id, 'new_username', null); - throw new Error('edit was processed successfully'); - } catch (err) { - expect(err).have.property( - 'translation_key', - 'EDIT_USERNAME_NOT_AUTHORIZED' + timekeeper.travel( + moment() + .add(5, 'days') + .toDate() ); - } - }); - it(`allows users to change their username if it was changed 14 days before today from ${status}`, async () => { - const user = mockUsers[0]; + try { + await UsersService.setUsername(user.id, 'new_username', null); + throw new Error('edit was processed successfully'); + } catch (err) { + expect(err).have.property( + 'translation_key', + 'EDIT_USERNAME_NOT_AUTHORIZED' + ); + } + }); - // Set the user to the desired status. - await UsersService.setUsernameStatus(user.id, status); + it(`allows users to change their username if it was changed 14 days before today from ${status}`, async () => { + const user = mockUsers[0]; - timekeeper.travel( - moment() - .add(15, 'days') - .toDate() - ); + // Set the user to the desired status. + await UsersService.setUsernameStatus(user.id, status); - await UsersService.setUsername(user.id, 'new_username', null); + timekeeper.travel( + moment() + .add(15, 'days') + .toDate() + ); + + await UsersService.setUsername(user.id, 'new_username', null); + }); }); }); - }); - } + } + }); }); describe('#isValidUsername', () => {