Initial pass at email confirmation

This commit is contained in:
Wyatt Johnson
2017-01-03 17:35:58 -07:00
parent b7380978f5
commit a7680ae5d9
23 changed files with 498 additions and 285 deletions
+6 -2
View File
@@ -6,6 +6,7 @@
const program = require('commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('../util');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
@@ -19,13 +20,16 @@ util.onshutdown([
*/
function processJobs() {
// Start the processor.
// Start the scraper processor.
scraper.process();
// Start the mail processor.
mailer.process();
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => scraper.shutdown()
() => kue.Task.shutdown()
]);
}
+7 -2
View File
@@ -5,6 +5,8 @@ const debug = require('debug')('talk:server');
const http = require('http');
const init = require('../init');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('../util');
@@ -119,15 +121,18 @@ startApp();
// Enable job processing on the thread if enabled.
if (program.jobs) {
// Start the processor.
// Start the scraper processor.
scraper.process();
// Start the mail processor.
mailer.process();
}
// Define a safe shutdown function to call in the event we need to shutdown
// because the node hooks are below which will interrupt the shutdown process.
// Shutdown the mongoose connection, the app server, and the scraper.
util.onshutdown([
() => program.jobs ? scraper.shutdown() : null,
() => program.jobs ? kue.Task.shutdown() : null,
() => mongoose.disconnect(),
() => server.close()
]);
+1 -1
View File
@@ -92,7 +92,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
export const fetchForgotPassword = email => dispatch => {
dispatch(forgotPassowordRequest(email));
coralApi('/users/request-password-reset', {method: 'POST', body: {email}})
coralApi('/account/password/reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
+3 -3
View File
@@ -14,10 +14,10 @@ const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
export const saveBio = (user_id, formData) => dispatch => {
dispatch(saveBioRequest());
coralApi(`/users/${user_id}/bio`, {method: 'PUT', body: formData})
.then(({settings}) => {
coralApi('/account/bio', {method: 'PUT', body: formData})
.then(() => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(settings));
dispatch(saveBioSuccess(formData));
})
.catch(error => dispatch(saveBioFailure(error)));
};
+1 -2
View File
@@ -31,8 +31,7 @@ export default function user (state = initialState, action) {
case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return initialState;
case actions.SAVE_BIO_SUCCESS:
return state
.set('settings', action.settings);
return state.set('settings', action.settings);
case actions.COMMENTS_BY_USER_SUCCESS:
return state.set('myComments', action.comments);
case assetActions.MULTIPLE_ASSETS_SUCCESS:
+1 -1
View File
@@ -583,7 +583,7 @@ paths:
description: The user that has been created.
schema:
$ref: '#/definitions/User'
/users/update-password:
/account/password/reset:
post:
parameters:
- name: body
+1 -1
View File
@@ -13,7 +13,7 @@ const ActionSchema = new Schema({
item_type: String,
item_id: String,
user_id: String,
metadata: Object, //Holds arbitrary metadata about the action.
metadata: Schema.Types.Mixed
}, {
timestamps: {
createdAt: 'created_at',
+5 -16
View File
@@ -1,7 +1,6 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const cache = require('../services/cache');
const WordlistSchema = new Schema({
banned: [String],
@@ -53,6 +52,10 @@ const SettingSchema = new Schema({
charCountEnable: {
type: Boolean,
default: false
},
requireEmailConfirmation: {
type: Boolean,
default: false
}
}, {
timestamps: {
@@ -121,19 +124,11 @@ const SettingService = module.exports = {};
*/
const selector = {id: '1'};
/**
* Cache expiry time in seconds for when the cached entry of the settings object
* expires. 2 minutes.
*/
const EXPIRY_TIME = 60 * 2;
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => {
return Setting.findOne(selector);
}).then((setting) => new Setting(setting));
SettingService.retrieve = () => Setting.findOne(selector);
/**
* This will update the settings object with whatever you pass in
@@ -146,12 +141,6 @@ SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
upsert: true,
new: true,
setDefaultsOnInsert: true
}).then((settings) => {
// Invalidate the settings cache.
return cache
.set('settings', settings, EXPIRY_TIME)
.then(() => settings);
});
/**
+59 -55
View File
@@ -4,7 +4,6 @@ const _ = require('lodash');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Action = require('./action');
const Comment = require('./comment');
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
@@ -31,6 +30,37 @@ if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
}
// ProfileSchema is the mongoose schema defined as the representation of a
// User's profile stored in MongoDB.
const ProfileSchema = new mongoose.Schema({
// ID provides the identifier for the user profile, in the case of a local
// provider, the id would be an email, in the case of a social provider,
// the id would be the foreign providers identifier.
id: {
type: String,
required: true
},
// Provider is simply the name attached to the authentication mode. In the
// case of a locally provided profile, this will simply be `local`, or a
// social provider which for Facebook would just be `facebook`.
provider: {
type: String,
required: true
},
// Metadata provides a place to put provider specific details. An example of
// something that could be stored here is the `metadata.confirmed_at` could be
// used by the `local` provider to indicate when the email address was
// confirmed.
metadata: {
type: mongoose.Schema.Types.Mixed
}
}, {
_id: false
});
// UserSchema is the mongoose schema defined as the representation of a User in
// MongoDB.
const UserSchema = new mongoose.Schema({
@@ -60,26 +90,7 @@ const UserSchema = new mongoose.Schema({
// Profiles describes the array of identities for a given user. Any one user
// can have multiple profiles associated with them, including multiple email
// addresses.
profiles: [new mongoose.Schema({
// ID provides the identifier for the user profile, in the case of a local
// provider, the id would be an email, in the case of a social provider,
// the id would be the foreign providers identifier.
id: {
type: String,
required: true
},
// Provider is simply the name attached to the authentication mode. In the
// case of a locally provided profile, this will simply be `local`, or a
// social provider which for Facebook would just be `facebook`.
provider: {
type: String,
required: true
}
}, {
_id: false
})],
profiles: [ProfileSchema],
// Roles provides an array of roles (as strings) that is associated with a
// user.
@@ -499,45 +510,43 @@ UserService.createPasswordResetToken = function (email) {
email = email.toLowerCase();
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
.then(user => {
.then((user) => {
if (!user) {
if (user === null) {
// since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the endpoint
return Promise.resolve(null);
// Since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the
// endpoint.
return;
}
const payload = {email, jti: uuid.v4(), userId: user.id, version: user.__v};
const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'});
const payload = {
jti: uuid.v4(),
email,
userId: user.id,
version: user.__v
};
return token;
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {algorithm: 'HS256'}, {
expiresIn: '1d'
});
});
};
/**
* verifies a jwt and returns the associated user
* Verifies a jwt and returns the associated user.
* @param {String} token the JSON Web Token to verify
*/
UserService.verifyPasswordResetToken = token => {
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => {
if (error) {
return reject(error);
jwt.verify(token, process.env.TALK_SESSION_SECRET, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
})
.then(decoded => {
/**
* TODO: check the jti from this decoded token in redis
* and make an entry if it does not exist.
* reject if entry already exists.
*/
return UserService.findById(decoded.userId);
});
.then(decoded => UserService.findById(decoded.userId));
};
/**
@@ -594,18 +603,13 @@ UserService.all = () => {
* Adds a new User bio
* @return {Promise}
*/
UserService.addBio = (id, bio) => (
UserModel.findOneAndUpdate({
id
}, {
$set: {
'settings.bio': bio
}
}, {
new: true
})
);
UserService.addBio = (id, bio) => UserModel.update({
id
}, {
$set: {
'settings.bio': bio
}
});
/**
* Add an action to the user.
+118
View File
@@ -0,0 +1,118 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
router.get('/', authorization.needed(), (req, res, next) => {
res.json(req.user);
});
/**
* this endpoint takes an email (username) and checks if it belongs to a User account
* if it does, create a JWT and send an email
*/
router.post('/password/reset', (req, res, next) => {
const {email} = req.body;
if (!email) {
return next('you must submit an email when requesting a password.');
}
User
.createPasswordResetToken(email)
.then((token) => {
// Check to see if the token isn't defined.
if (!token) {
// As it isn't, don't send any emails!
return;
}
return mailer.sendSimple({
app: req.app, // needed to render the templates.
template: 'email/password-reset', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL
},
subject: 'Password Reset Requested - Talk',
to: email
});
})
.then(() => {
// we want to send a 204 regardless of the user being found in the db
// if we fail on missing emails, it would reveal if people are registered or not.
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
// ErrPasswordTooShort is returned when the password length is too short.
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
ErrPasswordTooShort.status = 400;
// ErrMissingToken is returned in the event that the password reset is requested
// without a token.
const ErrMissingToken = new Error('token is required');
ErrMissingToken.status = 400;
/**
* expects 2 fields in the body of the request
* 1) the token that was in the url of the email link {String}
* 2) the new password {String}
*/
router.put('/password/reset', (req, res, next) => {
const {
token,
password
} = req.body;
if (!token) {
return next(ErrMissingToken);
}
if (!password || password.length < 8) {
return next(ErrPasswordTooShort);
}
User.verifyPasswordResetToken(token)
.then(user => {
return User.changePassword(user.id, password);
})
.then(() => {
res.status(204).end();
})
.catch(error => {
console.error(error);
next(authorization.ErrNotAuthorized);
});
});
router.put('/bio', authorization.needed(), (req, res, next) => {
const {
bio
} = req.body;
if (!bio) {
return next(new Error('You must submit a new bio'));
}
User
.addBio(req.user.id, bio)
.then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
+4
View File
@@ -31,6 +31,10 @@ router.delete('/', authorization.needed(), (req, res) => {
});
});
//==============================================================================
// PASSPORT ROUTES
//==============================================================================
/**
* This sends back the user data as JSON.
*/
+1
View File
@@ -17,6 +17,7 @@ router.use('/actions', authorization.needed(), require('./actions'));
router.use('/auth', require('./auth'));
router.use('/stream', require('./stream'));
router.use('/users', require('./users'));
router.use('/account', require('./account'));
// Bind the kue handler to the /kue path.
router.use('/kue', authorization.needed('admin'), require('../../services/kue').kue.app);
+9 -101
View File
@@ -1,12 +1,6 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const mailer = require('../../../services/mailer');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const resetEmailFile = fs.readFileSync(path.resolve(__dirname, '../../../views/password-reset-email.ejs'));
const resetEmailTemplate = ejs.compile(resetEmailFile.toString());
const authorization = require('../../../middleware/authorization');
router.get('/', authorization.needed('admin'), (req, res, next) => {
@@ -50,19 +44,22 @@ router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) =>
router.post('/:user_id/status', (req, res, next) => {
User
.setStatus(req.params.user_id, req.body.status, req.body.comment_id)
.then(status => {
res.json(status);
.then((status) => {
res.status(201).json(status);
})
.catch(next);
});
router.post('/', (req, res, next) => {
const {email, password, displayName} = req.body;
router.post('/', authorization.needed('admin'), (req, res, next) => {
const {
email,
password,
displayName
} = req.body;
User
.createLocalUser(email, password, displayName)
.then(user => {
res.status(201).json(user);
})
.catch(err => {
@@ -70,96 +67,7 @@ router.post('/', (req, res, next) => {
});
});
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
ErrPasswordTooShort.status = 400;
/**
* expects 2 fields in the body of the request
* 1) the token that was in the url of the email link {String}
* 2) the new password {String}
*/
router.post('/update-password', (req, res, next) => {
const {token, password} = req.body;
if (!password || password.length < 8) {
return next(ErrPasswordTooShort);
}
User.verifyPasswordResetToken(token)
.then(user => {
return User.changePassword(user.id, password);
})
.then(() => {
res.status(204).end();
})
.catch(error => {
console.error(error);
next(authorization.ErrNotAuthorized);
});
});
/**
* this endpoint takes an email (username) and checks if it belongs to a User account
* if it does, create a JWT and send an email
*/
router.post('/request-password-reset', (req, res, next) => {
const {email} = req.body;
if (!email) {
return next('you must submit an email when requesting a password.');
}
User
.createPasswordResetToken(email)
.then(token => {
if (token === null) {
return Promise.resolve('the email was not found in the db.');
}
const options = {
subject: 'Password Reset Requested - Talk',
from: process.env.TALK_SMTP_FROM_ADDRESS,
to: email,
html: resetEmailTemplate({
token,
// probably more clear to explicitly pass this
rootURL: process.env.TALK_ROOT_URL
})
};
return mailer.sendSimple(options);
})
.then(() => {
// we want to send a 204 regardless of the user being found in the db
// if we fail on missing emails, it would reveal if people are registered or not.
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
router.put('/:user_id/bio', (req, res, next) => {
const {user_id} = req.params;
const {bio} = req.body;
if (!bio) {
return next('You must submit a new bio');
}
User
.addBio(user_id, bio)
.then(user => {
res.json(user);
})
.catch((err) => {
next(err);
});
});
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
const {
action_type,
metadata
+4 -4
View File
@@ -4,12 +4,12 @@
selenium-standalone install
# Creating Admin Test User
{ echo admin@test.com; echo test; echo test; echo Admin Test User; echo admin;} | dotenv ./bin/cli-users create
{ echo admin@test.com; echo test; echo test; echo Admin Test User; echo admin;} | ./bin/cli-users create
# Creating Moderator Test User
{ echo moderator@test.com; echo test; echo test; echo Moderator Test User; echo moderator;} | dotenv ./bin/cli-users create
{ echo moderator@test.com; echo test; echo test; echo Moderator Test User; echo moderator;} | ./bin/cli-users create
# Creating Commenter Test User
{ echo commenter@test.com; echo test; echo test; echo Commenter Test User; echo ;} | dotenv ./bin/cli-users create
{ echo commenter@test.com; echo test; echo test; echo Commenter Test User; echo ;} | ./bin/cli-users create
npm start
npm start &
+76 -8
View File
@@ -1,11 +1,79 @@
const kue = require('kue');
const debug = require('debug')('talk:services:kue');
const redis = require('./redis');
module.exports = {
queue: kue.createQueue({
redis: {
createClientFactory: () => redis.createClient()
}
}),
kue
module.exports = {};
const kue = module.exports.kue = require('kue');
// Note that unlike what the name createQueue suggests, it currently returns a
// singleton Queue instance. So you can configure and use only a single Queue
// object within your node.js process.
const Queue = module.exports.queue = kue.createQueue({
redis: {
createClientFactory: () => redis.createClient()
}
});
module.exports.Task = class Task {
constructor({name, attempts = 3, delay = 1000}) {
this.name = name;
this.attempts = attempts;
this.delay = delay;
}
/**
* Add a new job to the queue.
*/
create(data) {
debug(`Creating new job for Queue[${this.name}]`);
return new Promise((resolve, reject) => {
let job = Queue
.create(this.name, data)
.attempts(this.attempts)
.delay(this.delay)
.backoff({type: 'exponential'})
.save((err) => {
if (err) {
return reject(err);
}
debug(`Job[${job.id}] created on Queue[${this.name}]`);
return resolve(job);
});
});
}
/**
* Process jobs for the queue.
*/
process(callback) {
return Queue.process(this.name, callback);
}
/**
* Shutdown running jobs.
*/
static shutdown() {
debug('Shutting down the Queue');
return new Promise((resolve, reject) => {
// Shutdown and give the queue 5 seconds to shutdown before we start
// killing jobs.
Queue.shutdown(5000, (err) => {
if (err) {
return reject(err);
}
debug('Queue shut down.');
resolve();
});
});
}
};
+85 -25
View File
@@ -1,4 +1,6 @@
const debug = require('debug')('talk:services:mailer');
const nodemailer = require('nodemailer');
const kue = require('./kue');
const smtpRequiredProps = [
'TALK_SMTP_FROM_ADDRESS',
@@ -7,11 +9,9 @@ const smtpRequiredProps = [
'TALK_SMTP_HOST'
];
smtpRequiredProps.forEach(prop => {
if (!process.env[prop]) {
console.error(`process.env.${prop} should be defined if you would like to send password reset emails from Talk`);
}
});
if (smtpRequiredProps.some(prop => !process.env[prop])) {
console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`);
}
const options = {
host: process.env.TALK_SMTP_HOST,
@@ -29,29 +29,89 @@ if (process.env.TALK_SMTP_PORT) {
const defaultTransporter = nodemailer.createTransport(options);
const mailer = {
const mailer = module.exports = {
/**
* sendSimple
*
* @param {Object} {from, to, subject, text = '', html = ''}
* @returns
*/
sendSimple({from, to, subject, text = '', html = '', transporter = defaultTransporter}) {
return new Promise((resolve, reject) => {
if (!from) {
reject('sendSimple requires a from address');
}
if (!to) {
reject('sendSimple requires a comma-separated list of "to" addresses');
}
if (!subject) {
reject('sendSimple requires a subject for the email');
}
* Create the new Task kue.
*/
task: new kue.Task({
name: 'mailer'
}),
return resolve(transporter.sendMail({from, to, subject, text, html}));
/**
* Render renders the template with the given locals and returns the rendered
* html/text.
*/
render(app, template, locals = {}) {
return new Promise((resolve, reject) => {
// Render the template with the app.render method.
app.render(template, locals, (err, rendered) => {
if (err) {
return reject(err);
}
return resolve(rendered);
});
});
},
sendSimple({app, template, locals, to, subject}) {
if (!to) {
return Promise.reject('sendSimple requires a comma-separated list of "to" addresses');
}
if (!subject) {
return Promise.reject('sendSimple requires a subject for the email');
}
return Promise.all([
// Render the HTML version of the email.
mailer.render(app, template, locals),
// Render the TEXT version of the email.
mailer.render(app, `${template}.txt`, locals)
])
.then(([html, text]) => {
// Create the job.
return mailer.task.create({
title: 'Mail',
message: {
to,
subject,
text,
html
}
});
});
},
/**
* Start the queue processor for the mailer job.
*/
process() {
debug(`Now processing ${mailer.task.name} jobs`);
return mailer.task.process(({id, data}, done) => {
debug(`Starting to send mail for Job[${id}]`);
// Set the `from` field.
data.message.from = process.env.TALK_SMTP_FROM_ADDRESS;
// Actually send the email.
defaultTransporter.sendMail(data.message, (err) => {
if (err) {
debug(`Failed to send mail for Job[${id}]:`, err);
return done(err);
}
debug(`Finished sending mail for Job[${id}]`);
return done();
});
});
}
};
module.exports = mailer;
};
+37 -5
View File
@@ -1,21 +1,53 @@
const mongoose = require('mongoose');
const debug = require('debug')('talk:db');
const queryDebuger = require('debug')('talk:db:query');
// Loading the formatter from Mongoose:
//
// https://github.com/Automattic/mongoose/blob/1a93d1f4d12e441e17ddf451e96fbc5f6e8f54b8/lib/drivers/node-mongodb-native/collection.js#L182
//
// so we can wrap parameters.
const formatter = require('mongoose').Collection.prototype.$format;
// Provide a newly wrapped debugQuery function which wraps the `debug` package.
function debugQuery(name, i, ...args) {
let functionCall = ['db', name, i].join('.');
let _args = [];
for (let j = args.length - 1; j >= 0; --j) {
if (formatter(args[j]) || _args.length) {
_args.unshift(formatter(args[j]));
}
}
let params = `(${_args.join(', ')})`;
queryDebuger(functionCall + params);
}
const enabled = require('debug').enabled;
// Append '-test' to the db if node_env === 'test'
let url = process.env.TALK_MONGO_URL || 'mongodb://localhost/coral-talk';
// Pull the mongo url out of the environment.
let url = process.env.TALK_MONGO_URL;
if (process.env.NODE_ENV === 'test') {
url += '-test';
// Reset the mongo url in the event it hasn't been overrided and we are in a
// testing environment. Every new mongo instance comes with a test database by
// default, this is consistent with common testing and use case practices.
if (process.env.NODE_ENV === 'test' && !url) {
url = 'mongodb://localhost/test';
}
// Use native promises
mongoose.Promise = global.Promise;
// Check if debugging is enabled on the talk:db prefix.
if (enabled('talk:db')) {
mongoose.set('debug', true);
// Enable the mongoose debugger, here we wrap the similar print function
// provided by setting the debug parameter.
mongoose.set('debug', debugQuery);
}
// Connect to the Mongo instance.
mongoose.connect(url, (err) => {
if (err) {
throw err;
+41 -6
View File
@@ -1,5 +1,6 @@
const passport = require('passport');
const User = require('../models/user');
const Setting = require('../models/setting');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
@@ -27,7 +28,7 @@ passport.deserializeUser((id, done) => {
* @param {User} user the user to be validated
* @param {Function} done the callback for the validation
*/
function ValidateUserLogin(user, done) {
function ValidateUserLogin(loginProfile, user, done) {
if (!user) {
return done(new Error('user not found'));
}
@@ -36,7 +37,36 @@ function ValidateUserLogin(user, done) {
return done(null, false, {message: 'Account disabled'});
}
return done(null, user);
// If the user isn't a local user (i.e., a social user).
if (loginProfile.provider !== 'local') {
return done(null, user);
}
// The user is a local user, check if we need email confirmation.
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
// If we have the requirement of checking that emails for users are
// verified, then we need to check the email address to ensure that it has
// been verified.
if (requireEmailConfirmation) {
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === loginProfile.id);
// This should never get to this point, if it does, don't let this past.
if (!profile) {
throw new Error('ID indicated by loginProfile is not on user object');
}
// 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(null, user);
});
}
//==============================================================================
@@ -54,7 +84,12 @@ passport.use(new LocalStrategy({
return done(null, false, {message: 'Incorrect email/password combination'});
}
return ValidateUserLogin(user, done);
// Define the loginProfile being used to perform an additional
// verificaiton.
let loginProfile = {id: email, provider: 'local'};
// Validate the user login.
return ValidateUserLogin(loginProfile, user, done);
})
.catch((err) => {
done(err);
@@ -70,9 +105,9 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET &&
}, (accessToken, refreshToken, profile, done) => {
User
.findOrCreateExternalUser(profile)
.then((user) =>
ValidateUserLogin(user, done)
)
.then((user) => {
return ValidateUserLogin(profile, user, done);
})
.catch((err) => {
done(err);
});
+21 -43
View File
@@ -1,7 +1,6 @@
const kue = require('./kue');
const debug = require('debug')('talk:services:scraper');
const Asset = require('../models/asset');
const JOB_NAME = 'scraper';
const metascraper = require('metascraper');
@@ -12,29 +11,27 @@ const metascraper = require('metascraper');
const scraper = {
/**
* creates a new scraper job and scrapes the url when it gets processed.
* Create the new Task kue.
*/
task: new kue.Task({
name: 'scraper'
}),
/**
* Creates a new scraper job and scrapes the url when it gets processed.
*/
create(asset) {
return new Promise((resolve, reject) => {
debug(`Creating job for Asset[${asset.id}]`);
let job = kue.queue
.create(JOB_NAME, {
title: `Scrape for asset ${asset.id}`,
asset_id: asset.id
})
.attempts(3)
.delay(1000)
.backoff({type: 'exponential'})
.save((err) => {
if (err) {
return reject(err);
}
debug(`Creating job for Asset[${asset.id}]`);
debug(`Created Job[${job.id}] for Asset[${asset.id}]`);
return scraper.task.create({
title: `Scrape for asset ${asset.id}`,
asset_id: asset.id
}).then((job) => {
return resolve(job);
});
debug(`Created Job[${job.id}] for Asset[${asset.id}]`);
return job;
});
},
@@ -48,6 +45,9 @@ const scraper = {
}));
},
/**
* Updates an Asset based on scraped asset metadata.
*/
update(id, meta) {
return Asset.update({id}, {
$set: {
@@ -68,10 +68,9 @@ const scraper = {
*/
process() {
debug(`Now processing ${JOB_NAME} jobs`);
debug(`Now processing ${scraper.task.name} jobs`);
// Process jobs with the processJob function.
kue.queue.process(JOB_NAME, (job, done) => {
scraper.task.process((job, done) => {
debug(`Starting on Job[${job.id}] for Asset[${job.data.asset_id}]`);
@@ -111,27 +110,6 @@ const scraper = {
done(err);
});
});
},
/**
* Shuts down the current queue to ensure that the application can shutdown
* cleanly.
*/
shutdown() {
return new Promise((resolve, reject) => {
// Shutdown and give the queue 5 seconds to shutdown before we start
// killing jobs.
kue.queue.shutdown(5000, (err) => {
if (err) {
return reject(err);
}
debug(`Processing for ${JOB_NAME} jobs stopped`);
resolve();
});
});
}
};
+11 -4
View File
@@ -19,22 +19,29 @@ describe('/api/v1/auth', () => {
});
});
const Setting = require('../../../../models/setting');
const settings = {id: '1'};
describe('/api/v1/auth/local', () => {
beforeEach(() => {
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria');
});
beforeEach(() => Promise.all([
User.createLocalUser('maria@gmail.com', 'password!', 'Maria'),
Setting.init(settings)
]));
describe('#post', () => {
it('should send back the user on a successful login', () => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!'})
.catch((res) => {
.then((res) => {
expect(res).to.have.status(200);
expect(res).to.be.json;
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('displayName', 'Maria');
})
.catch((err) => {
console.error(err);
});
});
@@ -1,6 +1,2 @@
<!-- extremely naive implementation of a password reset email -->
<p>We received a request to reset your password. If you did not request this change, you can ignore this email.<br />
If you did, <a href="<%= rootURL %>/admin/password-reset#<%= token %>">please click here to reset password</a>.</p>
<% if (process.env.NODE_ENV !== 'production') { %>
<p style="color: red"><%= token %></p>
<% } %>
+5
View File
@@ -0,0 +1,5 @@
We received a request to reset your password, click here to reset your password:
<%= rootURL %>/admin/password-reset#<%= token %>
If you did not request this change, you can ignore this email.
+2 -2
View File
@@ -117,9 +117,9 @@
}
$.ajax({
url: '/api/v1/users/update-password',
url: '/api/v1/account/password/reset',
contentType: 'application/json',
method: 'POST',
method: 'PUT',
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
}).then(function (success) {
location.href = '<%= redirectUri %>';