mirror of
https://github.com/wassname/talk.git
synced 2026-07-01 22:06:55 +08:00
Merge branch 'master' into mobile-width-fix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+10
-2
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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
@@ -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
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
waitForConditionTimeout: 8000,
|
||||
baseUrl: 'http://localhost:3000',
|
||||
baseUrl: 'http://localhost:3011',
|
||||
users: {
|
||||
admin: {
|
||||
email: 'admin@test.com',
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user