mirror of
https://github.com/wassname/talk.git
synced 2026-07-23 13:10:20 +08:00
resend verify email
This commit is contained in:
@@ -22,6 +22,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());
|
||||
@@ -32,7 +33,18 @@ export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(addItem(user, 'users'));
|
||||
})
|
||||
.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
|
||||
@@ -138,7 +150,20 @@ export const checkLogin = () => dispatch => {
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
};
|
||||
|
||||
export const requestConfirmEmail = () => dispatch => {
|
||||
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());
|
||||
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,7 @@ const initialState = Map({
|
||||
error: '',
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
emailConfirmationFailure: false,
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
@@ -33,6 +34,7 @@ export default function auth (state = initialState, action) {
|
||||
error: '',
|
||||
passwordRequestFailure: null,
|
||||
passwordRequestSuccess: null,
|
||||
emailConfirmationFailure: false,
|
||||
successSignUp: false
|
||||
}));
|
||||
case actions.CHANGE_VIEW :
|
||||
@@ -101,6 +103,8 @@ 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);
|
||||
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 _",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, FormField, Spinner} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
@@ -6,60 +6,89 @@ 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,
|
||||
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>
|
||||
</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 = {
|
||||
handleResendConfirmation: PropTypes.func.isRequired,
|
||||
handleChangeEmail: PropTypes.func.isRequired,
|
||||
emailToBeResent: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default SignInContent;
|
||||
|
||||
@@ -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,6 +40,8 @@ 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);
|
||||
@@ -71,6 +75,17 @@ class SignInContainer extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeEmail(e) {
|
||||
const {value} = e.target;
|
||||
console.log('handleChangeEmail', value);
|
||||
this.setState({emailToBeResent: value});
|
||||
}
|
||||
|
||||
handleResendConfirmation(e) {
|
||||
e.preventDefault();
|
||||
this.props.requestConfirmEmail(this.state.emailToBeResent);
|
||||
}
|
||||
|
||||
addError(name, error) {
|
||||
return this.setState(state => ({
|
||||
errors: {
|
||||
@@ -159,6 +174,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,8 +1,8 @@
|
||||
export default {
|
||||
en: {
|
||||
'signIn': {
|
||||
emailConfirmCTA: 'Please verify your email address. We sent an email to {0} for verification.',
|
||||
requestNewConfirmEmail: 'Request another email',
|
||||
emailConfirmCTA: 'Please verify your email address.',
|
||||
requestNewConfirmEmail: 'Request another email:',
|
||||
notYou: 'Not you?',
|
||||
loggedInAs: 'Logged in as',
|
||||
facebookSignIn: 'Sign in with Facebook',
|
||||
@@ -30,8 +30,8 @@ export default {
|
||||
},
|
||||
es: {
|
||||
'signIn': {
|
||||
emailConfirmCTA: 'Por favor verifique su correo electronico. Le enviamos un correo a {0} para verificar.',
|
||||
requestNewConfirmEmail: 'Enviar otro correo',
|
||||
emailConfirmCTA: 'Por favor verifique su correo electronico.',
|
||||
requestNewConfirmEmail: 'Enviar otro correo:',
|
||||
notYou: 'No eres tu?',
|
||||
loggedInAs: 'Entraste como',
|
||||
facebookSignIn: 'Entrar con Facebook',
|
||||
|
||||
@@ -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!', {
|
||||
@@ -110,6 +124,10 @@ const ErrNotAuthorized = new APIError('not authorized', {
|
||||
status: 401
|
||||
});
|
||||
|
||||
// ErrSettingsNotInit is returned when the settings object isn't available in
|
||||
// the mongo collection.
|
||||
const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli settings init` to setup the settings');
|
||||
|
||||
module.exports = {
|
||||
ExtendableError,
|
||||
APIError,
|
||||
@@ -125,5 +143,7 @@ module.exports = {
|
||||
ErrAssetCommentingClosed,
|
||||
ErrNotFound,
|
||||
ErrInvalidAssetURL,
|
||||
ErrSettingsNotInit,
|
||||
ErrAuthentication,
|
||||
ErrNotAuthorized
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.
|
||||
@@ -142,6 +143,25 @@ 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;
|
||||
|
||||
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, process.env.TALK_ROOT_URL)
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-25
@@ -531,41 +531,51 @@ module.exports = class UsersService {
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static createEmailConfirmToken(userID, email, referer) {
|
||||
static createEmailConfirmToken(userID = null, email, referer) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
const tokenOptions = {
|
||||
jwtid: uuid.v4(),
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
};
|
||||
|
||||
email = email.toLowerCase();
|
||||
let userPromise;
|
||||
|
||||
return UsersService
|
||||
.findById(userID)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(new Error('user not found'));
|
||||
}
|
||||
if (!userID) {
|
||||
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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 userPromise.then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(new Error('user not found'));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
referer,
|
||||
userID
|
||||
};
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
|
||||
jwtid: uuid.v4(),
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
});
|
||||
});
|
||||
// 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'));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
referer,
|
||||
userID: user.id
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, tokenOptions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,9 @@ describe('/api/v1/auth/local', () => {
|
||||
.send({email: 'maria@gmail.com', password: 'password!'})
|
||||
.catch((err) => {
|
||||
err.response.should.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);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user