Merge branch 'master' into forgot-password-change

This commit is contained in:
Belén Curcio
2018-05-04 13:40:22 -03:00
committed by GitHub
22 changed files with 744 additions and 43 deletions
@@ -96,8 +96,6 @@ class UserDetail extends React.Component {
bulkReject,
} = this.props;
console.log(rejectedComments, totalComments);
// if totalComments is 0, you're dividing by zero
let rejectedPercent = rejectedComments / totalComments * 100;
+31
View File
@@ -291,5 +291,36 @@ export default {
},
},
}),
SetCommentStatus: ({ variables: { status } }) => ({
updateQueries: {
CoralAdmin_UserDetail: prev => {
const increment = {
rejectedComments: {
$apply: count => (count < prev.totalComments ? count + 1 : count),
},
};
const decrement = {
rejectedComments: {
$apply: count => (count > 0 ? count - 1 : 0),
},
};
// If rejected then increment rejectedComments by one
if (status === 'REJECTED') {
const updated = update(prev, increment);
return updated;
}
// If approved then decrement rejectedComments by one
if (status === 'ACCEPTED') {
const updated = update(prev, decrement);
return updated;
}
return prev;
},
},
}),
},
};
+2 -1
View File
@@ -27,6 +27,7 @@ export default {
'UpdateAssetStatusResponse',
'UpdateSettingsResponse',
'ChangePasswordResponse',
'UpdateEmailAddressResponse'
'UpdateEmailAddressResponse',
'AttachLocalAuthResponse'
),
};
@@ -1,6 +1,5 @@
import { gql } from 'react-apollo';
import withMutation from '../hocs/withMutation';
import update from 'immutability-helper';
function convertItemType(item_type) {
switch (item_type) {
@@ -168,36 +167,6 @@ export const withSetCommentStatus = withMutation(
errors: null,
},
},
updateQueries: {
CoralAdmin_UserDetail: prev => {
const increment = {
rejectedComments: {
$apply: count =>
count < prev.totalComments ? count + 1 : count,
},
};
const decrement = {
rejectedComments: {
$apply: count => (count > 0 ? count - 1 : 0),
},
};
// If rejected then increment rejectedComments by one
if (status === 'REJECTED') {
const updated = update(prev, increment);
return updated;
}
// If approved then decrement rejectedComments by one
if (status === 'ACCEPTED') {
const updated = update(prev, decrement);
return updated;
}
return prev;
},
},
update: proxy => {
const fragment = gql`
fragment Talk_SetCommentStatus_Comment on Comment {
-1
View File
@@ -27,6 +27,5 @@ export {
withSetCommentStatus,
withChangePassword,
withChangeUsername,
withUpdateEmailAddress,
} from 'coral-framework/graphql/mutations';
export { compose } from 'recompose';
@@ -0,0 +1,8 @@
.errorMsg {
color: #FA4643;
font-size: 0.9em;
}
.warningIcon {
color: #FA4643;
}
@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ErrorMessage.css';
import { Icon } from 'plugin-api/beta/client/components/ui';
const ErrorMessage = ({ children }) => (
<div className={styles.errorMsg}>
<Icon className={styles.warningIcon} name="warning" />
<span>{children}</span>
</div>
);
ErrorMessage.propTypes = {
children: PropTypes.node,
};
export default ErrorMessage;
@@ -0,0 +1,80 @@
.detailItem {
margin-bottom: 12px;
}
.detailItemContainer {
display: flex;
}
.columnDisplay {
flex-direction: column;
.detailItemMessage {
padding: 4px 0 0;
}
}
.detailItemContent {
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 {
color: #4C4C4D;
font-size: 1em;
display: block;
margin-bottom: 4px;
}
.detailValue {
background: transparent;
border: none;
font-size: 1em;
color: #000;
outline: none;
flex: 1;
height: 100%;
box-sizing: border-box;
}
.detailItemMessage {
flex-grow: 1;
display: flex;
align-items: center;
padding-left: 6px;
padding-top: 16px;
.warningIcon, .checkIcon {
font-size: 17px;
}
}
.checkIcon {
color: #00CD73;
}
.warningIcon {
color: #FA4643;
}
@@ -0,0 +1,94 @@
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';
const InputField = ({
id = '',
label = '',
type = 'text',
name = '',
onChange = () => {},
showError = true,
hasError = false,
errorMsg = '',
children,
columnDisplay = false,
showSuccess = false,
validationType = '',
icon = '',
value = '',
defaultValue = '',
disabled = false,
}) => {
const inputValue = {
...(value ? { value } : {}),
...(defaultValue ? { defaultValue } : {}),
};
return (
<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 && showError },
{ [styles.disabled]: disabled }
)}
>
{icon && <Icon name={icon} className={styles.detailIcon} />}
<input
id={id}
type={type}
name={name}
className={styles.detailValue}
onChange={onChange}
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}
</div>
);
};
InputField.propTypes = {
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,82 @@
.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: 50%;
transform: translateY(-50%);
padding: 20px;
border-radius: 4px;
font-family: Helvetica,Helvetica Neue,Verdana,sans-serif;
color:#3B4A53;
}
.title {
font-size: 1.3em;
margin: 15px 0;
text-align: center;
}
.description {
font-size: 1em;
line-height: 20px;
margin: 0;
margin-bottom: 15px;
}
.list {
padding: 0;
margin: 20px 0;
list-style: none;
}
.item {
display: flex;
margin-bottom: 20px;
.itemIcon {
flex-grow: 0;
}
.text {
flex-grow: 1;
padding-left: 10px;
}
> i.itemIcon {
font-size: 1.3em;
}
}
.button {
color: #787D80;
border-radius: 2px;
background-color: transparent;
height: 30px;
font-size: 0.9em;
line-height: normal;
width: 100%;
display: inline-block;
text-align: center;
line-height: 30px;
font-size: 1em;
&:hover {
background-color: #eaeaea;
cursor: pointer;
}
&.cancel {
background-color: transparent;
color: #787D80;
}
&.proceed {
background-color: #3498DB;
color: white;
}
&.danger {
background-color: #FA4643;
color: white;
}
}
@@ -0,0 +1,153 @@
import React from 'react';
import isMatch from 'lodash/isEqual';
import isEqualWith from 'lodash/isEqual';
import PropTypes from 'prop-types';
import { Dialog } from 'plugin-api/beta/client/components/ui';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './AddEmailAddressDialog.css';
import AddEmailContent from './AddEmailContent';
import VerifyEmailAddress from './VerifyEmailAddress';
import EmailAddressAdded from './EmailAddressAdded';
const initialState = {
step: 0,
showErrors: false,
errors: {},
formData: {},
};
class AddEmailAddressDialog extends React.Component {
state = initialState;
validKeys = ['emailAddress', 'confirmPassword', 'confirmEmailAddress'];
onChange = e => {
const { name, value, type } = e.target;
this.setState(
state => ({
formData: {
...state.formData,
[name]: value,
},
}),
() => {
this.fieldValidation(value, type, name);
}
);
};
fieldValidation = (value, type, name) => {
if (!value.length) {
this.addError({
[name]: 'Field is required',
});
} else if (!validate[type](value)) {
this.addError({ [name]: errorMsj[type] });
} else {
this.removeError(name);
}
};
addError = err => {
this.setState(({ errors }) => ({
errors: { ...errors, ...err },
}));
};
removeError = errKey => {
this.setState(state => {
const { [errKey]: _, ...errors } = state.errors;
return {
errors,
};
});
};
hasError = err => {
return Object.keys(this.state.errors).indexOf(err) !== -1;
};
formHasError = () => {
const formHasErrors = !!Object.keys(this.state.errors).length;
const formIncomplete = !isEqualWith(
Object.keys(this.state.formData),
this.validKeys,
isMatch
);
return formHasErrors || formIncomplete;
};
showErrors = () => {
this.setState({
showErrors: true,
});
};
confirmChanges = async () => {
if (this.formHasError()) {
this.showErrors();
return;
}
const { emailAddress, confirmPassword } = this.state.formData;
const { attachLocalAuth } = this.props;
try {
await attachLocalAuth({
email: emailAddress,
password: confirmPassword,
});
this.props.notify('success', 'Email Added!');
this.goToNextStep();
} catch (err) {
this.props.notify('error', getErrorMessages(err));
}
};
goToNextStep = () => {
this.setState(({ step }) => ({
step: step + 1,
}));
};
render() {
const { errors, formData, showErrors, step } = this.state;
const { root: { settings } } = this.props;
return (
<Dialog className={styles.dialog} open={true}>
{step === 0 && (
<AddEmailContent
formData={formData}
errors={errors}
showErrors={showErrors}
confirmChanges={this.confirmChanges}
onChange={this.onChange}
/>
)}
{step === 1 &&
!settings.requireEmailConfirmation && (
<EmailAddressAdded done={() => {}} />
)}
{step === 1 &&
settings.requireEmailConfirmation && (
<VerifyEmailAddress
emailAddress={formData.emailAddress}
done={() => {}}
/>
)}
</Dialog>
);
}
}
AddEmailAddressDialog.propTypes = {
attachLocalAuth: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
};
export default AddEmailAddressDialog;
@@ -0,0 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './AddEmailAddressDialog.css';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
import InputField from './InputField';
import { t } from 'plugin-api/beta/client/services';
const AddEmailContent = ({
formData,
errors,
showErrors,
confirmChanges,
onChange,
}) => (
<div>
<h4 className={styles.title}>
{t('talk-plugin-local-auth.add_email.content.title')}
</h4>
<p className={styles.description}>
{t('talk-plugin-local-auth.add_email.content.description')}
</p>
<ul className={styles.list}>
<li className={styles.item}>
<Icon name="done" className={styles.itemIcon} />
<span className={styles.text}>
{t('talk-plugin-local-auth.add_email.content.item_1')}
</span>
</li>
<li className={styles.item}>
<Icon name="done" className={styles.itemIcon} />
<span className={styles.text}>
{t('talk-plugin-local-auth.add_email.content.item_2')}
</span>
</li>
<li className={styles.item}>
<Icon name="done" className={styles.itemIcon} />
<span className={styles.text}>
{t('talk-plugin-local-auth.add_email.content.item_3')}
</span>
</li>
</ul>
<form autoComplete="off">
<InputField
id="emailAddress"
label={t('talk-plugin-local-auth.add_email.enter_email_address')}
name="emailAddress"
type="email"
onChange={onChange}
defaultValue=""
hasError={!formData.emailAddress || errors.emailAddress}
errorMsg={t('talk-plugin-local-auth.add_email.invalid_email_address')}
showError={showErrors}
columnDisplay
showSuccess={false}
/>
<InputField
id="confirmEmailAddress"
label={t('talk-plugin-local-auth.add_email.confirm_email_address')}
name="confirmEmailAddress"
type="email"
onChange={onChange}
defaultValue=""
hasError={
!formData.emailAddress ||
formData.emailAddress !== formData.confirmEmailAddress
}
errorMsg={t('talk-plugin-local-auth.add_email.email_does_not_match')}
showError={showErrors}
columnDisplay
showSuccess={false}
/>
<InputField
id="confirmPassword"
label={t('talk-plugin-local-auth.add_email.insert_password')}
name="confirmPassword"
type="password"
onChange={onChange}
defaultValue=""
hasError={!formData.confirmPassword}
showError={showErrors}
columnDisplay
showSuccess={false}
/>
<div className={styles.actions}>
<a
className={cn(styles.button, styles.proceed)}
onClick={confirmChanges}
>
{t('talk-plugin-local-auth.add_email.add_email_address')}
</a>
</div>
</form>
</div>
);
AddEmailContent.propTypes = {
formData: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
showErrors: PropTypes.bool.isRequired,
confirmChanges: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default AddEmailContent;
@@ -0,0 +1,32 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './AddEmailAddressDialog.css';
import { t } from 'plugin-api/beta/client/services';
const EmailAddressAdded = ({ done }) => (
<div>
<h4 className={styles.title}>
{t('talk-plugin-local-auth.add_email.added.title')}
</h4>
<p className={styles.description}>
{t('talk-plugin-local-auth.add_email.added.description')}
</p>
<strong>{t('talk-plugin-local-auth.add_email.added.subtitle')}</strong>
<p className={styles.description}>
{t('talk-plugin-local-auth.add_email.added.description_2')}{' '}
<strong>{t('talk-plugin-local-auth.add_email.added.path')}</strong>.
</p>
<div className={styles.actions}>
<a className={cn(styles.button, styles.proceed)} onClick={done}>
{t('talk-plugin-local-auth.add_email.done')}
</a>
</div>
</div>
);
EmailAddressAdded.propTypes = {
done: PropTypes.func.isRequired,
};
export default EmailAddressAdded;
@@ -62,6 +62,7 @@
flex: 1;
height: 100%;
box-sizing: border-box;
padding: 0 6px;
}
.detailItemMessage {
@@ -81,4 +82,4 @@
.warningIcon {
color: #FA4643;
}
}
@@ -44,7 +44,7 @@ const InputField = ({
<div
className={cn(
styles.detailInput,
{ [styles.error]: hasError },
{ [styles.error]: hasError && showError },
{ [styles.disabled]: disabled }
)}
>
@@ -0,0 +1,28 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './AddEmailAddressDialog.css';
import { t } from 'plugin-api/beta/client/services';
const VerifyEmailAddress = ({ emailAddress, done }) => (
<div>
<h4 className={styles.title}>
{t('talk-plugin-local-auth.add_email.verify.title')}
</h4>
<p className={styles.description}>
{t('talk-plugin-local-auth.add_email.verify.description', emailAddress)}
</p>
<div className={styles.actions}>
<a className={cn(styles.button, styles.proceed)} onClick={done}>
{t('talk-plugin-local-auth.add_email.done')}
</a>
</div>
</div>
);
VerifyEmailAddress.propTypes = {
emailAddress: PropTypes.string.isRequired,
done: PropTypes.func.isRequired,
};
export default VerifyEmailAddress;
@@ -0,0 +1,30 @@
import { compose, gql } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import AddEmailAddressDialog from '../components/AddEmailAddressDialog';
import { notify } from 'coral-framework/actions/notification';
import { withAttachLocalAuth } from '../hocs';
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
const withData = withFragments({
root: gql`
fragment TalkPluginLocalAuth_AddEmailAddressDialog_root on RootQuery {
me {
id
email
}
settings {
requireEmailConfirmation
}
}
`,
});
export default compose(
connect(null, mapDispatchToProps),
withAttachLocalAuth,
withData,
excludeIf(({ root: { me } }) => !me || me.email)
)(AddEmailAddressDialog);
@@ -1,6 +1,50 @@
import { withMutation } from 'plugin-api/beta/client/hocs';
import { gql } from 'react-apollo';
import update from 'immutability-helper';
import withMutation from 'coral-framework/hocs/withMutation';
export const withAttachLocalAuth = withMutation(
gql`
mutation AttachLocalAuth($input: AttachLocalAuthInput!) {
attachLocalAuth(input: $input) {
...AttachLocalAuthResponse
}
}
`,
{
props: ({ mutate }) => ({
attachLocalAuth: input => {
return mutate({
variables: {
input,
},
update: proxy => {
const AttachLocalAuthQuery = gql`
query Talk_AttachLocalAuth {
me {
id
email
}
}
`;
const prev = proxy.readQuery({ query: AttachLocalAuthQuery });
const data = update(prev, {
me: {
email: { $set: input.email },
},
});
proxy.writeQuery({
query: AttachLocalAuthQuery,
data,
});
},
});
},
}),
}
);
export const withUpdateEmailAddress = withMutation(
gql`
@@ -1,4 +1,5 @@
import ChangePassword from './containers/ChangePassword';
import AddEmailAddressDialog from './containers/AddEmailAddressDialog';
import Profile from './containers/Profile';
import translations from './translations.yml';
import graphql from './graphql';
@@ -8,6 +9,7 @@ export default {
slots: {
profileHeader: [Profile],
profileSettings: [ChangePassword],
stream: [AddEmailAddressDialog],
},
...graphql,
};
@@ -36,6 +36,29 @@ en:
change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications."
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 only be changed every 14 days."
add_email:
add_email_address: "Add Email Address"
enter_email_address: "Enter Email Address:"
invalid_email_address: "Invalid Email address"
confirm_email_address: "Confirm Email Address:"
email_does_not_match: "Email Address does not match"
insert_password: "Insert Password:"
done: "done"
content:
title: "Add an Email Address"
description: "For your added security, we require users to add an email address to their accounts. Your email address will be used to:"
item_1: "Receive updates regarding any changes to your account (email address, username, password, etc.)"
item_2: "Allow you to download your comments."
item_3: "Send comment notifications that you have chosen to receive."
verify:
title: "Verify Your Email Address"
description: "Weve sent an email to {0} to verify your account. You must verify your email address so that it can be used for account change confirmations and notifications."
added:
title: "Email Address Added"
description: "Your email address has been added to your account."
subtitle: "Need to change your email address?"
description_2: "You can change your account settings by visiting"
path: "My Profile > Settings"
es:
talk-plugin-local-auth:
change_password:
@@ -5,7 +5,6 @@ const {
ErrIncorrectPassword,
} = require('./errors');
const { get } = require('lodash');
const bcrypt = require('bcryptjs');
// hasLocalProfile checks a user's profiles to see if they already have a local
// profile associated with their account.
@@ -89,7 +88,7 @@ async function attachUserLocalAuth(ctx, email, password) {
await Users.isValidPassword(password);
// Hash the new password.
const hashedPassword = await bcrypt.hash(password, 10);
const hashedPassword = await Users.hashPassword(password);
try {
// Associate the account with the user.
+7 -3
View File
@@ -560,7 +560,7 @@ class Users {
throw new ErrPasswordTooShort();
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const hashedPassword = await Users.hashPassword(password);
return User.update(
{ id },
@@ -637,7 +637,7 @@ class Users {
Users.isValidPassword(password),
]);
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const hashedPassword = await Users.hashPassword(password);
let user = new User({
username,
@@ -814,6 +814,10 @@ class Users {
return { user, redirect, version };
}
static async hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
// TODO: update doc
static async resetPassword(token, password) {
const { user, redirect, version } = await this.verifyPasswordResetToken(
@@ -824,7 +828,7 @@ class Users {
throw new ErrPasswordTooShort();
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const hashedPassword = await Users.hashPassword(password);
// Update the user's password.
await User.update(