mirror of
https://github.com/wassname/talk.git
synced 2026-07-12 06:38:12 +08:00
Initial pass at email confirmation
This commit is contained in:
+6
-2
@@ -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
@@ -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()
|
||||
]);
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -31,6 +31,10 @@ router.delete('/', authorization.needed(), (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT ROUTES
|
||||
//==============================================================================
|
||||
|
||||
/**
|
||||
* This sends back the user data as JSON.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
@@ -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.
|
||||
@@ -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 %>';
|
||||
|
||||
Reference in New Issue
Block a user