Merge branch 'master' into mobile-width-fix

This commit is contained in:
David Jay
2017-02-01 12:16:57 -05:00
committed by GitHub
30 changed files with 571 additions and 230 deletions
@@ -32,6 +32,10 @@ const updateModeration = (updateSettings, mod) => () => {
updateSettings({moderation});
};
const updateEmailConfirmation = (updateSettings, verify) => () => {
updateSettings({requireEmailConfirmation: !verify});
};
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
const infoBoxEnable = !infoBox;
updateSettings({infoBoxEnable});
@@ -67,106 +71,123 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
}
// just putting this here for shorthand below
const on = styles.enabledSetting;
const off = styles.disabledSetting;
return (
<div className={styles.commentSettingsSection}>
<h3>{title}</h3>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</div>
<div className={styles.content}>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<div className={styles.content}>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
checked={settings.requireEmailConfirmation} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
{lang.t('configure.require-email-verification-text')}
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</div>
<div className={styles.content}>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<div className={styles.content}>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.closed-comments-desc')}
<div>
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.closed-comments-desc')}
<div>
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox}`}>
<div className={styles.content}>
{lang.t('configure.close-after')}
<br />
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
value={getTimeoutAmount(settings.closedTimeout)}
label={lang.t('configure.closed-comments-label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
<Option value={'days'}>{lang.t('configure.days')}</Option>
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
</SelectField>
</div>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox}`}>
<div className={styles.content}>
{lang.t('configure.close-after')}
<br />
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
value={getTimeoutAmount(settings.closedTimeout)}
label={lang.t('configure.closed-comments-label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
<Option value={'days'}>{lang.t('configure.days')}</Option>
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
</SelectField>
</div>
</Card>
</div>
</Card>
</div>
);
};
+4
View File
@@ -50,6 +50,8 @@
"configure": {
"enable-pre-moderation": "Enable pre-moderation",
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"require-email-verification": "Require Email Confirmation",
"require-email-verification-text": "New Users must verify their email before commenting",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
@@ -155,6 +157,8 @@
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
"require-email-verification": "Necesita confirmación de correo",
"require-email-verification-text": "Nuevos usuarios deben verificar sus correos antes de comentar",
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
"include-text": "Incluir tu texto aqui.",
+2 -1
View File
@@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
const {logout, showSignInDialog} = authActions;
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
@@ -191,6 +191,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
requestConfirmEmail: () => dispatch(requestConfirmEmail()),
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
+31 -1
View File
@@ -21,6 +21,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE});
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const emailConfirmError = () => ({type: actions.EMAIL_CONFIRM_ERROR});
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
@@ -30,7 +31,18 @@ export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
})
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
.catch(error => {
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata)));
dispatch(emailConfirmError());
} else {
// invalid credentials
dispatch(signInFailure(lang.t('error.emailPasswordError')));
}
});
};
// Sign In - Facebook
@@ -137,3 +149,21 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginFailure(`${error.message}`));
});
};
const confirmEmailRequest = () => ({type: actions.CONFIRM_EMAIL_REQUEST});
const confirmEmailSuccess = () => ({type: actions.CONFIRM_EMAIL_SUCCESS});
const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE});
export const requestConfirmEmail = email => dispatch => {
dispatch(confirmEmailRequest());
return coralApi('/users/resend-confirm', {method: 'POST', body: {email}})
.then(() => {
dispatch(confirmEmailSuccess());
})
.catch(err => {
console.log('failed to send email confirmation', err);
// email might have already been confirmed
dispatch(confirmEmailFailure());
});
};
+5
View File
@@ -32,3 +32,8 @@ export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const EMAIL_CONFIRM_ERROR = 'EMAIL_CONFIRM_ERROR';
export const CONFIRM_EMAIL_REQUEST = 'CONFIRM_EMAIL_REQUEST';
export const CONFIRM_EMAIL_SUCCESS = 'CONFIRM_EMAIL_SUCCESS';
export const CONFIRM_EMAIL_FAILURE = 'CONFIRM_EMAIL_FAILURE';
+10 -4
View File
@@ -34,15 +34,21 @@ const buildOptions = (inputOptions = {}) => {
};
const handleResp = res => {
if (res.status === 401) {
throw new Error('Not Authorized to make this request');
} else if (res.status > 399) {
if (res.status > 399) {
return res.json().then(err => {
let message = err.message || res.status;
const error = new Error();
if (err.error && err.error.metadata && err.error.metadata.message) {
error.metadata = err.error.metadata.message;
}
if (err.error && err.error.translation_key) {
message = err.error.translation_key;
}
throw new Error(message);
error.message = message;
throw error;
});
} else if (res.status === 204) {
return res.text();
+16
View File
@@ -11,6 +11,9 @@ const initialState = Map({
error: '',
passwordRequestSuccess: null,
passwordRequestFailure: null,
emailConfirmationFailure: false,
emailConfirmationLoading: false,
emailConfirmationSuccess: false,
successSignUp: false
});
@@ -33,6 +36,9 @@ export default function auth (state = initialState, action) {
error: '',
passwordRequestFailure: null,
passwordRequestSuccess: null,
emailConfirmationFailure: false,
emailConfirmationSuccess: false,
emailConfirmationLoading: false,
successSignUp: false
}));
case actions.CHANGE_VIEW :
@@ -101,6 +107,16 @@ export default function auth (state = initialState, action) {
return state
.set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!')
.set('passwordRequestSuccess', null);
case actions.EMAIL_CONFIRM_ERROR:
return state
.set('emailConfirmationFailure', true)
.set('emailConfirmationLoading', false);
case actions.CONFIRM_EMAIL_REQUEST:
return state.set('emailConfirmationLoading', true);
case actions.CONFIRM_EMAIL_SUCCESS:
return state
.set('emailConfirmationSuccess', true)
.set('emailConfirmationLoading', false);
default :
return state;
}
+2
View File
@@ -5,6 +5,7 @@
"contentNotAvailable": "This content is not available",
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"error": {
"emailNotVerified": "Email address {0} not verified.",
"email": "Not a valid E-Mail",
"password": "Password must be at least 8 characters",
"displayName": "Display names can contain letters, numbers and _ only",
@@ -27,6 +28,7 @@
"contentNotAvailable": "El contenido no se encuentra disponible",
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"error": {
"emailNotVerified": "Dirección de correo electrónico {0} no verificada.",
"email": "No es un email válido",
"password": "La contraseña debe tener por lo menos 8 caracteres",
"displayName": "Los nombres pueden contener letras, números y _",
@@ -17,7 +17,12 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
}}>
<span className={styles.close} onClick={handleClose}>×</span>
{view === 'SIGNIN' && <SignInContent {...props} />}
{view === 'SIGNUP' && <SignUpContent {...props} />}
{
view === 'SIGNUP' && <SignUpContent
emailConfirmationLoading={props.emailConfirmationLoading}
emailConfirmationSuccess={props.emailConfirmationSuccess}
{...props} />
}
{view === 'FORGOT' && <ForgotContent {...props} />}
</Dialog>
);
@@ -1,67 +1,100 @@
import React from 'react';
import Button from 'coral-ui/components/Button';
import FormField from './FormField';
import React, {PropTypes} from 'react';
import Alert from './Alert';
import Spinner from 'coral-ui/components/Spinner';
import {Button, FormField, Spinner, Success} from 'coral-ui';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const SignInContent = ({handleChange, formData, ...props}) => (
<div>
<div className={styles.header}>
<h1>
{lang.t('signIn.signIn')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form onSubmit={props.handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
const SignInContent = ({
handleChange,
handleChangeEmail,
emailToBeResent,
handleResendConfirmation,
emailConfirmationLoading,
emailConfirmationSuccess,
formData,
...props
}) => {
return (
<div>
<div className={styles.header}>
<h1>
{props.auth.emailConfirmationFailure ? lang.t('signIn.emailConfirmCTA') : lang.t('signIn.signIn')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
{
props.auth.emailConfirmationFailure
? <form onSubmit={handleResendConfirmation}>
<p>{lang.t('signIn.requestNewConfirmEmail')}</p>
<FormField
id="confirm-email"
type="email"
label={lang.t('signIn.email')}
value={emailToBeResent}
onChange={handleChangeEmail} />
<Button id='resendConfirmEmail' type='submit' cStyle='black' full>Send Email</Button>
{emailConfirmationLoading && <Spinner />}
{emailConfirmationSuccess && <Success />}
</form>
: <form onSubmit={props.handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
</div>
</form>
}
<div className={styles.footer}>
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
</div>
</form>
<div className={styles.footer}>
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
</div>
</div>
);
);
};
SignInContent.propTypes = {
emailConfirmationLoading: PropTypes.bool.isRequired,
emailConfirmationSuccess: PropTypes.bool.isRequired,
handleResendConfirmation: PropTypes.func.isRequired,
handleChangeEmail: PropTypes.func.isRequired,
emailToBeResent: PropTypes.string.isRequired
};
export default SignInContent;
@@ -1,9 +1,6 @@
import React from 'react';
import FormField from './FormField';
import Alert from './Alert';
import Button from 'coral-ui/components/Button';
import Spinner from 'coral-ui/components/Spinner';
import Success from 'coral-ui/components/Success';
import {Button, FormField, Spinner, Success} from 'coral-ui';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
+12 -22
View File
@@ -14,28 +14,6 @@
font-size: 1.2em;
}
.formField {
margin-top: 15px;
}
.formField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.formField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
@@ -150,3 +128,15 @@ input.error{
background-color: 1px solid coral;
padding: 10px;
}
.emailConfirmDialog {
margin-top: 15px;
}
.confirmLabel {
display: block;
}
.confirmSubmit {
}
@@ -16,6 +16,7 @@ import {
hideSignInDialog,
fetchSignInFacebook,
fetchForgotPassword,
requestConfirmEmail,
facebookCallback,
invalidForm,
validForm,
@@ -30,6 +31,7 @@ class SignInContainer extends Component {
password: '',
confirmPassword: ''
},
emailToBeResent: '',
errors: {},
showErrors: false
};
@@ -38,9 +40,10 @@ class SignInContainer extends Component {
super(props);
this.state = this.initialState;
this.handleChange = this.handleChange.bind(this);
this.handleChangeEmail = this.handleChangeEmail.bind(this);
this.handleResendConfirmation = this.handleResendConfirmation.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.handleSignIn = this.handleSignIn.bind(this);
this.handleClose = this.handleClose.bind(this);
this.addError = this.addError.bind(this);
}
@@ -71,6 +74,23 @@ class SignInContainer extends Component {
});
}
handleChangeEmail(e) {
const {value} = e.target;
this.setState({emailToBeResent: value});
}
handleResendConfirmation(e) {
e.preventDefault();
this.props.requestConfirmEmail(this.state.emailToBeResent)
.then(() => {
setTimeout(() => {
// allow success UI to be shown for a second, and then close the modal
this.props.handleClose();
}, 2500);
});
}
addError(name, error) {
return this.setState(state => ({
errors: {
@@ -124,12 +144,9 @@ class SignInContainer extends Component {
this.props.fetchSignIn(this.state.formData);
}
handleClose() {
this.props.hideSignInDialog();
}
render() {
const {auth, showSignInDialog, noButton, offset} = this.props;
const {emailConfirmationLoading, emailConfirmationSuccess} = auth;
return (
<div>
{!noButton && <Button id='coralSignInButton' onClick={showSignInDialog} full>
@@ -139,6 +156,8 @@ class SignInContainer extends Component {
open={auth.showSignInDialog}
view={auth.view}
offset={offset}
emailConfirmationLoading={emailConfirmationLoading}
emailConfirmationSuccess={emailConfirmationSuccess}
{...this}
{...this.state}
{...this.props}
@@ -159,6 +178,7 @@ const mapDispatchToProps = dispatch => ({
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
fetchSignInFacebook: () => dispatch(fetchSignInFacebook()),
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
requestConfirmEmail: email => dispatch(requestConfirmEmail(email)),
showSignInDialog: () => dispatch(showSignInDialog()),
changeView: view => dispatch(changeView(view)),
handleClose: () => dispatch(hideSignInDialog()),
+4
View File
@@ -1,6 +1,8 @@
export default {
en: {
'signIn': {
emailConfirmCTA: 'Please verify your email address.',
requestNewConfirmEmail: 'Request another email:',
notYou: 'Not you?',
loggedInAs: 'Logged in as',
facebookSignIn: 'Sign in with Facebook',
@@ -28,6 +30,8 @@ export default {
},
es: {
'signIn': {
emailConfirmCTA: 'Por favor verifique su correo electronico.',
requestNewConfirmEmail: 'Enviar otro correo:',
notYou: 'No eres tu?',
loggedInAs: 'Entraste como',
facebookSignIn: 'Entrar con Facebook',
+48
View File
@@ -0,0 +1,48 @@
.formField {
margin-top: 15px;
}
.formField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.formField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
input.error{
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
@@ -1,5 +1,5 @@
import React from 'react';
import styles from './styles.css';
import React, {PropTypes} from 'react';
import styles from './FormField.css';
const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => (
<div className={`${styles.formField} ${className ? className : ''}`}>
@@ -15,4 +15,12 @@ const FormField = ({className, showErrors = false, errorMsg, label, ...props}) =
</div>
);
FormField.propTypes = {
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
errorMsg: PropTypes.string,
type: PropTypes.string
};
export default FormField;
+2
View File
@@ -13,4 +13,6 @@ export {default as Icon} from './components/Icon';
export {default as List} from './components/List';
export {default as Item} from './components/Item';
export {default as Card} from './components/Card';
export {default as FormField} from './components/FormField';
export {default as Success} from './components/Success';
export {default as Pager} from './components/Pager';
+15
View File
@@ -89,6 +89,20 @@ class ErrAssetCommentingClosed extends APIError {
}
}
/**
* ErrAuthentication is returned when there is an error authenticating and the
* message is provided.
*/
class ErrAuthentication extends APIError {
constructor(message = null) {
super('authentication error occured', {
status: 401
}, {
message
});
}
}
// ErrContainsProfanity is returned in the event that the middleware detects
// profanity/wordlisted words in the payload.
const ErrContainsProfanity = new APIError('Suspected profanity. If you think this in error, please let us know!', {
@@ -130,5 +144,6 @@ module.exports = {
ErrAssetCommentingClosed,
ErrNotFound,
ErrInvalidAssetURL,
ErrAuthentication,
ErrNotAuthorized
};
+2 -2
View File
@@ -24,7 +24,7 @@ module.exports = {
},
'test_settings': {
'default': {
'launch_url' : 'http://localhost:3000',
'launch_url' : 'http://localhost:3011',
'selenium_port': 6666,
'selenium_host': 'localhost',
'silent': true,
@@ -48,7 +48,7 @@ module.exports = {
]
},
'integration': {
'launch_url': 'http://localhost:3000'
'launch_url': 'http://localhost:3011'
}
}
};
+1 -1
View File
@@ -12,7 +12,7 @@
"lint-fix": "eslint bin/* . --fix",
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${NPM_PACKAGE_CONFIG_MOCHA_REPORTER:-spec}",
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
"pree2e": "NODE_ENV=test scripts/pree2e.sh",
"pree2e": "NODE_ENV=test TALK_PORT=3011 scripts/pree2e.sh",
"e2e": "NODE_ENV=test nightwatch",
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs",
+5
View File
@@ -1,6 +1,11 @@
const express = require('express');
const router = express.Router();
// Get /email-confirmation expects a signed JWT in the hash
router.get('/confirm-email', (req, res) => {
res.render('admin/confirm-email');
});
// Get /password-reset expects a signed token (JWT) in the hash.
// Links to this endpoint are generated by /views/password-reset-email.ejs.
router.get('/password-reset', (req, res) => {
+2 -2
View File
@@ -28,8 +28,8 @@ router.post('/email/confirm', (req, res, next) => {
UsersService
.verifyEmailConfirmation(token)
.then(() => {
res.status(204).end();
.then(({referer}) => {
res.json({redirectUri: referer});
})
.catch((err) => {
next(err);
+29 -8
View File
@@ -4,8 +4,10 @@ const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
// get a list of users.
router.get('/', authorization.needed('ADMIN'), (req, res, next) => {
const {
value = '',
@@ -44,6 +46,7 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) =>
.catch(next);
});
// update the status of a user
router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => {
UsersService
.setStatus(req.params.user_id, req.body.status)
@@ -97,8 +100,8 @@ router.post('/:user_id/email', authorization.needed('admin'), (req, res, next) =
* @param {String} userID the id for the user to send the email to
* @param {String} email the email for the user to send the email to
*/
const SendEmailConfirmation = (app, userID, email) => UsersService
.createEmailConfirmToken(userID, email)
const SendEmailConfirmation = (app, userID, email, referer) => UsersService
.createEmailConfirmToken(userID, email, referer)
.then((token) => {
return mailer.sendSimple({
app, // needed to render the templates.
@@ -113,12 +116,10 @@ const SendEmailConfirmation = (app, userID, email) => UsersService
});
});
// create a local user.
router.post('/', (req, res, next) => {
const {
email,
password,
displayName
} = req.body;
const {email, password, displayName} = req.body;
const redirectUri = req.header('Referer');
UsersService
.createLocalUser(email, password, displayName)
@@ -131,7 +132,7 @@ router.post('/', (req, res, next) => {
if (requireEmailConfirmation) {
SendEmailConfirmation(req.app, user.id, email)
SendEmailConfirmation(req.app, user.id, email, redirectUri)
.then(() => {
// Then send back the user.
@@ -176,6 +177,26 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
});
});
// trigger an email confirmation re-send by a new user
router.post('/resend-confirm', (req, res, next) => {
const {email} = req.body;
const redirectUri = req.header('Referer');
if (!email) {
return next(errors.ErrMissingEmail);
}
// find user by email.
// if the local profile is verified, return an error code?
// send a 204 after the email is re-sent
SendEmailConfirmation(req.app, null, email, redirectUri)
.then(() => {
res.status(204).end();
})
.catch(next);
});
// trigger an email confirmation re-send from the admin panel
router.post('/:user_id/email/confirm', authorization.needed('ADMIN'), (req, res, next) => {
const {
user_id
+3 -2
View File
@@ -3,6 +3,7 @@ const UsersService = require('./users');
const SettingsService = require('./settings');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const errors = require('../errors');
//==============================================================================
// SESSION SERIALIZATION
@@ -34,7 +35,7 @@ function ValidateUserLogin(loginProfile, user, done) {
}
if (user.disabled) {
return done(null, false, {message: 'Account disabled'});
return done(new errors.ErrAuthentication('Account disabled'));
}
// If the user isn't a local user (i.e., a social user).
@@ -61,7 +62,7 @@ function ValidateUserLogin(loginProfile, user, done) {
// If the profile doesn't have a metadata field, or it does not have a
// confirmed_at field, or that field is null, then send them back.
if (!profile.metadata || !profile.metadata.confirmed_at || profile.metadata.confirmed_at === null) {
return done(null, false, {message: `Email address ${loginProfile.id} not verified.`});
return done(new errors.ErrAuthentication(loginProfile.id));
}
}
+39 -27
View File
@@ -538,40 +538,51 @@ module.exports = class UsersService {
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
static createEmailConfirmToken(userID, email) {
static createEmailConfirmToken(userID = null, email, referer = process.env.TALK_ROOT_URL) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
// Conform the email to lowercase.
email = email.toLowerCase();
return UsersService
.findById(userID)
.then((user) => {
if (!user) {
return Promise.reject(new Error('user not found'));
}
const tokenOptions = {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
};
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
let userPromise;
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
return Promise.reject(new Error('email address already confirmed'));
}
if (!userID) {
const payload = {
email,
userID
};
// If there is no userID, we're coming from the endpoint where a new user
// is re-requesting a confirmation email and we don't know the userID.
userPromise = UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
} else {
userPromise = UsersService.findById(userID);
}
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
});
return userPromise.then((user) => {
if (!user) {
return Promise.reject(errors.ErrNotFound);
}
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
return Promise.reject(new Error('email address already confirmed'));
}
return jwt.sign({
email,
referer,
userID: user.id
}, process.env.TALK_SESSION_SECRET, tokenOptions);
});
}
/**
@@ -586,8 +597,7 @@ module.exports = class UsersService {
.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email}) => {
.then(({userID, email, referer}) => {
return UserModel
.update({
id: userID,
@@ -601,8 +611,10 @@ module.exports = class UsersService {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
});
})
.then(() => ({userID, email, referer}));
});
}
/**
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
waitForConditionTimeout: 8000,
baseUrl: 'http://localhost:3000',
baseUrl: 'http://localhost:3011',
users: {
admin: {
email: 'admin@test.com',
+3
View File
@@ -75,6 +75,9 @@ describe('/api/v1/auth/local', () => {
.send({email: 'maria@gmail.com', password: 'password!'})
.catch((err) => {
expect(err).to.have.status(401);
err.response.body.should.have.property('error');
err.response.body.error.should.have.property('metadata');
err.response.body.error.metadata.should.have.property('message', 'maria@gmail.com');
return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
})
+92
View File
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title>Confirm Email</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<style media="screen">
#root {
max-width: 400px;
padding-top: 100px;
margin: 0 auto;
background: #fff;
}
.coral-card-wide > .mdl-card__title {
color: #fff;
height: 176px;
background: #F47E6B url('/path/to/logo.jpg') center / cover;
}
.coral-card-wide > .mdl-card__menu {
color: #fff;
}
.error-console {
display: none;
margin-top: 10px;
border-radius: 4px;
background-color: pink;
color: red;
border: 1px solid red;
padding: 10px;
}
.error-console.active {
display: block;
}
</style>
</head>
<body>
<div id="root">
<div class="coral-card-wide mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Confirm Email Address</h2>
</div>
<div class="mdl-card__supporting-text">
Click the button below to confirm your new user account.
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" id="confirm-email">
Confirm
</a>
<div style="display: none" id="p2" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script>
$(function () {
function showError(message) {
$('.error-console').text(message).addClass('active');
}
function handleClick (e) {
e.preventDefault();
$('#p2').css('display', 'block');
$('.error-console').removeClass('active');
$.ajax({
url: '/api/v1/account/email/confirm',
contentType: 'application/json',
method: 'POST',
headers: {
'X-CSRF-Token': '<%= csrfToken %>'
},
data: JSON.stringify({token: location.hash.replace('#', '')})
}).then(function (success) {
location.href = success.redirectUri;
}).catch(function (error) {
showError(error.responseText);
});
}
$('#confirm-email').on('click', handleClick);
});
</script>
</body>
</html>
+1 -1
View File
@@ -1,3 +1,3 @@
<p>A email confirmation has been requested for the following account: <b><%= email %></b>.</p>
<p>To confirm the account, please visit the following link: <a href="http://example.com/email/confirm/endpoint#<%= token %>">http://example.com/email/confirm/endpoint#<%= token %></a></p>
<p>To confirm the account, please visit the following link: <a href="<%= rootURL %>/admin/confirm-email#<%= token %>">Confirm Email</a></p>
<p>If you did not request this, you can safely ignore this email.</p>
+1 -1
View File
@@ -4,6 +4,6 @@ A email confirmation has been requested for the following account:
To confirm the account, please visit the following link:
http://example.com/email/confirm/endpoint#<%= token %>
<%= rootURL %>/confirm/endpoint#<%= token %>
If you did not request this, you can safely ignore this email.