mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'master' into configure-enh
This commit is contained in:
@@ -4,13 +4,32 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import styles from './Profile.css';
|
||||
import TabPanel from '../containers/TabPanel';
|
||||
|
||||
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
const DefaultProfileHeader = ({ username, emailAddress }) => (
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
DefaultProfileHeader.propTypes = {
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
const Profile = ({ id, username, emailAddress, root, slotPassthrough }) => {
|
||||
return (
|
||||
<div className="talk-my-profile talk-profile-container">
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
<Slot
|
||||
fill="profileHeader"
|
||||
size={1}
|
||||
defaultComponent={DefaultProfileHeader}
|
||||
passthrough={{
|
||||
...slotPassthrough,
|
||||
id,
|
||||
username,
|
||||
emailAddress,
|
||||
}}
|
||||
/>
|
||||
<Slot fill="profileSections" passthrough={slotPassthrough} />
|
||||
<TabPanel root={root} slotPassthrough={slotPassthrough} />
|
||||
</div>
|
||||
@@ -18,6 +37,7 @@ const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
};
|
||||
|
||||
Profile.propTypes = {
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
|
||||
@@ -30,6 +30,7 @@ class ProfileContainer extends Component {
|
||||
|
||||
return (
|
||||
<Profile
|
||||
id={me.id}
|
||||
username={me.username}
|
||||
emailAddress={emailAddress}
|
||||
root={root}
|
||||
@@ -53,6 +54,15 @@ const withProfileQuery = withQuery(
|
||||
me {
|
||||
id
|
||||
username
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
...${getDefinitionName(TabPanel.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { isString } = require('lodash');
|
||||
const { get, isString } = require('lodash');
|
||||
const moment = require('moment');
|
||||
const { check } = require('../utils');
|
||||
const types = require('../constants');
|
||||
|
||||
@@ -12,11 +13,21 @@ module.exports = (user, perm) => {
|
||||
isString(user.password) &&
|
||||
user.password.length > 0
|
||||
);
|
||||
|
||||
case types.CHANGE_USERNAME:
|
||||
return user.status.username.status === 'REJECTED';
|
||||
|
||||
case types.SET_USERNAME:
|
||||
return user.status.username.status === 'UNSET';
|
||||
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 deadline = moment().subtract(14, 'days');
|
||||
return (
|
||||
user.status.username.status === 'UNSET' ||
|
||||
get(user, 'status.username.history', []).every(({ created_at }) =>
|
||||
moment(created_at).isBefore(deadline)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
case types.CREATE_COMMENT:
|
||||
case types.CREATE_ACTION:
|
||||
|
||||
@@ -26,5 +26,6 @@ export {
|
||||
withStopIgnoringUser,
|
||||
withSetCommentStatus,
|
||||
withChangePassword,
|
||||
withChangeUsername,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
export { compose } from 'recompose';
|
||||
|
||||
@@ -5,6 +5,7 @@ import translations from './translations.yml';
|
||||
import Login from './login/containers/Main';
|
||||
import reducer from './login/reducer';
|
||||
import ChangePassword from './profile-settings/containers/ChangePassword';
|
||||
import ChangeUsername from './profile-settings/containers/ChangeUsername';
|
||||
|
||||
export default {
|
||||
reducer,
|
||||
@@ -12,6 +13,7 @@ export default {
|
||||
slots: {
|
||||
stream: [UserBox, SignInButton, SetUsernameDialog],
|
||||
login: [Login],
|
||||
profileHeader: [ChangeUsername],
|
||||
profileSettings: [ChangePassword],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
border: 1px solid #787d80;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 1em;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import Form from './Form';
|
||||
import InputField from './InputField';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
|
||||
@@ -148,7 +147,7 @@ class ChangePassword extends React.Component {
|
||||
{t('talk-plugin-auth.change_password.change_password')}
|
||||
</h3>
|
||||
{editing && (
|
||||
<Form className="talk-plugin-auth--change-password-form">
|
||||
<form className="talk-plugin-auth--change-password-form">
|
||||
<InputField
|
||||
id="oldPassword"
|
||||
label="Old Password"
|
||||
@@ -188,7 +187,7 @@ class ChangePassword extends React.Component {
|
||||
errorMsg={errors['confirmNewPassword']}
|
||||
showErrors
|
||||
/>
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
{editing ? (
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
.container {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
color: #202020;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
|
||||
&.editing {
|
||||
background-color: #EDEDED;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.email {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid #787d80;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
background-color: #3498DB;
|
||||
border-color: #3498DB;
|
||||
color: white;
|
||||
|
||||
> i {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #399ee2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border-color: #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
color: #4f5c67;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
color:#787D80;
|
||||
margin-top: 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
border: solid 1px #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
width: 230px;
|
||||
display: flex;
|
||||
|
||||
> .detailLabelIcon {
|
||||
font-size: 1.2em;
|
||||
padding: 0 5px;
|
||||
color: #787D80;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
color: #000;
|
||||
height: 30px;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bottomText {
|
||||
color: #474747;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.detailList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ChangeUsername.css';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import ChangeUsernameDialog from './ChangeUsernameDialog';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import InputField from './InputField';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import { canUsernameBeUpdated } from 'coral-framework/utils/user';
|
||||
|
||||
const initialState = {
|
||||
editing: false,
|
||||
showDialog: false,
|
||||
formData: {},
|
||||
};
|
||||
|
||||
class ChangeUsername extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
clearForm = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
enableEditing = () => {
|
||||
this.setState({
|
||||
editing: true,
|
||||
});
|
||||
};
|
||||
|
||||
disableEditing = () => {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.clearForm();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
showDialog = () => {
|
||||
this.setState({
|
||||
showDialog: true,
|
||||
});
|
||||
};
|
||||
|
||||
onSave = async () => {
|
||||
this.showDialog();
|
||||
};
|
||||
|
||||
saveChanges = async () => {
|
||||
const { newUsername } = this.state.formData;
|
||||
const { changeUsername } = this.props;
|
||||
|
||||
try {
|
||||
await changeUsername(newUsername);
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-auth.change_username.changed_username_success_msg')
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
|
||||
this.clearForm();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
this.setState(state => ({
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
closeDialog = () => {
|
||||
this.setState({
|
||||
showDialog: false,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
username,
|
||||
emailAddress,
|
||||
root: { me: { state: { status } } },
|
||||
notify,
|
||||
} = this.props;
|
||||
const { editing, formData, showDialog } = this.state;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn('talk-plugin-auth--edit-profile', styles.container, {
|
||||
[styles.editing]: editing,
|
||||
})}
|
||||
>
|
||||
<ChangeUsernameDialog
|
||||
canUsernameBeUpdated={canUsernameBeUpdated(status)}
|
||||
showDialog={showDialog}
|
||||
onChange={this.onChange}
|
||||
formData={formData}
|
||||
username={username}
|
||||
closeDialog={this.closeDialog}
|
||||
saveChanges={this.saveChanges}
|
||||
notify={notify}
|
||||
/>
|
||||
|
||||
{editing ? (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.detailList}>
|
||||
<InputField
|
||||
icon="person"
|
||||
id="newUsername"
|
||||
name="newUsername"
|
||||
onChange={this.onChange}
|
||||
defaultValue={username}
|
||||
columnDisplay
|
||||
validationType="username"
|
||||
>
|
||||
<span className={styles.bottomText}>
|
||||
{t('talk-plugin-auth.change_username.change_username_note')}
|
||||
</span>
|
||||
</InputField>
|
||||
<InputField
|
||||
icon="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={emailAddress}
|
||||
validationType="username"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? (
|
||||
<p className={styles.email}>{emailAddress}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{editing ? (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.saveButton)}
|
||||
icon="save"
|
||||
onClick={this.onSave}
|
||||
disabled={
|
||||
!this.state.formData.newUsername ||
|
||||
this.state.formData.newUsername === username
|
||||
}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.save')}
|
||||
</Button>
|
||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
||||
{t('talk-plugin-auth.change_username.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="settings"
|
||||
onClick={this.enableEditing}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.edit_profile')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUsername.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
changeUsername: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ChangeUsername;
|
||||
@@ -0,0 +1,84 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 320px;
|
||||
top: 10px;
|
||||
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.3em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1em;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
color: #4C4C4D;
|
||||
font-size: 1em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.bottomNote {
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
padding-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bottomActions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.usernamesChange {
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
border: 1px solid #787d80;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmChanges {
|
||||
background-color: #3498DB;
|
||||
border-color: #3498DB;
|
||||
color: white;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
background-color: #3ba3ec;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './ChangeUsernameDialog.css';
|
||||
import InputField from './InputField';
|
||||
import { Button, Dialog } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
class ChangeUsernameDialog extends React.Component {
|
||||
state = {
|
||||
showError: false,
|
||||
};
|
||||
|
||||
showError = () => {
|
||||
this.setState({
|
||||
showError: true,
|
||||
});
|
||||
};
|
||||
|
||||
confirmChanges = async () => {
|
||||
if (this.formHasError()) {
|
||||
this.showError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.props.canUsernameBeUpdated) {
|
||||
this.props.notify(
|
||||
'error',
|
||||
t('talk-plugin-auth.change_username.change_username_attempt')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.props.saveChanges();
|
||||
this.props.closeDialog();
|
||||
};
|
||||
|
||||
formHasError = () =>
|
||||
this.props.formData.confirmNewUsername !== this.props.formData.newUsername;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.props.showDialog}
|
||||
className={cn(styles.dialog, 'talk-plugin-auth--edit-profile-dialog')}
|
||||
>
|
||||
<span className={styles.close} onClick={this.props.closeDialog}>
|
||||
×
|
||||
</span>
|
||||
<h1 className={styles.title}>
|
||||
{t('talk-plugin-auth.change_username.confirm_username_change')}
|
||||
</h1>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.description}>
|
||||
{t('talk-plugin-auth.change_username.description')}
|
||||
</p>
|
||||
<div className={styles.usernamesChange}>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-auth.change_username.old_username')}:{' '}
|
||||
{this.props.username}
|
||||
</span>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-auth.change_username.new_username')}:{' '}
|
||||
{this.props.formData.newUsername}
|
||||
</span>
|
||||
</div>
|
||||
<form>
|
||||
<InputField
|
||||
id="confirmNewUsername"
|
||||
label="Re-enter new username"
|
||||
name="confirmNewUsername"
|
||||
type="text"
|
||||
onChange={this.props.onChange}
|
||||
defaultValue=""
|
||||
hasError={this.formHasError() && this.state.showError}
|
||||
errorMsg={t(
|
||||
'talk-plugin-auth.change_username.username_does_not_match'
|
||||
)}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
showSuccess={false}
|
||||
validationType="username"
|
||||
>
|
||||
<span className={styles.bottomNote}>
|
||||
{t('talk-plugin-auth.change_username.bottom_note')}
|
||||
</span>
|
||||
</InputField>
|
||||
</form>
|
||||
<div className={styles.bottomActions}>
|
||||
<Button className={styles.cancel}>
|
||||
{t('talk-plugin-auth.change_username.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.confirmChanges}
|
||||
onClick={this.confirmChanges}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.confirm_changes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUsernameDialog.propTypes = {
|
||||
saveChanges: PropTypes.func,
|
||||
closeDialog: PropTypes.func,
|
||||
showDialog: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
username: PropTypes.string,
|
||||
formData: PropTypes.object,
|
||||
canUsernameBeUpdated: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ChangeUsernameDialog;
|
||||
@@ -1,6 +1,5 @@
|
||||
.errorMsg {
|
||||
color: #FA4643;
|
||||
padding-left: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.detailList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Form.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Form = ({ children, className = '' }) => (
|
||||
<form className={className}>
|
||||
<ul className={styles.detailList}>{children}</ul>
|
||||
</form>
|
||||
);
|
||||
|
||||
Form.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Form;
|
||||
@@ -7,8 +7,38 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.columnDisplay {
|
||||
flex-direction: column;
|
||||
|
||||
.detailItemMessage {
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailItemContent {
|
||||
min-width: 280px;
|
||||
border: solid 1px #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
width: 230px;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .detailIcon {
|
||||
font-size: 1.2em;
|
||||
padding: 0 5px;
|
||||
color: #787D80;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: solid 2px #FA4643;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
@@ -19,22 +49,21 @@
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
padding: 6px 2px;
|
||||
border: solid 1px #979797;
|
||||
display: block;
|
||||
font-size: 1.1em;
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
color: #979797;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
color: #000;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailItemMessage {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2px;
|
||||
padding-left: 6px;
|
||||
padding-top: 16px;
|
||||
|
||||
.warningIcon, .checkIcon {
|
||||
@@ -48,4 +77,4 @@
|
||||
|
||||
.warningIcon {
|
||||
color: #FA4643;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './InputField.css';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
@@ -10,51 +11,84 @@ const InputField = ({
|
||||
type = 'text',
|
||||
name = '',
|
||||
onChange = () => {},
|
||||
value = '',
|
||||
showError = true,
|
||||
hasError = false,
|
||||
errorMsg = '',
|
||||
children,
|
||||
columnDisplay = false,
|
||||
showSuccess = false,
|
||||
validationType = '',
|
||||
icon = '',
|
||||
value = '',
|
||||
defaultValue = '',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const inputValue = {
|
||||
...(value ? { value } : {}),
|
||||
...(defaultValue ? { defaultValue } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={styles.detailItem}>
|
||||
<div className={styles.detailItemContainer}>
|
||||
<div className={styles.detailItemContent}>
|
||||
<div className={styles.detailItem}>
|
||||
<div
|
||||
className={cn(styles.detailItemContainer, {
|
||||
[styles.columnDisplay]: columnDisplay,
|
||||
})}
|
||||
>
|
||||
{label && (
|
||||
<label className={styles.detailLabel} id={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
styles.detailItemContent,
|
||||
{ [styles.error]: hasError },
|
||||
{ [styles.disabled]: disabled }
|
||||
)}
|
||||
>
|
||||
{icon && <Icon name={icon} className={styles.detailIcon} />}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
className={styles.detailValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
autoComplete="off"
|
||||
data-validation-type={validationType}
|
||||
disabled={disabled}
|
||||
{...inputValue}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailItemMessage}>
|
||||
{!hasError &&
|
||||
showSuccess &&
|
||||
value && <Icon className={styles.checkIcon} name="check_circle" />}
|
||||
{hasError && showError && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InputField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
id: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
errorMsg: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
columnDisplay: PropTypes.bool,
|
||||
showSuccess: PropTypes.bool,
|
||||
validationType: PropTypes.string,
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'plugin-api/beta/client/hocs';
|
||||
import ChangeUsername from '../components/ChangeUsername';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withChangeUsername } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
export default compose(connect(null, mapDispatchToProps), withChangeUsername)(
|
||||
ChangeUsername
|
||||
);
|
||||
@@ -142,6 +142,20 @@ en:
|
||||
cancel: "Cancel"
|
||||
edit: "Edit"
|
||||
changed_password_msg: "Changed Password - Your password has been successfully changed"
|
||||
change_username:
|
||||
change_username_note: "Usernames can be changed every 14 days"
|
||||
save: "Save"
|
||||
edit_profile: "Edit Profile"
|
||||
cancel: "Cancel"
|
||||
confirm_username_change: "Confirm Username Change"
|
||||
description: "You are attempting to change your username. Your new username will appear on all of your past and future comments."
|
||||
old_username: "Old Username"
|
||||
new_username: "New Username"
|
||||
bottom_note: "Note: You will not be able to change your username again for 14 days"
|
||||
confirm_changes: "Confirm Changes"
|
||||
username_does_not_match: "Username does not match"
|
||||
changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days."
|
||||
change_username_attempt: "Username can't be updated. Usernames can be changed every 14 days"
|
||||
de:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
@@ -243,6 +257,20 @@ es:
|
||||
cancel: "Cancelar"
|
||||
edit: "Editar"
|
||||
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
|
||||
change_username:
|
||||
change_username_note: "El usuario puede ser cambiado cada 14 días"
|
||||
save: "Guardar"
|
||||
edit_profile: "Editar Perfil"
|
||||
cancel: "Cancelar"
|
||||
confirm_username_change: "Confirmar Cambio de Usuario"
|
||||
description: "Estás intentando cambiar tu usuario. Tu nuevo usuario aparecerá en todos tus pasados y futuros comentarios."
|
||||
old_username: "Usuario viejo"
|
||||
new_username: "Usuario nuevo"
|
||||
bottom_note: "Nota: No podrás cambiar tu usuario por 14 días"
|
||||
confirm_changes: "Confirmar Cambios"
|
||||
username_does_not_match: "El usuario no coincide"
|
||||
changed_username_success_msg: "Usuario Actualizado - Tu usuario ha sido exitosamente actualizado. No podrás cambiar el usuario por 14 días."
|
||||
change_username_attempt: "El usuario no puede ser actualizado. Los usuarios pueden ser cambiados cada 14 días."
|
||||
fr:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
|
||||
+104
-41
@@ -1,4 +1,5 @@
|
||||
const uuid = require('uuid');
|
||||
const moment = require('moment');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const {
|
||||
ErrMaxRateLimit,
|
||||
@@ -234,57 +235,74 @@ class Users {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async _setUsername(
|
||||
id,
|
||||
username,
|
||||
fromStatus,
|
||||
toStatus,
|
||||
assignedBy,
|
||||
resetAllowed = false
|
||||
) {
|
||||
static async setUsername(id, username, assignedBy) {
|
||||
try {
|
||||
const oldestEditTime = moment()
|
||||
.subtract(14, 'days')
|
||||
.toDate();
|
||||
|
||||
// A username can be set if:
|
||||
//
|
||||
// - The previous status was 'UNSET'
|
||||
// - The username has not been changed within the last 14 days.
|
||||
const query = {
|
||||
id,
|
||||
'status.username.status': fromStatus,
|
||||
};
|
||||
if (!resetAllowed) {
|
||||
query.username = { $ne: username };
|
||||
}
|
||||
|
||||
let user = await User.findOneAndUpdate(
|
||||
query,
|
||||
{
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
'status.username.status': toStatus,
|
||||
$or: [
|
||||
{
|
||||
'status.username.status': 'UNSET',
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status: toStatus,
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now(),
|
||||
},
|
||||
{
|
||||
'status.username.status': { $in: ['APPROVED', 'SET'] },
|
||||
$or: [
|
||||
{
|
||||
'status.username.history.created_at': {
|
||||
$lte: oldestEditTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
'status.username.history': [],
|
||||
},
|
||||
{
|
||||
'status.username.history': { $exists: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
'status.username.status': 'SET',
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status: 'SET',
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let user = await User.findOneAndUpdate(query, update, {
|
||||
new: true,
|
||||
});
|
||||
if (!user) {
|
||||
user = await Users.findById(id);
|
||||
if (user === null) {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (user.status.username.status !== fromStatus) {
|
||||
if (
|
||||
!['UNSET', 'APPROVED', 'SET'].includes(user.status.username.status) ||
|
||||
user.status.username.history.some(({ created_at }) =>
|
||||
moment(created_at).isAfter(oldestEditTime)
|
||||
)
|
||||
) {
|
||||
throw new ErrPermissionUpdateUsername();
|
||||
}
|
||||
|
||||
if (!resetAllowed && user.username === username) {
|
||||
throw new ErrSameUsernameProvided();
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
}
|
||||
|
||||
@@ -298,12 +316,57 @@ class Users {
|
||||
}
|
||||
}
|
||||
|
||||
static async setUsername(id, username, assignedBy) {
|
||||
return Users._setUsername(id, username, 'UNSET', 'SET', assignedBy, true);
|
||||
}
|
||||
|
||||
static async changeUsername(id, username, assignedBy) {
|
||||
return Users._setUsername(id, username, 'REJECTED', 'CHANGED', assignedBy);
|
||||
try {
|
||||
const query = {
|
||||
id,
|
||||
username: { $ne: username },
|
||||
'status.username.status': 'REJECTED',
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
'status.username.status': 'CHANGED',
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status: 'CHANGED',
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let user = await User.findOneAndUpdate(query, update, {
|
||||
new: true,
|
||||
});
|
||||
if (!user) {
|
||||
user = await Users.findById(id);
|
||||
if (user === null) {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (user.status.username.status !== 'REJECTED') {
|
||||
throw new ErrPermissionUpdateUsername();
|
||||
}
|
||||
|
||||
if (user.username === username) {
|
||||
throw new ErrSameUsernameProvided();
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw new ErrUsernameTaken();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const Context = require('../../../graph/context');
|
||||
const timekeeper = require('timekeeper');
|
||||
const moment = require('moment');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
@@ -302,6 +304,62 @@ 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];
|
||||
|
||||
// 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', '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];
|
||||
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, status);
|
||||
|
||||
timekeeper.travel(
|
||||
moment()
|
||||
.add(5, 'days')
|
||||
.toDate()
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it(`allows users to change their username if it was changed 14 days before today from ${status}`, async () => {
|
||||
const user = mockUsers[0];
|
||||
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, status);
|
||||
|
||||
timekeeper.travel(
|
||||
moment()
|
||||
.add(15, 'days')
|
||||
.toDate()
|
||||
);
|
||||
|
||||
await UsersService.setUsername(user.id, 'new_username', null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user